React hooks原理浅谈

avatar
@哈啰

react的工作流程

fiber是react的基本工作单元,所有的操作都要基于它实现。其实fiber就类似一个个element元素,react的工作流程其实就是遍历fiber tree。

image.png

image.png

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不同阶段调用的方法不同

image.png

image.png

因而useState在mount时实际上调用的是mountState方法,update时调用的是updateState方法(updateState是updateReducer的语法糖写法)。

image.png

当mount阶段依次调用hook时,第一个生成的hook是挂在当前组件节点(reactFiber节点)的memoizedState属性上,之后生成的hook则依次挂在上一个hook的next属性上。

image.png

image.png

所以当我们将hook置于循环、条件语句、嵌套函数中时,那么hook链表就会错乱,会导致hook调用顺序不可预测,那就没法保证组件内部状态一致性。当我们setState时会返回一个初始state和用于更新state的函数。

image.png

我们知道mount阶段useState调用的是mountState,查看源码后知道返回的其实是[hook.memoizedState, dispatch]。

image.png

dispatch其实就是dispatchSetState通过bind到当前组件节点、更新队列后的函数。

image.png

假设执行了3次setOrder,分别是 setOrder('1')、setOrder('2')、setOrder('3')。

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')时

image.png

setOrder('2') 时

image.png

setOrder('3')时

image.png

上面说过updte阶段实际调用的是updateReducer方法,这个方法中主要做了这几件事

  • 如果有新的更新还未处理,则加入当前更新链表中
  • 清空待更新链表(queue.pending = null)
  • 从待更新链表的第一个循环迭代更新,直到最后一个
  • 更新当前hook状态值并return出去

image.png

通过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函数。

image.png

无论useEffect的依赖项是否相同都会调用pushEffect函数,唯一区别的是pushEffect函数的第一个参数是不同的,如果依赖项没有变化则第一个参数是hookFlags,反之则是HookHasEffect|hookFlags(标识存在副作用更新钩子)。

image.png

image.png

image.png

image.png

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。

(本文作者:尚军平)

关注公众号「哈啰技术」,第一时间收到最新技术推文。