前面的文章分析了 Concurrent 模式下异步更新的逻辑,以及 Fiber 架构是如何进行时间分片的,更新过程中的很多内容都省略了,评论区也收到了一些同学对更新过程的疑惑,今天的文章就来讲解下 React Fiber 架构的更新机制。

Fiber 数据结构

我们先回顾一下 Fiber 节点的数据结构(之前文章省略了一部分属性,所以和之前文章略有不同):

function FiberNode (tag, key) {

缓存机制

可以注意到 Fiber 节点有个 alternate 属性,该属性在节点初始化的时候默认为空(this.alternate = null)。这个节点的作用就是用来缓存之前的 Fiber 节点,更新的时候会判断 fiber.alternate 是否为空来确定当前是首次渲染还是更新。下面我们上代码:

import React 

在调用 createRoot 的时候,会先生成一个FiberRootNode,在 FiberRootNode 下会有个 current 属性,current 指向 RootFiber 可以理解为一个空 Fiber。后续调用的 render 方法,就是将传入的组件挂载到 FiberRootNode.current(即 RootFiber) 的空 Fiber 节点上。

// 实验版本对外暴露的 createRoot 需要加上 `unstable_` 前缀

render 最后调用 scheduleUpdateOnFiber 进入更新任务,该方法之前有说明,最后会通过 scheduleCallback 走 MessageChannel 消息进入下个任务队列,最后调用 performConcurrentWorkOnRoot 方法。

// scheduleUpdateOnFiber

开始更新时,如果 workInProgress 为空会指向一个新的空 Fiber 节点,表示正在进行工作的 Fiber 节点。

workInProgress.alternate = currentcurrent.alternate = workInProgress

fiber tree

构造好 workInProgress 之后,就会开始在新的 RootFiber 下生成新的子 Fiber 节点了。

function renderRootConcurrent(root) {

按照我们前面的案例, workLoopConcurrent 调用完成后,最后得到的 fiber 树如下:

class App extends React.Component {

fiber tree

最后进入 Commit 阶段的时候,会切换 FiberRootNode 的 current 属性:

function performConcurrentWorkOnRoot() {

fiber tree

上面的流程为第一次渲染,通过 setState({ val: 1 }) 更新时,workInProgress 会切换到 root.current.alternate

function createWorkInProgress() {

fiber tree

在后续的遍历过程中(workLoopConcurrent()),会在旧的 RootFiber 下构建一个新的 fiber tree,并且每个 fiber 节点的 alternate 都会指向 current fiber tree 下的节点。

fiber tree

这样 FiberRootNode 的 current 属性就会轮流在两棵 fiber tree 不停的切换,即达到了缓存的目的,也不会过分的占用内存。

更新队列

在 React 15 里,多次 setState 会被放到一个队列中,等待一次更新。

// setState 方法挂载到原型链上

同样在 Fiber 架构中,也会有一个队列用来存放 setState 的值。每个 Fiber 节点都有一个 updateQueue 属性,这个属性就是用来缓存 setState 值的,只是结构从 React 15 的数组变成了链表结构。

无论是首次 Render 的 Mount 阶段,还是 setState 的 Update 阶段,内部都会调用 enqueueUpdate 方法。

// --- Render 阶段 ---

enqueueUpdate 方法的主要作用就是将 setState 的值挂载到 Fiber 节点上。

function enqueueUpdate(fiber, update) {

多次 setState 会在 sharedQueue.pending 上形成一个单向循环链表,具体例子更形象的展示下这个链表结构。

class App extends React.Component {

点击 div 之后,会连续进行三次 setState,每次 setState 都会更新 updateQueue。

第一次 setState

第二次 setState

第三次 setState

更新过程中,我们遍历下 updateQueue 链表,可以看到结果与预期的一致。

let $pending = sharedQueue.pending

链表数据

递归 Fiber 节点

Fiber 架构下每个节点都会经历递(beginWork)归(completeWork)两个过程:

  • beginWork:生成新的 state,调用 render 创建子节点,连接当前节点与子节点;
  • completeWork:依据 EffectTag 收集 Effect,构造 Effect List;

先回顾下这个流程:

function workLoopConcurrent() {

递(beginWork)

先看看 beginWork 进行了哪些操作:

function beginWork(current, workInProgress) {

首先判断 current(即:workInProgress.alternate) 是否存在,如果存在表示需要更新,不存在就是首次加载,didReceiveUpdate 变量设置为 false,didReceiveUpdate 变量用于标记是否需要调用 render 新建 fiber.child,如果为 false 就会重新构建fiber.child,否则复用之前的 fiber.child

然后会依据 workInProgress.tag 调用不同的方法构建  fiber.child。关于 workInProgress.tag 的含义可以参考 react/packages/shared/ReactWorkTags.js,主要是用来区分每个节点各自的类型,下面是常用的几个:

var FunctionComponent = 

调用的方法不一一展开讲解,我们只看看 updateClassComponent

// 更新 class 组件

首先遍历了之前提到的 updateQueue 更新 state,然后就是判断 state 是否更新,以此来推到组件是否需要更新(这部分代码省略了),最后调用的组件 render 方法生成子组件的虚拟 DOM。最后的 reconcileChildren 就是依据 render 的返回值来生成 fiber 节点并挂载到 workInProgress.child 上。

// 构造子节点

篇幅有限,看看 render 返回值为对象的情况(通常情况下,render 方法 return 的如果是 jsx 都会被转化为虚拟 DOM,而虚拟 DOM 必定是对象或数组):

if (

归(completeWork)

fiber.child 为空时,就会进入 completeWork 流程。而 completeWork 主要就是收集 beginWork 阶段设置的 effectTag,如果有设置 effectTag 就表明该节点发生了变更, effectTag  的主要类型如下(默认为 NoEffect ,表示节点无需进行操作,完整的定义可以参考 react/packages/shared/ReactSideEffectTags.js):

export 

我们看看 completeWork 过程中,具体进行了哪些操作:

function completeWork(current, workInProgress) {

beginWork 一样,completeWork 过程中也会依据 workInProgress.tag 来进行不同的处理,其他类型的组件基本可以略过,只用关注下 HostComponentHostText,这两种类型的节点会反应到真实 DOM 中,所以会有所处理。

function ( current, workInProgress, type, newProps) {

updateHostComponent 方法最后会通过 diffProperties 方法获取一个更新队列,挂载到 fiber.updateQueue 上,这里的 updateQueue 不同于 Class 组件对应的 fiber.updateQueue,不是一个链表结构,而是一个数组结构,用于更新真实 DOM。

下面举一个例子,修改 App 组件的 state 后,下面的 span 标签对应的 data-valstylechildren 都会相应的发生修改,同时,在控制台打印出 updatePayload 的结果。

import React 

console

副作用链表

在最后的更新阶段,为了不用遍历所有的节点,在 completeWork 过程结束后,会构造一个 effectList 连接所有 effectTag 不为 NoEffect 的节点,在 commit 阶段能够更高效的遍历节点。

function completeUnitOfWork() {

上面的代码就是构造 effectList 的过程,光看代码还是比较难理解的,我们还是通过实际的代码来解释一下。

import React 

App

我们构造一个 2 * 2 的 Table,每次点击组件,td 的 children 都会发生修改,下面看看这个过程中的 effectList 是如何变化的。

第一个 td 完成 completeWork 后,EffectList 结果如下:

1

第二个 td 完成 completeWork 后,EffectList 结果如下:

2

两个 td 结束了 completeWork 流程,会回溯到 tr 进行 completeWork ,tr 结束流程后 ,table 会直接复用 tr 的 firstEffect 和 lastEffect,EffectList 结果如下:

3

后面两个 td 结束 completeWork 流程后,EffectList 结果如下:

4

回溯到第二个 tr 进行 completeWork ,由于 table 已经存在 firstEffect 和 lastEffect,这里会直接修改 table 的 firstEffect 的 nextEffect,以及重新指定 lastEffect,EffectList 结果如下:

5

最后回溯到 App 组件时,就会直接复用 table 的 firstEffect 和 lastEffect,最后 的EffectList 结果如下:

6

提交更新

这一阶段的主要作用就是遍历 effectList 里面的节点,将更新反应到真实 DOM 中,当然还涉及一些生命周期钩子的调用,我们这里只展示最简单的逻辑。

function commitRoot(root) {

这里不再展开讲解每个 effect 下具体的操作,在遍历完 effectList 之后,就是将当前的 fiber 树进行切换。

function commitRoot() {

总结

到这里整个更新流程就结束了,可以看到 Fiber 架构下,所有数据结构都是链表形式,链表的遍历都是通过循环的方式来实现的,看代码的过程中经常会被突然出现的 return、break 扰乱思路,所以要完全理解这个流程还是很不容易的。

最后,希望大家在阅读文章的过程中能有收获,下一篇文章会开始写 Hooks 相关的内容。

react div 事件优先级_React 架构的演变 更新机制相关推荐

  1. react 16 对外暴露function_【第 25 期】React 架构的演变 从同步到异步(一)

    正文开始~ 写这篇文章的目的,主要是想弄懂 React 最新的 fiber 架构到底是什么东西,但是看了网上的很多文章,要不模棱两可,要不就是一顿复制粘贴,根本看不懂,于是开始认真钻研源码.钻研过程中 ...

  2. react div onclick叠加_深入学习 React 合成事件

    翁斌斌,微医云前端工程师,在程序员的修炼道路上永不止步. 以下分析基于React, ReactDOM 16.13.1版本 提出问题 我们借鉴一个比较典型的案例开始来分析React事件 export 从 ...

  3. React 之 简易实现 Fiber架构

    文章目录 fiber架构是什么?它解决了什么问题? fiber 的核心思想,实现 fiber 我们需要做到什么?如何做? Fiber reconcile Commit 阶段 实现useState管理状 ...

  4. 【React源码】(十六)React 合成事件

    React 合成事件 概览 从v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中. 引入官方提供的图片: 图 ...

  5. 架构师之路---架构的演变详解

    前言:架构的演变流程 单体架构 ==> 垂直架构 ==> 前后端分离 ==> EAI架构  ==> SOA架构 ==> 微服务 ==> 微服务2.0 1.单体架构: ...

  6. Hybris平台Web架构模式演变:前后端分离

    "前后端分离"显然已不是什么新鲜的话题,表面上看是一场架构模式的变革,但实质上是为了解决以往传统的服务端MVC设计模式的一些诟病和痛点.前后端分离带来的全新的前后端协作方式能够让专 ...

  7. react 八千字长文深入了解react合成事件底层原理,原生事件中阻止冒泡是否会阻塞合成事件?

    壹 ❀ 引 在前面两篇文章中,我们花了较大的篇幅介绍react的setState方法,在介绍setState同步异步时提到,在react合成事件中react对于this.state更新都是异步,但在原 ...

  8. react循环key值_React源码揭秘(三):Diff算法详解

    编者按:本文作者奇舞团前端开发工程师苏畅. 代码参照React 16.13.1 什么是Diff 在前两篇文章中我们分别介绍了 React 的首屏渲染流程1和组件更新流程2,其中 首屏渲染会渲染一整棵 ...

  9. React学习:事件绑定、组件定义、for、map循环-学习笔记

    文章目录 React学习:事件绑定.组件定义.for.map循环-学习笔记 事件绑定 组件定义 (参数传递) for.map循环 React学习:事件绑定.组件定义.for.map循环-学习笔记 事件 ...

最新文章

  1. C语言char*s 4,求讲解几道C语言的题 52 声明语句为“char s[4][15],*p1,**p2;int x,*y;”,下列语句中正...
  2. 使用Log4j进行日志操作(牛小浩)不错的
  3. android:ellipsize实现跑马灯效果总结
  4. 的级联选择_级联接收机的计算及Y因子噪声因子测量法
  5. 软件包管理 之 fedora-rpmdevtools 工具介绍
  6. C/C++vsnprintf用法(要配合va_list使用)
  7. Jetson tk1 刷机教程
  8. windows和linux 下将tomcat注册为服务
  9. 解决新版chrome跨域问题:cookie丢失以及samesite属性问题
  10. C++中的向量vector
  11. 【考研】数据库知识点总结
  12. 【第二届PHP全球开发者大会】惠新宸(鸟哥):PHP7性能之源
  13. java的弱引用_理解Java中的弱引用(Weak Reference)
  14. 快递e栈控制台版实现心得
  15. 计算机制作节日贺卡教案,幼儿园节日教案:做贺卡
  16. 【valist】c语言可变参数宏
  17. sql,python
  18. 128. 损益类科目能说出几个?
  19. EV: 汽车驾驶技术与技巧
  20. vue.js+node.js+mysql在线聊天室源码

热门文章

  1. 细说 ASP.NET Cache 及其高级用法
  2. Hashtable和HashMap类的区别
  3. FireBug调试工具笔记
  4. 对.NET的GC(垃圾回收)的理解都存在错误认识
  5. 拼多多算法笔试2020
  6. html js 回调函数,js中回调函数的学习笔记
  7. 直播丨Oracle 12cR2 ADG LGWR Library Cache案例分享
  8. 直播丨分布式数据库:从PG-XL到TBASE
  9. 几万年前,有一只猴子大闹地府后删库跑路...
  10. 20万DBA在关注的11个问题