React架构图解——Reconcile与Renderer篇

382 阅读4分钟

最近在回顾之前看过的React源码解析,改正了之前很多错误的理解。本文主要分享对ReconcileRenderer部分的认识,可对应类式组件的生命周期进行思考。

1、React设计理念

这部分属于前置知识,可以重点关注Fiber架构的实现原理与工作原理部分:Fiber节点中存储的某些参数的含义,如stateNode、tag、type;双缓存Fiber树等

React设计理念.jpg

2、React架构(省略Scheduler部分)

React架构的整个执行过程可以分为两个阶段(可对应ClassComponent生命周期的两个阶段):render阶段和commit阶段,其中render阶段由协调器Reconcile完成,commit阶段由渲染器Renderer实现。前者主要进行Fiber树的计算(异步,可中断的),后者则根据effectList完成DOM操作及页面的渲染(同步,不可中断)。总体上即根据页面新的JSX在内存中构建workInProgress Fiber,在此过程中可复用current Fiber React架构.png

(1)render阶段

先看一下render阶段,通过可中断的递归实现,其中“递”进行深度优先遍历,目的是为每个WorkInProgress Fiber节点创建子节点并进行连接,到子节点进行“归”,子节点有兄弟节点则兄弟“递”,否则父“归”。

1️⃣递

这个过程中主要调用beginWork()函数,该函数会首先根据current指针是否为null判断在挂载阶段还是更新阶段。挂载阶段需要根据tagclassComponent、FunctionComponent、HostComponent。。。)来创建子Fiber节点并返回给workInProgress.children。更新阶段则需要根据当前current节点和workInProgress节点的propstype及优先级判断是否节点可复用,不可复用最终会通过diff算法复用current节点,并为子Fiber节点增加EffectTagPlacement、Deletion、Update...)字段来保存commit阶段要执行的DOM操作,对于函数式组件,若使用了useEffectuseLayoutEffect也需要增加EffectTagHasEffect、NoEffect...)可复用还需要检查子树是否需要更新,同样的生成的子节点进行连接后,子节点作为下一轮“递”的输入。

1️⃣归

这个过程中主要调用compeleteWork()函数,会根据tag区别对待,这里以HostComponent为例。首先会判断是挂载阶段还是更新阶段,前者Fiber节点中没有对应的DOM对象即stateNode,所以会为其生成DOM节点并将子孙DOM插入,若update阶段stateNodenull则进行同样操作,否则更新updateQueue(保存如className、onClick、style等)并将子孙DOM插入。最后需要将有effectTag的Fiber挂在EffectList中,后续commit阶段就不需要再次遍历Fiber树寻找更新,该链表可在rootFiberfirstEffect中读取。

(2)commit阶段

整个commit阶段主要分为三部分:DOM操作前before mutation,DOM操作mutation,DOM操作后layout,每个部分都会遍历effectList数组。

1️⃣before mutation

这个步骤主要作三件事:DOM的blurautoFocus处理逻辑、对于类式组件,执行生命周期函数getSnapshotBeforeUpdate()、对于类式组件使用useEffect()的,在scheduleCallback()中调度flushPassiveEffects(),在layout阶段之后将effectList赋值给rootWithPendingPassiveEffects,并执行flushPassiveEffects()flushPassiveEffects()会遍历rootWithPendingPassiveEffects,即执行useEffect()的回调函数。

2️⃣mutation

这个过程会执行所有useLayoutEffect()的销毁函数,通过遍历effectList并根据effectTag对DOM进行不同的增、删、改(细节见图)操作,更新ref等

3️⃣layout

这个阶段DOM操作已经完成,对于类式组件会执行生命周期函数类如componentDidMount()componentDidUpdate();对于函数式组件,会调用useLayoutEffect()的回调函数,调度useEffect()的销毁、回调函数,赋值ref等。

由于JS的同步执行阻塞了主线程,所以此时JS已经可以获取到新的DOM,但是浏览器对新的DOM并没有完成渲染useLayoutEffectuseEffect的执行时机有所不同,虽然它们的回调都在layout阶段被处理,但具体的执行顺序和浏览器渲染的时机是关键。useLayoutEffect的回调是同步执行(立即执行)的,在浏览器绘制之前执行,而useEffect的回调是异步的,React会调度useEffect的回调,但实际执行是在浏览器完成绘制之后,这会导致在useLayoutEffect中进行的DOM操作可以立即反映到屏幕上,而useEffect可能会有闪烁,因为浏览器可能已经绘制过一次了。

// React 提交阶段伪代码
function commitRoot() {
  // 1. 执行 useLayoutEffect 的销毁函数
  commitBeforeMutationEffects();
  
  // 2. 更新 DOM
  commitMutationEffects();
  
  // 3. 执行 useLayoutEffect 的回调
  commitLayoutEffects(); // 🔴 同步执行
  
  // 4. 浏览器绘制(Paint)
  requestPaint(); 
  
  // 5. 调度 useEffect 的回调
  scheduleEffectCallbacks(); // ⚡ 异步执行(微任务)
}

React的渲染流程分为渲染阶段和提交阶段,提交阶段又包括pre-commitcommitpost-``commit,而useLayoutEffectcommit阶段同步执行,此时DOM已经更新但浏览器还未重绘,而useEffect则是在post-commit阶段异步执行,此时浏览器已经完成绘制。

image.png image.png 参考文献:react.iamkasong.com