背景
工作快两年了,这个时候开始思考如何继续提升自己,keep going。既然如此那就一起来学习一下React的源码吧! 那就首先从工作中最常用的 useState 开始吧~
带着问题来学习
- 如何存储状态?
- 不同的hook是如何区分的?
- 如何更新状态,并返回最新状态?
let's go
首先要知道hooks出来之前,函数组件无法存储状态,一般是被动接收参数,然后渲染。hooks出来之后,便可以将状态存储起来,那么是如何存储的呢?
看看react源码中的定义: 方便理解这里只需要记住三个值 memoizedState(当前状态)、queue(更新队列)、next(下一个hook对象)
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};
第一个问题的答案:每一个hook执行之后都会生成一个hook对象。并且这些对象是用链表结构存储的,这也就是为什么hook不能写在if条件中(避免hooks链表顺序错误)
代码实现
首先我们先搭好函数的框架,大致分为以下几步:
- 获取(生成)hook 对象,并添加到链表尾部
- 更新 hook 对象中的状态
- 返回最新的状态和一个修改函数的函数,即 [state, setState]
function useState(initState) {
/**
* TODO
* 1、获取hook对象
*/
/**
* TODO
* 2、更新hook对象
*/
/**
* 3、返回
*/
return [hook.memorizedState, dispatchAction.bind(null, hook.queue)]
}
步骤拆分
1. 获取hook对象并添加到链表尾部
- 首先需要区分是否是第一次加载,这里简单用一个变量值来表示是否是第一次加载
如果首次加载,初始化hook对象
不是首次加载,则在链表中找到这个hook节点 - 然后将这个 hook 对象添加到链表尾部
到这里就会有疑问,首次添加结点时如何找到链表尾部? 非首次添加如何找到对应的hook结点?这里就要去理解链表结构与 workInProgressHook 指针的关系。
1. hooks 链表存储在 fiberNode 中
2. workInProgressHook 是一个指针,指向当前的链表中正在执行的 hook 对象
3. 当 workInProgressHook 为 null 时,代表当前在加载第一个 hook
4. hook在链表中严格按顺序加载,所以要找到当前 hook, 就要找上一个hook(workInProgressHook)
具体实现见代码与注释:
let workInProgressHook = null;
let currentFiberNode = {
memorizedState: null
};
function useState(initState) {
/**
* 1、获取hook对象
* 分支1:首次加载-> 初始hook对象
* 分支2:触发更新-> 更新每个hook对象
*/
// 如果是首次加载 为了方便理解这里用一个简单的值来代表首次加载,源码中不是这样的
if (isFirstTimeMount) {
let hook = {
memorizedState: initState,
next: null,
queue: {
pending: null,
},
}
// workInProgressHook 是一个指针,指向当前的链表中最后一个 hook 对象
// 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
if (!workInProgressHook) {
// currentFiberNode 代表当前的fiberNode节点 将链表的首个节点挂在 fiberNode 的 memorizedState 上
currentFiberNode.memorizedState = hook;
workInProgressHook = hook;
} else {
// 直接将新的 hook 对象添加到链表尾部, 并更新指针位置
workInProgressHook.next = hook;
workInProgressHook = workInProgressHook.next;
}
} else {
// 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
if (!workInProgressHook) {
// currentFiberNode.memorizedState 链表首个节点
hook = currentFiberNode.memorizedState;
workInProgressHook = hook;
} else {
// 获取hook,更新指针位置
hook = workInProgressHook.next;
workInProgressHook = hook.next;
}
}
/**
* TODO
* 2、更新hook对象
*/
/**
* 3、返回
*/
return [hook.memorizedState, dispatchAction.bind(null, hook.queue)]
}
2. 更新hook对象
- 首先需要思考什么时候会更新 hook 对象?然后发现当组件内调用 setState 函数(dispatchAction)时,就会进行更新,并且重新渲染组件。所以首先需要了解的是这个 dispatchAction 函数
- 当 dispatchAction 执行完毕后,组件会重新渲染,就会再次调用 useState hook,此时就会去更新该 hook 的更新队列 queue。 queue是一个循环链表结构,queue.pending 总是指向最新的更新对象(update)。
这里就来看 dispatchAction 中做了什么:
- 生成一个 update 对象(每次调用都会生成一个update对象)
- 将 update 对象添加到循环链表中,并将指针 queue.pending 指向自己
- 组件渲染更新(调用 fiber 中的调度器,这里简化为执行 fiberSchedule 函数)
const dispatchAction = function(queue, action){
// 每次更新都要生成一个 update 对象 同样该对象也是也链表形式存在,且是一个循环链表
let update = {
action,
next: null,
};
// 判断是否是首次更新,首次更新,指向自己
if (queue.pending === null) {
update.next = update;
} else {
/**
* 链表结构:
* update1 -> update2 -> update3 -> update1
* 现在需要将update4插入循环链表中
* queue.pending指向最新的update 此例中指向update3,即可直接将update4指向update1(queue.pending.next)
* update3(queue.pending).next = update4
*/
// update的下一个节点指向update1
update.next = queue.pending.next ;
queue.pending.next = update;
}
// 更新queue.pengding指向
queue.pending = update;
// 完成更新后,渲染组件,这里就进入了fiber的调度过程,这里简化为fiberSchedule()
fiberSchedule()
}
当组件重新渲染更新时,又会执行 hook 函数。此时的流程则是:
通过 workInProgressHook 找到对应的 hook
找到 hook 后通过 queue (update循环链表)获取到最新的 update 对象
更新 hook.memoizedState 状态。
最后将 hook 的最新状态返回。
function useState(initState) {
/**
* 1、获取hook对象
* 分支1:首次加载-> 初始hook对象
* 分支2:触发更新-> 更新每个hook对象
*/
// 如果是首次加载 为了方便理解这里用一个简单的值来代表首次加载,源码中不是这样的
if (isFirstTimeMount) {
let hook = {
memorizedState: initState,
next: null,
queue: {
pending: null,
},
}
// workInProgressHook 是一个指针,指向当前的链表中最后一个 hook 对象
// 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
if (!workInProgressHook) {
// currentFiberNode 代表当前的fiberNode节点 将链表的首个节点挂在 fiberNode 的 memorizedState 上
currentFiberNode.memorizedState = hook;
workInProgressHook = hook;
} else {
// 直接将新的 hook 对象添加到链表尾部, 并更新指针位置
workInProgressHook.next = hook;
workInProgressHook = workInProgressHook.next;
}
} else {
// 当 workInProgressHook 不存在 ->代表当前正在加载第一个 hook 对象
if (!workInProgressHook) {
// currentFiberNode.memorizedState 链表首个节点
hook = currentFiberNode.memorizedState;
workInProgressHook = hook;
} else {
// 获取hook,更新指针位置
hook = workInProgressHook.next;
workInProgressHook = hook.next;
}
}
/**
* 2、更新hook对象
* 使用useState返回值是 [state, setState], 函数内部则是[hook.memorizedState, dispatchAction.bind(null, hook.queue)]
* 外部调用setState(value) 则是调用 dispatchAction.bind(null, hook.queue)(value)
* dispatchAction执行的时候,如何把value赋值给update对象?
* react中使用了while循环来更新update对象
*/
if(hook.queue.pending !== null){
first = update = hook.queue.pending.next;
do{
action = update.action;
hook.memoizedState = typeof action === 'function' ? action(hook.memoizedState) : action;
update = update.next
}while(update !== null && update !== first)
}
return [hook.memorizedState, dispatchAction.bind(null, hook.queue)]
}
const dispatchAction = function(queue, action){
// 每次更新都要生成一个 update 对象 同样该对象也是也链表形式存在,且是一个循环链表
let update = {
action,
next: null,
};
// 判断是否是首次更新,首次更新,指向自己
if (queue.pending === null) {
update.next = update;
} else {
/**
* 链表结构:
* update1 -> update2 -> update3 -> update1
* 现在需要将update4插入循环链表中
* queue.pending指向最新的update 此例中指向update3,即可直接将update4指向update1(queue.pending.next)
* update3(queue.pending).next = update4
*/
// update的下一个节点指向update1
update.next = queue.pending.next ;
queue.pending.next = update;
}
// 更新queue.pengding指向
queue.pending = update;
// 完成更新后,渲染组件,这里就进入了fiber的调度过程,这里简化为fiberSchedule()
fiberSchedule()
}
至此开头的三个问题就都有了答案:
1、如何存储状态? 解答:所有的数据都存储在 hook 对象中,包括每一次更新产生的状态(update对象)。
2、如何区分不同的 hooks? 解答:hooks 通过链表存储在 fiberNode 中,不同 hook 生成的对象不同,并且严格按照顺序执行,hook 对象与组件中的执行 hooks 顺序一一对应。
3、如何更新状态,并返回最新状态? 解答:通过调用 useState 返回的 dispatchAction 函数生成 update 对象,此时会重新渲染组件,更新过程中会再次调用 hooks 函数,就会去获取当前 hook 对象的最新状态并返回。
最后还有一处疑问:既然 queue.pending 指向最新的 update 对象,为什么还要用 while 循环遍历链表的方式来获取最新状态?直接拿 queue.pending 不就可以了嘛?
有解释说是因为 fiber 更新中 如果有优先级高的任务先执行的话,可能上一次的更新任务还没有完成,所以使用遍历的形式来更新。