在实现useState之前我们要先了解一下useState的使用,在react中,如果要更新数据的话,一般是定义一个useState函数,然后从中拿到state
和setState
,当更新数据时,只需要调用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