实现一个精简React -- 利用update函数,实现useState(9)

在实现useState之前我们要先了解一下useState的使用,在react中,如果要更新数据的话,一般是定义一个useState函数,然后从中拿到statesetState,当更新数据时,只需要调用setState函数并传入新的值即可。

useState的使用

js 复制代码
const Foo = () => {
    const [count, setCount] = useState(10)
    function handleClick() {
        setCount((c) => c + 1)
    }

    return (
        <div>
            Foo: {count}
            <button onClick={handleClick}>click</button>
        </div>
    )
}

代码实现

从上面的例子可以看到,useState是接受了一个初始值,并返回了初始值count和一个函数setCount,当更新数据时,调用了setCount并传入一个函数(也可以不是函数),这个函数中接受count的值并返回处理后的新值。

step1.实现最简单的useState

js 复制代码
function useState(initial) { // 接收初始值initial
    // 创建state的对象
    const stateHook = {
        state: initial
    };

    function setState(action) {
        // 调用传过来的函数,重新赋值
        stateHook.state = action(stateHook.state);
        
        // 触发更新....
    }

    return [stateHook.state, setState];
}

这样我们就实现了一个非常基础的useState,但这个useState更新时并没有触发页面数据的更新,在前一篇文章更新props 中我们有说到,触发更新是通过给nextUnitOfFier重新赋值后触发更新,并定义了一个更新函数 update。因此我们可以在调用 setState 函数后再给nextUnitOfFier重新赋值后触发更新。

step2.setState时触发更新

js 复制代码
function useState(initial) {
    const stateHook = {
        state: initial
    };

    function setState(action) {
        stateHook.state = action(stateHook.state);
        
        // 触发更新
        wipRoot = currentFiber;
        wipRoot.alternate = currentFiber;
        nextUnitOfFier = wipRoot;
    }

    return [stateHook.state, setState];
}

这样的会有一个问题 :触发更新时会调用 updateFunctionComponent 方法并再次调用函数组件来拿到最新的vdom,其中会重新触发 useState 方法, 并传递初始值。 这样当我们再次调用setState时的state的值相当于没有变化。

因此我们可以将上一次更新的值存储起来,在新旧dom更新的篇章中我们定义了一个值alternate来表示旧的dom树,因此我们将旧的stateHook值存储到alternate上,这样更新时会获取之前的fiber并通过alterante来创建新旧关系,就可以拿到上一个值。

js 复制代码
function useState(initial) {
    // 拿到当前的fiber
    let currentFiber = wipFiber;
    // 获取旧的vdom上有没有stateHook,有的话则使用旧的stateHook
    const oldHook = wipRoot.alternate?.stateHook;
    // 创建state的对象
    const stateHook = {
        state: oldHook ? oldHook.state : initial
    };
		
    // 给当前的fiber赋值,下次调用useState时先使用上一个
    currentFiber.stateHook = stateHook;

    function setState(action) {
        // 调用传过来的函数,重新赋值
        stateHook.state = action(stateHook.state);
        // 触发更新
        wipRoot = currentFiber;
        wipRoot.alternate = currentFiber;

        nextUnitOfFier = wipRoot;
    }

    return [stateHook.state, setState];
}

注意:这里采用的事闭包的写法,函数中引用了外部的变量,会导致变量无法被销毁,所以当修改变量值时,其他引用此变量的地方也会修改

step3.解决当存在多个useState时,useState会覆盖的问题

如果定义多个useState的话,我们会发现一个问题,当重复调用调用useState时,stateHook里面的值会被后一个给覆盖掉。所以可以新建一个数组stateHooks的全局变量来存储useState,当更新时再依次取出来使用即可。

js 复制代码
let stateHooks; // 创建数组,存储stateHook
let stateHookIndex;  // 创建标识符,来调用对应的stateHook
// 这两个值可以在创建dom树也就是调用updateFunctionComponent函数时进行初始化

function useState(initial) {
    let currentFiber = wipFiber;
    // 依次调用
    const oldHook = wipRoot.alternate?.stateHooks[stateHookIndex];
    const stateHook = {
        state: oldHook ? oldHook.state : initial
    };

    // step1、当调用useState时存储起来
    stateHooks.push(stateHook);
    // step2、标识符+1
    stateHookIndex++;

    // 给当前的fiber赋值
    currentFiber.stateHooks = stateHooks;

    function setState(action) {
        stateHook.state = action(stateHook.state);

        wipRoot = currentFiber;
        wipRoot.alternate = currentFiber;

        nextUnitOfFier = wipRoot;
    }

    return [stateHook.state, setState];
}


function updateFunctionComponent(fiber) {
    // 当处理函数组件时进行初始化,保证每一个函数组件的useState都是自己的
    stateHooks = [];
    stateHookIndex = 0;

    wipFiber = fiber;
    const children = [fiber.type(fiber.props)];
    reconcileChildren(fiber, children);
}

step4.兼容和性能优化

优化点:

如果一个setState重复调用的话,每次更新值都会导致页面重新渲染更新。

假设一个变量从10 → 11 → 12 → 13,中间会经历三次更新,但其实只需要更新一次,即10 → 13,中间的更新时没有必要的。

所以可以将action存储起来,等到最后一次的时一次性调用完后拿到最新的值,参与更新。

如果值相对于上次没有变化,则不用更新。

兼容点: 如果useState穿的是个普通的值而不是函数,我们就自己用函数包装一下。

js 复制代码
function useState(initial) {
    let currentFiber = wipFiber;
    const oldHook = wipRoot.alternate?.stateHooks[stateHookIndex];
    const stateHook = {
        state: oldHook ? oldHook.state : initial,
        queue: oldHook ? oldHook.queue : [],  //创建参数存储action
    };

    // 更新组件时统一调用 获取到最新的值
    stateHook.queue.forEach((action) => {
        stateHook.state = action(stateHook.state);
    });
    // 调用完后清空
    stateHook.queue = [];

    stateHooks.push(stateHook);
    stateHookIndex++;

    currentFiber.stateHooks = stateHooks;

    function setState(action) {
        // 提前一步获取到action的值
        let eagerState = typeof action === "function" ? action(stateHook.state) : action;
        // 将提前获取到的state跟现在的state作对比,相同则终止
        if (eagerState === stateHook.state) {
            return;
        }
        // 兼容,如果没有传函数,则转化成函数。
        // 将action push 到queue中,最后一次更新统一调用。
        stateHook.queue.push(
            typeof action !== "function" ? () => action : action
        );

        wipRoot = currentFiber;
        wipRoot.alternate = currentFiber;

        nextUnitOfFier = wipRoot;
    }

    return [stateHook.state, setState];
}

至此,就实现了一useState

相关推荐
打野赵怀真17 分钟前
前端资源发布路径怎么实现非覆盖式发布(平滑升级)?
前端·javascript
顾林海26 分钟前
Flutter Dart 流程控制语句详解
android·前端·flutter
tech_zjf27 分钟前
装饰器:给你的代码穿上品如的衣服
前端·typescript·代码规范
xiejianxin52028 分钟前
如何封装axios和取消重复请求
前端·javascript
parade岁月29 分钟前
从学习ts的三斜线指令到项目中声明类型的最佳实践
前端·javascript
狼性书生31 分钟前
electron + vue3 + vite 渲染进程与渲染进程之间的消息端口通信
前端·javascript·electron
阿里巴巴P8资深技术专家31 分钟前
使用vue3.0+electron搭建桌面应用并打包exe
前端·javascript·vue.js
coder_leon35 分钟前
Vite打包优化实践:从分包到性能提升
前端
shmily_yyA35 分钟前
【2025】Electron 基础一 (目录及主进程解析)
前端·javascript·electron
吞吞071138 分钟前
浅谈前端性能指标、性能监控、以及搭建性能优化体系
前端