I. 实现useState
在前一天的课程中,我们是通过调用React.update()函数返回的update函数来触发重新渲染的。今天的课程,我们会把它替换成React.js中的useState 写法。
1. 使用useState语句
按照React.js的写法使用useState.
import React from "./core/React.js"
function Foo() {
const [count, setCount] = React.useState(10)
function handleClick() {
setCount(pre => pre + 2)
}
return (
<div>
<h1>Foo : {count}</h1>
<button onClick={handleClick}>click</button>
</div>
)
}
function App() {
return (
<div>
<h1>App</h1>
<Foo></Foo>
</div>
)
}
export default App
2. 补齐useState定义
useStatefunction 以数组的形式返回state和setState- setState包含之前的react.update 操作
为了简单,我们先支持useState()参数是一个函数的情况setState(pre => pre + 2)
// function update() {
// // 函数组件在运行此HOC的时候顺便记录当前fiber节点wipFiber
// let currentFiber = wipFiber;
// return () => {
// wipRoot = {
// ...currentFiber,
// alternate: currentFiber,
// };
// nextWorkOfUnit = wipRoot;
// }
// }
function useState(initial) {
let currentFiber = wipFiber;
const stateHook = {
state: initial,
}
const setState = (action) => {
stateHook.state = action(stateHook.state)
console.log(stateHook.state);
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
nextWorkOfUnit = wipRoot;
}
return [stateHook.state, setState];
}
【问题】console.log打印更新的state值,确实是12,但是一直都是12不再往上增加了。而且页面上也没显示更新后的数值。
【分析】每一次setState 导致的state更新,要保存到fiber节点上:
- 这里只是调用了action来得到更新的数值,但是这个数值并没有存储下来供下一次使用。每次都是读的initial值。我们需要添加一个值来记录它,并保存在fiber上。
let currentFiber = wipFiber;
// 取出上一次渲染时的state
let oldHook = currentFiber.alternate?.stateHook;
// 如果有上一次的值就用上一次的值,没有的话就用初始值
const stateHook = {
state: oldHook ? oldHook.state : initial,
}
// 将拿到的值存储在fiber上
currentFiber.stateHook = stateHook;
state初始化以及更新流程图:
3. 扩展到支持多个state:
- 用
stateHooks数组存储一个节点中的多个useState;用stateHookIndex来区分同节点内的不同的useState
//全局变量
let stateHooks;
let stateHookIndex;
- 对于每个fiber节点,进行初始化
function updateFunctionComponent(fiber) {
stateHooks = [];
stateHookIndex = 0;
wipFiber = fiber;
// 如果是函数组件,不直接为其append dom
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
- 按index取值,更新hooks数组;每调用一次useState, index++。每一次运行函数组件函数,都会运行
useState。从而const stateHook被更新,setState()中调用的stateHook也被更新,返回的state也更新。
function useState(initial) {
let currentFiber = wipFiber;
// 根据index找到对应的stateHook
let oldHook = currentFiber.alternate?.stateHooks[stateHookIndex];
// 存储本次的state供setState使用
const stateHook = {
state: oldHook ? oldHook.state : initial,
}
stateHooks.push(stateHook);
stateHookIndex++;
//刷新hooks数组
currentFiber.stateHooks = stateHooks;
const setState = (action) => {
stateHook.state = action(stateHook.state);
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
nextWorkOfUnit = wipRoot;
}
return [stateHook.state, setState];
}
💡启发
这也就解释了为什么useState 只能在组件里直接调用,不能在if里使用,这样会导致index混乱。
II. 批量执行action
实现
【问题】有时,用户的一个行为会触发多次render:当setState 被一个操作多次触发的时候
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
【解决】批量执行state更新,然后再渲染fiber节点,减少不必要的渲染。由于这个优化是针对每一个state,所以队列也添加到每个stateHook上。
let stateHooks;
let stateHookIndex;
function useState(initial) {
let currentFiber = wipFiber;
// 根据index找到对应的stateHook
let oldHook = currentFiber.alternate?.stateHooks[stateHookIndex];
// 存储本次的state供setState使用
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;
const setState = (action) => {
stateHook.queue.push(typeof action === "function" ? action : () => action)
// stateHook.state = action(stateHook.state)
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
nextWorkOfUnit = wipRoot;
}
return [stateHook.state, setState];
}
支持传值而非action函数
将值转化为一个返回该值得箭头函数() => action
完整语句:typeof action === "fuction" ? action : () => action;
代码分析
【问】上面的代码中,action入队之后,nextWorkOfUnit会被更新,看起来fiber节点也会入队。这是如何避免多次渲染造成的浪费的?
【答】问题中的预设并不一定成立:每次调用setState,nextWorkOfUnit会被更新:nextWorkOfUnit = wipRoot,但是workloop 需要等待浏览器空闲时间来执行。一种情况是,workloop还没有等到可以执行的时间,nextWorkOfUnit就又被第二次、第三次的setState刷新了。当workloop等到时间来执行的时候,实际上最后一次的nextWorkOfUnit = wipRoot已经完成了,这时候进行的渲染实际上就是基于最新的action queue的了。
【佐证】 添加log打印可以印证上面的回答
const setState = (action) => {
stateHook.queue.push(typeof action === "function" ? action : () => action)
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
nextWorkOfUnit = wipRoot;
console.log("setState nextWorkOfUnit", nextWorkOfUnit);
}
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
console.log("get nextWorkOfUnit", nextWorkOfUnit);
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
...
联想、对比
对比之前在setState中直接执行action得到结果的做法,批量执行会把action存入alternate。然后触发渲染,进而执行useState。此时再批量执行action。
- 第一次执行useState是进行初始化并提供setState函数
- 调用了setState函数后会将action入队以及将fiber节点入队
- fiber节点的入队会带来渲染函数组件,执行函数组件函数就会再度执行useState 进而运行队列里的所有action
【问】加入queue执行的机制中,怎么保证最后一个批次的action queue也被执行?
【答】如上图所示,action的入队这个行为定义在setState函数中。当setState函数被调用,首先执行的是更新之前的alternate中的action queue。然后再渲染和批量执行,这样很顺利的就得到了本次需要计算出来的state值。
III. 减少不必要的更新
检测state值是否变了,没变的话就不重新渲染
const setState = (action) => {
// 处理值一样的情况
const eagerState = typeof action === "function" ? action(stateHook.state) : action
if (eagerState === stateHook.state) return
stateHook.queue.push(typeof action === "function" ? action : () => action)
// stateHook.state = action(stateHook.state)
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
nextWorkOfUnit = wipRoot
}
华丽的分割线
至此,本天的课程其实已经结束了,但是我的思考没有停止:
【疑问】如果每次都执行action,那action入队就没必要了吧?都已经得到执行结果了没必要再执行一遍
【试验】不使用action queue,直接设置stateHook.state = action(stateHook.state)。添加console
log看看。
const setState = (action) => {
const eagerState = typeof action === "function" ? action(stateHook.state) : action
if (eagerState === stateHook.state) return
//stateHook.queue.push(typeof action === "function" ? action : () => action)
stateHook.state = eagerState
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
nextWorkOfUnit = wipRoot
console.log("eagerState", eagerState, "set nextWorkOfUnit", nextWorkOfUnit)
}
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
console.log("get nextWorkOfUnit", nextWorkOfUnit)
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
...
【结果】会发现即使不使用queue,多次的setState 调用也并不会直接触发多次的渲染。这还是因为setState的间隔较短,并未分不到不同的浏览器空闲时间内。可见本次课程中的queue方式优化并不严谨。