实现一个精简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 分钟前
node_modules 明明写进 .gitignore,却还是被 push/commit 的情况
前端·javascript·vue.js
前端小超超41 分钟前
如何配置capacitor 打包的ios app固定竖屏展示?
前端·ios·web app
nightunderblackcat1 小时前
新手向:从零理解LTP中文文本处理
前端·javascript·easyui
kyle~1 小时前
python---PyInstaller(将Python脚本打包为可执行文件)
开发语言·前端·python·qt
User:你的影子1 小时前
WPF ItemsControl 绑定
开发语言·前端·javascript
会有钱的-_-1 小时前
基于webpack的场景解决
前端·vue.js·webpack·安全性测试
LFly_ice2 小时前
学习React-10-useTransition
前端·学习·react.js
咔咔一顿操作2 小时前
【CSS 3D 交互】实现精美翻牌效果:从原理到实战
前端·css·3d·交互·css3
知识分享小能手2 小时前
React学习教程,从入门到精通,React 构造函数(Constructor)完整语法知识点与案例详解(16)
前端·javascript·学习·react.js·架构·前端框架·vue
召摇2 小时前
Nue.js深度解析:极简主义前端框架的革新实践
前端·node.js