|
React16更新渲染源码分析
React16更新渲染源码分析
陈祥芬@贝壳找房
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年12月10日 11:59
一、引言React16 架构分为三层(如图 1.1 所示):Scheduler(调度器)Reconciler(协调器)Renderer(渲染器)图 1.1 React16 架构图Scheduler用来调度任务,优先级高的任务优先进入Reconciler执行,待Reconciler找出变化的组件后将它们交给Renderer,由Renderer将其渲染到页面上。我们先来看个例子,感受下这三层架构的工作。如上所示的 demo,一个 ClickCounter 组件,它返回两个孩子元素:span 和 button。点击 button 会对 state.count 进行加 1 操作。另外,添加了一个生命周期函数 componentdidUpdate,它在必要的时刻会被调用。那么,点击 button,产生一个更新 count:0->1,Scheduler 会先进行调度,由于本例中没有更高优的任务,因此 react 会进入到以下活动:更新 state.count 值。找出变化的节点,发现 count 的变化导致 span 需要更新。对 span 元素执行更新操作。执行函数 componentdidUpdate。其中活动 1、2 属于 Reconciler 的工作,即 render 阶段。活动 3、4 属于 Renderer 的工作,即 commit 阶段。本次将和大家一起,从源码角度聊一聊 render 阶段和 commit 阶段的工作流程,看看他们在 setState 后是如何完成更新渲染的。二、概念介绍为了更好的理解 render 阶段和 commit 阶段的工作,我们先来了解下这两个阶段涉及到的一些基础概念。2.1 fiber 节点与 fiber 树React16 所有的工作都是基于 fiber 节点(可以理解为一个工作单元)来进行的。每一个 react 元素都会对应一个 fiber 节点,fiber 节点通过属性连接构成 fiber 树。 fiber 节点的生成如图 2.1 所示,react 通过createFiberFromTypeAndProps方法,根据 react 元素(react element)的 type 和 props 来构建出相应的 fiber 节点。图 2.1 构建 fiber 节点fiber 节点属性说明,以 span 节点(图 2.2)为例alternate: 保存了该 fiber 节点的替代节点。child:指向孩子元素。effectTag:副作用标签,在 commit 阶段根据改标签的值,执行不同的操作。memoizeProps:当前 props。memoizeState:当前状态。pendingProps:即将更新的 props。nextEffect:指向下一个需要执行操作的 fiber 节点。return: 指向父元素。sibling:指向兄弟元素。stateNode: 保存一些引用,例如类实例或原生 dom 等。tag: 标签。原生 dom 值为 5,类组件值为 1。type:类型。类组件为构造函数,dom 节点为 html 标签。updateQueue:更新队列。图 2.2 span fiber 节点其中updateQueue共有三种类型的数据结构,我们这里介绍两种:ClassComponent(类组件类型)、hostComponent(原生dom类型)。ClickCounter fiber 中的 updateQueue(图 2.3),它是一个链表结构。其中,pending 存放着将要处理的更新内容。payload 存放的是 setState 传入的函数。next 指向下一个需要更新的内容。pending 是一个环状链表,这里的 pending.next 指向的是它自己。既然是环状,如何区分最后一个更新内容呢,当 pending.next 的值和第一个链表的更新内容一致,则认为已经到达最后一个了。图 2.3 ClassComponent updateQueuespan fiber节点的updateQueue(如图2.4),它是一个队列。其中,偶数下标表示key,奇数下标表示value。对span fiber节点来说,点击button后,状态count从0变成了1,那么对应的span节点更新队列就是两个元素,key 为children属性,其对应的值为1。图 2.4 hostComponent updateQueuefiber树fiber 节点通过属性:return、child 和 sibling 来建立连接,构成 fiber 树(如图 2.5)。图 2.5 fiber 树2.2 rootfiber与fiberroot图 2.6 含有 fiberroot 的 fiber 树ReactDOM.render(, document.querySelector("#app"))React 在执行 ReactDOM.render 时会创建 fiberroot 和 rootfiber。fiberroot 就是整个 fiber 树的根(图 2.6 中的 FiberRootNode)。rootFiber 为容器。对于本 demo 来说 rootfiber 就是 id 为 app 对应的节点。其中 fiberroot 通过 current 指向 当前屏幕已经渲染的树(如图 2.6)。2.3 current 树与 work-in-progress 树图 2.7 current 树与 wip 树如图2.7所示:current指向的是当前屏幕已经渲染的树。在react 更新时,会构建work-in-progress(wip)树,它是即将渲染到屏幕中的树,一旦wip树更新到屏幕中,它将变成current树。current 树与 wip 树直接通过 alternate 相互指向。2.4 finishedwork 树与 effectList图 2.8 finishedwork 树与 effectListfinishedwork,react在更新过程中(render阶段),会构建wip树,render阶段结束后的wip树就是finishedwork树。finishedwork树,挂在在rootfiber上。effectList,render阶段结束后,会产生一些需要执行副作用操作的fiber 节点,这些fiber节点挂在在finishedwork.rootfiber上,他们以firstEffect 为起点,通过nexctEffect 指针连接,从而形成一条单向链表。commit 阶段主要遍历 effectList 这条链表,根据 tag 值和 effectTag,去执行相应的操作,从而完成渲染。注意:在 react 中进行 dom 操作、执行生命周期函数的活动都可以称之为副作用。在 fiber 节点中通过 effectTag(react17 叫 flags)来表示。不同的值,代表要执行不同的工作。例如:dom 节点副作用操作主要有:新增(Placement:2)、更新(Update:4)和删除(Deletion:8)。执行 componentDidUpdate 对应的 effectTag 为 Update。三、render 阶段3.1 工作思路render 段主要找出变化的 fiber 节点,产生带有 effectTag 单向链表的 wip 树。该阶段可分为两部分,“递”阶段和“归”阶段,其核心函数为:beginWorkcompleteWork该阶段的工作思路如下:“递”阶段:从 rootfiber 开始向下深度优先遍历,遍历到的每个 Fiber 节点调用 beginWork 方法,该方法会根据传入的 Fiber 节点创建子 fiber 节点(将 react 元素与 current fiber 节点进行 diff,产生新的 fiber 节点 ),并将这两个 fiber 节点连接起来。当遍历到叶子节点时,就会进入“归”阶段。“归”阶段:在“归”阶段会调用 completeWork 处理 Fiber 节点(生成 updateQueue 等)。当某个 fiber 节点执行完 completeWork,如果其存在兄弟 fiber 节点(即 fiber.sibling !== null),会进入其兄弟 Fiber 的“递”阶段。如果不存在兄弟 fiber 节点,会进入父级 fiber 节点的“归”阶段。递”和“归”阶段会交错执行直到“归”到 rootfiber。归阶段结束后会将有 effectTag 的 fiber 节点 挂在到 effectList 的尾部(deletion 除外)。按照上述思路,可以打印出 demo 中各个节点的执行顺序,如图 3.1 所示。图 3.1 render 阶段各节点执行顺序render阶段结束后,产生的effectList如图3.2所示。effectList用来确定哪些dom需要操作,哪些生命周期需要执行。第一个需要执行副作用操作的是span节点,它需要进行更新dom操作,因此在该节点上打上Update更新标签,下一个需要执行副作用操作的是ClickCounter节点,它需要执行componentDidUpdate,因此在该节点上打上Update更新标签。图 3.2 effectList下面我们着重看下,ClickCounter 节点和 span 节点在该阶段的处理,以及 effectList 的生成。由于 ClickCounter 和 span 的工作分别集中在 beginWork 和 completeWork 中,因此,我们主要探讨下它们的工作。3.2 beginWork 处理 ClickCounter 节点核心工作如下:将 effctTag 标记为更新。更新 fiber 节点和类实例的状态。执行 render 方法4.Diff 孩子,更新孩子 fiber。Diff 孩子,更新孩子 fiber。返回 child(下一个 fiber 进行 beginWork)图 3.3 beginWork 处理 ClickCounter 节点核心源码注意:reconcileChildren是对孩子节点(当前demo来说diff的是span和button)进行diff,将新的react elememnt与current fiber进行比较,生成新的孩子fiber节点。详细的diff算法可看这里。我们看下 ClickCounter beginWork 后,ClickCounter fiber 和 span fiber 节点(button 无变化,在此不做对比)的前后对比。ClickCounter fiber 节点对比图 3.4 ClickCounter fiber 节点对比图到这ClickCounter fiber节点的工作已经完成,只需要在completeWork后,将改fiber节点添加到effectList中即可。span fiber 节点对比图 3.5 span fiber 节点对比图经过diff后,由于state.count更新了,使得span react element的props.children变成了1,因此span fiber节点直接复制span react element的属性值到pendingProps.children。这也为span fiber节点后续建立updateQueue打下了基础(对比pendingProps与memoizeProps来判断是否有更新)。3.3 completeWork 处理 span 节点completeWork 对 span fiber 节点的处理分为以下几步:判断 oldProps 是否等于 newProps,如果相等则返回,否则进入步骤 2。注意这里的 oldProps 即 fiber.memoizeProps,newProps 为 fiber.pendingProps。根据fiber.pendingProps生成updateQueue,并添加到span fiber中(commit 阶段,更新span节点属性会用到)。将 effectTag 标记为 Update,标志该节点有副作用,需要进行 dom 更新操作。completeWork 对 span fiber 节点工作的核心源码如下;我们看下经过 completeWork 工作后,span fiber 节点的情况,如图 3.6 所示。图 3.6 completeWork 后 span fiber 节点情况说明3.4 生成 effectListcompleteWork 工作结束会将有副作用标签的 fiber 节点,添加到 effectList 的末尾。首先 span 和 ClickCounter 有副作用标签,其次 span 先 completeWork,ClickCounter 再 completeWork。因此得到 effectList 为:注意,ClickCounter的effectTag值为5,为Update|Placement,表示ClickCounter已经完成了所有的工作,我们记住含有Update即可。生成effectList核心源码如下:注意:render 节点处了生成 effectList 之外,对于 classComponent 来说还会执行一些生命周期函数,例如:constructor、getDerivedStateFromProps、shouldComponentUpdate 和 render。四、commit 阶段4.1 工作总述commit 阶段的主要工作为:调用生命周期函数/hook。执行 dom 操作(主要包括 dom 节点的插入、删除、更新)。该阶段的入口函数为:commitRoot(root),其中输入的 root,是以 FiberRootNode 为根的 fiber 树,如图 4.1 所示。图 4.1 root 树它包含两颗树和一个副作用列表:current 树。finishedwork 树(render 阶段完成的 wip 树)。effectList(以 firstEffect 为起点,通过 nextEffect 连接下一个需要执行操作的 fiber 节点的单向链表)。commitRoot(root)的工作内容主要分为三部分(三次遍历 effectList):1. Beforemutation 阶段(第 1 次遍历 effectList)在标记有 effectTag 为 Snapshot 的节点上调用生命周期方法:getSnapshotBeforeUpdate在标记有 effectTag 为 Passive 的节点上异步调度 hook:useEffect2. Mutation 阶段(第 2 次遍历 effectList)在标记有 effectTag 为 Deletion 的节点上调用生命周期方法:componentWillUnmount /useEffect 的 return 方法执行所有的 DOM 插入、更新和删除3. Layout 阶段(第 3 次遍历 effectList)在标记有 effectTag 为 Placement 的节点上调用生命周期方法:componentDidMount/ componentDidUpdate在标记有 effectTag 为 Update | Callback 在标记调用 hook:useLayoutEffect注意:在 Mutation 阶段后, Layout 阶段前,会将 finishedWork 树设置为 current 树。以上步骤的核心子函数如下:这些子函数中的每一个都通过一个 while 循环,来遍历 effectList,根据 effectTag 和 tag 的值,来做相应的操作,直到 nextEffect==null,终止循环。下面我们分别来看下,这三个阶段的工作。4.2 BeforeMutation 阶段该阶段的主要工作为:对于 classComponent ,在标记有 effectTag 为 Snapshot 的节点上调用生命周期方法:getSnapshotBeforeUpdate;对于 functionComponent,在标记有 effectTag 为 Passive 的节点上异步调度 hook:useEffect其核心源码如下:注意:useEffect 只在这里调度,并不执行其回调函数。由于 commit 阶段同步执行,不会中断,所以 useEffect 的回掉函数的执行在该阶段完成后,浏览器有空余时间时才执行。4.3 Mutation 阶段commitMutationEffects是 React 执行 dom 操作等其他活动的函数。其基本思路:根据不同的 effectTag 执行对应的操作。主要包括:对于 dom 操作,主要有新增 Palcement、更新 Update 和删除 Deletion。其核心源码与如下:1. 新增 dom 操作该操作主要分三步完成:获取当前 fiber 节点的父级 DOM 节点。获取 Fiber 节点的 DOM 兄弟节点根据 DOM 兄弟节点是否存在决定调用 parentNode.insertBefore 或 parentNode.appendChild 执行 DOM 插入操作。即:如果存在兄弟节点,那么执行 insertBefore 否则执行 appendChild。其核心源码与下:2. 更新 dom 操作该操作的内容是:把 Fiber 节点中的 updateQueue 对应的内容渲染在页面上。以 span 为例,其 updateQueue:['children','1'],span 节点执行更新: span.textContent=1。其核心源码如下:3. 删除 dom 操作该操作思路:获取父 dom 节点,进行节点移除。通过 while 循环体查找父节点,直到找到为原生 dom 的父节点为止。通过 parent.removeChild()完成节点删除。核心源码如下:4. Layout 阶段工作内容如下:对于 FunctionComponent,在标记有 effectTag 为 Update | Callback 的 fiber 节点上调用 hook:useLayoutEffect。对于 ClassComponent,在标记有 effectTag 为 Update 的节点上调用生命周期方法:componentDidMount/ componentDidUpdate。如果是首次渲染则执行 componentDidMount,否则执行 componentDidUpdate,是否是首次,通过 current==null 来区分。因此,ClickCounter 在该阶段执行生命周期 componentDidUpdate。其核心源码如下:注意:useEffect 与 useLayoutEffect 调用方式不一样。前者异步调度,后者同步执行。根据场景决定使用哪个:如果不想让副作用操作阻塞浏览器的渲染,可以在 useEffect 中处理。因为它在 commit 阶段异步调度,在页面渲染完成后,浏览器有空闲时间时才会执行。而在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步执行。因此一些对用户可见的 dom 变更,可以在 useLayoutEffect 处理。到此更新渲染流程完成。五、总结经过上述分析,点击 button 后,react 的工作可总结为如下流程:其fiber树的变化可总结如下:六、参考资料[1]react 技术揭秘:https://react.iamkasong.com/preparation/newConstructure.html#react16%E6%9E%B6%E6%9E%84[2]In-depth explanation of state and props update in React:https://indepth.dev/posts/1009/in-depth-explanation-of-state-and-props-update-in-react
预览时标签不可点
大前端69FE33react7大前端 · 目录#大前端上一篇小程序开放平台架构指南(上)下一篇贝壳Flutter组件库 — Bruno正式开源!关闭更多小程序广告搜索「undefined」网络结果
|
|