精确步骤分解
-
state或props变化 (触发更新):- 当
setState被调用或父组件传递下来的props发生变化时,React 会标记该组件(及其子组件)为“需要更新”(这个过程是通过更新某个fiber节点的flag标记来完成的)。 - 注意:
setState调用是异步的批量更新,不会立即触发render。 setState之后, 会通知React调度器有一个更新需要处理, 同时在 fiber 节点中插入一些标记。
- 当
不是立即执行: 当你调用
setState时,React 不会立即同步地更新组件的state以及触发重新渲染。相反,React 会将这些setState调用放入一个队列中。为什么
setState是异步的呢? 主要还是为了性能的考虑。避免多次渲染:如果每次
setState都立即触发渲染,那么在短时间内多次调用setState会导致组件多次重新渲染,这会非常消耗性能。批量更新:React 会在合适的时机(例如:当前事件循环结束后,浏览器准备绘制之前)将队列中的
setState调用合并为一次更新,然后触发组件的重新渲染 (也就是我们说的批量更新)。优化渲染:React 会把多个
setState调用合并成一次。发生在事件处理函数中:通常,在 React 事件处理函数中(例如:
onClick、onChange等), React 会自动进行批量更新。
-
开始
Render阶段 (Reconciliation):- React 会调用组件函数,执行函数组件的代码。
- 这个阶段,会执行 Hooks,例如
useState返回新的值和更新函数、useEffect会被"遇到"/“注册” (收集 Effect 信息),useMemo也会重新计算。- 注意: 这个 "执行", 并不是 执行
useEffect的回调函数。
- 注意: 这个 "执行", 并不是 执行
- 这个阶段,会执行 Hooks,例如
- React 会构建新的 Fiber 树,并在构建的过程中利用 diff 算法 进行新旧 Fiber 树的比较。
- 对比过程中,React 会检查当前 Fiber 树节点对应的组件的
props和state是否发生了改变。
- 对比过程中,React 会检查当前 Fiber 树节点对应的组件的
- Diff 算法会找出需要更新的 DOM 节点,并将这些节点标记为“需要更新”。
- 如果没有需要更新的 dom 节点, 此时会跳过
commit阶段, 只执行effect。
- 如果没有需要更新的 dom 节点, 此时会跳过
- React 会调用组件函数,执行函数组件的代码。
-
useEffect的收集和准备:- 在
Render阶段(即 Fiber 创建和 Diff 过程中):- React 会依据本次渲染的
useEffect和上次渲染的useEffect依赖值比较,来判断是否更新useEffect。
- React 会依据本次渲染的
- 如果没有依赖更新, 那么上次的副作用会被保留, 这次
render不会添加新的effect。 - 如果依赖更新, 把上次的
useEffect的清除函数记录下来,同时, 也把当前的useEffect的回调函数记录下来,等待执行。- 注意:这里仅仅是 "收集"
useEffect信息,并放入本次渲染的副作用列表,useEffect的回调函数代码不执行。
- 注意:这里仅仅是 "收集"
- 在
-
进入 Commit 阶段:
- React 会真正地开始执行
UI 更新的操作。 - 这个阶段会不可打断。
- 在这个阶段,React 从 fiber 树 中获取需要更新的 DOM 节点,并执行对应的 DOM 操作。
- 紧接着会执行
useLayoutEffect队列里所有的useLayoutEffect的回调函数, 并 清理上次更新的useLayoutEffect副作用。useLayoutEffect是 同步执行,会阻塞浏览器的绘制。
- React 会真正地开始执行
-
DOM 更新完成:
- 此时更新还仅仅发生在浏览器内部,还没有同步到屏幕上,你可以粗略地理解成 react 完成了 DOM 的写入操作,完成了 JS 代码的任务
-
useEffect的排队执行:- React 会按照顺序, 遍历整个 Fiber 树节点,找出本次渲染更新的
useEffect。 - 对每一个本次渲染需要更新的
useEffect:- 如果上次
useEffect有清除函数,则把 上次的清除函数放入微任务队列,等待执行。 - 把当前的
effector(也就是useEffect的回调函数) 放入微任务队列中。
- 如果上次
- React 会按照顺序, 遍历整个 Fiber 树节点,找出本次渲染更新的
-
浏览器绘制:
- 浏览器进行屏幕绘制, 这时用户才能看到界面变化。
-
useEffect回调函数执行 (微任务队列执行):- 在浏览器完成绘制 之后, 微任务队列开始执行。
- 微任务队列里,
useEffect的清除函数会先于useEffect回调函数执行。
- 微任务队列里,
- React 异步 执行
useEffect中的回调函数, 这些回调函数中可以执行副作用,比如:- 发送 API 请求
- 操作 DOM
- 监听事件
- 设置定时器
- 注意: 这些操作在浏览器进行页面渲染之后进行执行,所以不会阻塞 UI 渲染。
- 在浏览器完成绘制 之后, 微任务队列开始执行。
useLayoutEffect在浏览器绘制完成之前执行,useEffect在浏览器绘制完成之后执行
精简总结:
- 状态/属性变化: 标记组件需要更新。
Render阶段:- 执行组件,执行 hooks,收集
useEffect信息。 - 构建 Fiber 树,计算 Diff, 识别需要更新的 UI。
- 执行组件,执行 hooks,收集
Commit阶段: 更新 DOM, 执行useLayoutEffect。- DOM 更新完成: 屏幕上可见新的 UI。
useEffect放入微任务队列:- 异步执行
useEffect: 执行useEffect回调。 避免误解的关键:
Render!= DOM 更新 和 执行副作用:Render只是计算和比较 Fiber 树, 找出需要更新的节点,并收集需要执行的副作用信息。useEffect的 “触发” != “执行”:useEffect的回调在render阶段并不会执行, 只是收集信息等待执行。useLayoutEffect同步执行: DOM 更新后,useLayoutEffect的代码同步执行。- 异步副作用: DOM 更新后,
useEffect的代码执行,是由微任务队列异步调度执行, 而且是在浏览器绘制之后。 结合Hook生命周期: - Mount: 组件首次渲染时,执行一次上述所有步骤。
- Update: 当
state/props变化时, 重复步骤 1-8。 每次更新时,如果依赖更新会先执行上次的 cleanup function。 - Unmount: 组件卸载时,会执行
useEffect返回的清除函数。 简单的流程图帮助理解: