本文作者: 苦瓜梅
前置知识:
1、每个组件对应一个fiber节点 2、react hooks是按顺序执行的,不能嵌套
问题:
1、hooks怎么实现按顺序执行?
2、使用useState更新不同对象,react hooks是怎么区分更新哪一个的?
3、同时更新state多次,怎么来保证同步的?
工作原理
以最简单计数器为例:
export function Demo() {
const [count, updateCount] = useState(1)
const [name, updateName] = useState('a')
const handleClick = () => {
updateCount(count => count +1)
updateCount(count => count +1)
}
const handleChange = () => {
updateName(name => name +'a')
updateName(name => name +'b')
}
return (<>
<p>useState简易实现</p>
<button onClick={handleClick}>ADD</button>
<button onClick={handleChange}>Change</button>
</>)
}
通过定义useState这个函数,useState的返回值是 [count, updateCount],count永远保存的最新的值,updateCount是用来更新这个count的方法。updateCount的改变会触发组件重新render。对name的更新同理。所以react hooks工作流程大致如下:
1、通过一些方式产生更新,并且更新会使得组件重新render(函数组件Demo1重新执行)
2、组件render时useState返回的是更新后的结果
其中,更新分为mount和update两个阶段:
1、mount时赋予初始值(initialValue),产生mount的更新
2、通过点击button让值加1,产生update的更新
开始实现一个setState
react hooks采用单向链表的形式进行链接,所以需要一个存储hooks链表的地方,这个地方就是fiber对象,定义一个简单fiber对象如下:
let fiber = {
memoizedState: null, //保存组件对应的hooks链表 ,每个链表节点是一个hook对象
stateNode: Demo //指向函数组件
}
让fiber的stateNode执行函数组件,这样就可以通过调用fiber.stateNode()让函数组件重新执行;通过fiber.memoizedState以链表的形式记录第一个hook到最后一个hook的信息
既然有链表,就得有指针,记录当前工作流下执行的是哪个hook,所以定义一个变量workInProgressHook作为指针。
定义一个变量isMount区分是在挂载阶段还是更新阶段
let isMount = true //用来区分是挂载还是更新
let workInProgressHook = null //
然后定义一个调度器schedule来执行组件rerender,并且每次重新执行,需要将workInProgressHook 复位指向第一个hook (这其实也是为什么每次更新,都是按顺序执行一遍hooks)
function schedule(){
workInProgressHook = fiber.memoizedState;
fiber.stateNode()
}
这时,让我们简单的写下useState代码,大概逻辑如下:
function useState(initialState) {
// 当前useState使用的hook会被赋值该该变量
let hook;
if (isMount) {
// ...mount时需要生成hook对象
} else {
// ...update时从workInProgressHook中取出该useState对应的hook
}
//处理更新,并且return [state,setStateAction],返回当前状态,和设置状态的方法
}
定义hook对象,并且初始化让fiber.memoizedState指向第一个useState,然后第二个hook进来(执行第二个useState),让第一个指向第二个
代码如下:
function useState(initialState){
let hook
if (isMount) {
// ...mount时需要生成hook对象
hook = {
memoizedState:initialState,
next:null
}
if(!fiber.memoizedState){
fiber.memoizedState = hook
}else{
workInProgressHook.next = hook
}
workInProgressHook = hook
} else {
// ...update时从workInProgressHook中取出该useState对应的hook
}
let baseState = hook.memoizedState;
//TODO: 处理更新
//...
return [baseSate,dispatchAction]
}
既然react hook是通过每次更新是组件rerender,那么我们就需要定义一个更新对象,用来存储每次更新的信息,由于更新的时候可能是同时更新了多次,如下所示调用了三次setCount:
const handleClick = () => {
updateCount(count => count +1)
updateCount(count => count +1)
updateCount(count => count +1)
}
模拟react对更新操作的处理,会将这些更新同样的用连起来,通过环状单向链表的形式,使用环状的目的是,react中对更新操作有优先级的概念,环状链表可以方便定位到任何一次高优先级的更新位置去执行,而暂时摒弃低优先级的更新。定义更新的结构如下:
const update = {
action,// 更新执行的函数 count => count +1
next: null// 与同一个Hook的其他更新形成链表
}
问: 何时进行更新对象的链接?更新的信息存在哪?
答: 将更新的信息存储在对应的hook对象中,在hook对象中添加一个属性queue,queue的pending指针就指向更新的链表。在调用dispatchAction时将这些更新链接起来。
所以我们更新下hook对象结构,更新后的hook对象结构:
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
环状单向链表示意图:
定义dispatchAction 方法如下:
function dispatchAction (queue,action) { //调用hook的更新方法,实现更新(queue是存放update信息的地方,通过改变queue。pending和其next指针完成链接)
// 创建update
const update = {
action,
next: null
}
// 环状单向链表操作
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
schedule(); // 模拟React开始调度更新
}
环状链表串联示意图:
每调用一次dispatchAction让这些更新对象串联起来,通过schedule使组件重新执行,然后在useState里执行挂载在hook的更新
function useState(initialState) {
let hook;
if (isMount) {
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
// 移动workInProgressHook指针
workInProgressHook = hook;
} else {
// update时找到对应hook
hook = workInProgressHook;
// 移动workInProgressHook指针
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending)
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)]
总结:
通过链表的形式连接hooks对象,将链表的信息存储在fiber对象的memoizedState中以供更新使用,每次调用dispatchAction时将更新操作连接,更新信息存在对应hook对象的queue中,然后schedule重新执行函数组件,进入useState,查看对应的hook的queue,执行。最后将结果返回。
最后: clone一份react源码,找到ReactFiberHooks.old.js,搜索useState,可以看到useState存在多个HooksDispatcher里,找到对应mountState、updateState方法的实现,可以看到与简易的useState实现思路是一致的。
对比与React的区别:
1、React Hooks没有使用isMount变量,而是在不同时机使用不同的dispatcher。(react 源码里搜:HooksDispatcher)
2、React Hooks有中途跳过更新的优化手段。
3、React Hooks有批量更新batchedUpdates,当在click中触发三次updateNum,精简React会触发三次更新,而React只会触发一次。
4、React Hooks的update有优先级概念,可以跳过不高优先的update,简易实现虽然模拟react有单向环状链表,但是其实并没有利用到环状结构的优势。
参考文章: @魔法师卡颂 www.yuque.com/liangxincha… react.iamkasong.com/process/fib…