React hooks原理浅谈

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。

(本文作者:尚军平)

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

相关推荐
上单带刀不带妹2 分钟前
手写 Vue 中虚拟 DOM 到真实 DOM 的完整过程
开发语言·前端·javascript·vue.js·前端框架
杨进军23 分钟前
React 创建根节点 createRoot
前端·react.js·前端框架
ModyQyW38 分钟前
用 AI 驱动 wot-design-uni 开发小程序
前端·uni-app
说码解字1 小时前
Kotlin lazy 委托的底层实现原理
前端
爱分享的程序员1 小时前
前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
前端·javascript·node.js
翻滚吧键盘1 小时前
vue 条件渲染(v-if v-else-if v-else v-show)
前端·javascript·vue.js
vim怎么退出1 小时前
万字长文带你了解微前端架构
前端·微服务·前端框架
你这个年龄怎么睡得着的1 小时前
为什么 JavaScript 中 'str' 不是对象,却能调用方法?
前端·javascript·面试
Java水解2 小时前
前端常用单位em/px/rem/vh/vm到底有什么区别?
前端
CAD老兵2 小时前
Vite 如何借助 esbuild 实现极速 Dev Server 体验,并支持无 source map 的源码调试
前端