react的工作流程
fiber是react的基本工作单元,所有的操作都要基于它实现。其实fiber就类似一个个element元素,react的工作流程其实就是遍历fiber tree。
performUnitOfWork函数会执行当前的fiber节点,然后把这个fiber的子节点赋值给workInProgress,当子节点不存在时,就把兄弟节点赋值给workInProgress。
上层的workLoopSync函数的 while循环会根据下个workInProgress去遍历。这样就能实现一个深度优先遍历,从而把所有的fiber执行完毕。
在performUnitOfWork函数中分为两个阶段:
1.beginWork
- 执行render函数以及hook,然后返回jsx
- 对返回的jsx执行diff,如果有新的fiber节点生成则赋值给workInProgress继续迭代
2.completeWork回溯fiber tree
- 生成dom节点,组成一个虚拟dom树
- 处理props
- 把所有含有副作用的fiber节点用firstEffect和lastEffect链接起来,组成一个链表,然后在commit阶段遍历执行
在completeWork执行到根节点时,证明所有的工作已经完成,就会执行commitRoot,它又分为三个阶段:
1.before mutation(执行dom操作前)
调用挂载前的生命周期钩子,比如getSnapshotBeforeUpdate,调度useEffect
2.mutation(执行dom操作)
执行dom操作,如果有组件被删除,那么还会调用componentWilUnmount或useLayoutEffect的销毁函数
3.layout(执行dom操作后)
- 切换fiber tree
- 调用componentDidUpdate、componentDidMount或者useLayoutEffect的回调函数。
- layout结束后,执行之前调度的useEffect的创建和销毁函数。
接下来我们重点看下hook的实现。
useState不同阶段调用的方法不同
因而useState在mount时实际上调用的是mountState方法,update时调用的是updateState方法(updateState是updateReducer的语法糖写法)。
当mount阶段依次调用hook时,第一个生成的hook是挂在当前组件节点(reactFiber节点)的memoizedState属性上,之后生成的hook则依次挂在上一个hook的next属性上。
所以当我们将hook置于循环、条件语句、嵌套函数中时,那么hook链表就会错乱,会导致hook调用顺序不可预测,那就没法保证组件内部状态一致性。当我们setState时会返回一个初始state和用于更新state的函数。
我们知道mount阶段useState调用的是mountState,查看源码后知道返回的其实是[hook.memoizedState, dispatch]。
dispatch其实就是dispatchSetState通过bind到当前组件节点、更新队列后的函数。
假设执行了3次setOrder,分别是 setOrder('1')、setOrder('2')、setOrder('3')。
ini
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
setOrder('1')时
setOrder('2') 时
setOrder('3')时
上面说过updte阶段实际调用的是updateReducer方法,这个方法中主要做了这几件事
- 如果有新的更新还未处理,则加入当前更新链表中
- 清空待更新链表(queue.pending = null)
- 从待更新链表的第一个循环迭代更新,直到最后一个
- 更新当前hook状态值并return出去
通过setOrder(1)、setOrder(2)、setOrder(3)的图例我们知晓 queue.pending.next 即更新链表的总是指向第一个update,而queue.pending总是指向最后一个。
一开始将update(1)赋值给update,然后获取newState也就是1,接下来update=update.next,此时update成了update(2),依次遍历,终止条件为update === null || update === first,也就是当update = update(3)时满足了终止条件,此时newState = 3,取到了最新值。这样可以保证整个update链表都循环了一遍同时取到的是链表中的最后一个节点。所以无论setState多少次,拿到的总是最新的值(问题2)。
useEffect不同阶段调用的方法不同
mount阶段,useEffect调用的是mountEffect,update阶段,useEffect调用的是updateEffect函数。
无论useEffect的依赖项是否相同都会调用pushEffect函数,唯一区别的是pushEffect函数的第一个参数是不同的,如果依赖项没有变化则第一个参数是hookFlags,反之则是HookHasEffect|hookFlags(标识存在副作用更新钩子)。
pushEffect主要做两件事:
- 创建 effect 对象并返回
- 把这个 effect 链接到 currentlyRenderingFiber的updateQueue属性上
结论:useEffect会生成一个effect对象,保存在hook节点的memoizedState中,同时也更新到currentlyRenderingFiber的updateQueue中,组成循环链表。每次render时,都会对比一下新旧hook里保存的effect的deps有没有改变,如果改变了,那就更新memoizedState为最新的effect,并且把effect的tag标识为存在副作用,然后currentlyRenderingFiber的updateQueue属性里。在commit阶段,beforeMutation中,对有副作用的fiber,发起一个异步调度。等到layout结束后,这个异步调度的回调开始执行,处理effect的创建和销毁回调。它会先调用effect的destroy,再调用create。
(本文作者:尚军平)
关注公众号「哈啰技术」,第一时间收到最新技术推文。