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。

(本文作者:尚军平)

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

相关推荐
ZhengEnCi40 分钟前
12 种高级 UI 交互效果及对应 AI 提示词:从按压回弹到底部抽屉的物理感教学
前端·css·交互设计
IT_陈寒3 小时前
Redis大key删除为什么会导致服务卡顿?我的线上事故复盘
前端·人工智能·后端
用户2705077725244 小时前
MCP 最小 Server 实测:3 个 JSON-RPC 报错与修法
前端
用户499335025074 小时前
一人VIP账号多人共用:Web 代理里的会话共享怎么实现
前端
用户老李4 小时前
无线电自学教程 05:第一台收音机——从矿石机到 SDR
前端
IPIPD4 小时前
Node.js请求遇到503怎么重试?先解析Retry-After,再做有上限退避
前端
葡萄城技术团队4 小时前
dsh-spreadjs-editor 的集成链路:把 SpreadJS Designer 嵌进 Agent 工作台
前端
FlyWIHTSKY5 小时前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
码云之上5 小时前
10 颗一模一样的「Edit」:中后台 Web Agent 怎么找到对的那一行
前端·人工智能·浏览器
量化分析码农6 小时前
【Python量化多因子实战 04】因子暴露不是 alpha:市值与行业中性化实战
前端·后端