大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

一、前言

为什么有这篇文章?当时有人问我下面这个点击button,网页应该变成什么样? 注意他们的key是相同的

import React, { useState } from "react";function Demo2() {const [count, setCount] = useState(0);return (<div><button onClick={() => setCount((i) => i + 1)}>点击Count+1</button><h3 key={count}>大{count}</h3><h2 key={count}>舌{count}</h2><h1 key={count}>头{count}</h1></div>);
}export default Demo2;
复制代码

我去看了7km老师的博客[1] 收集到了答案

答案和你想象的一样吗??不一样就继续往下看看呗!!!结尾有答案

二、前置概念

react框架可以用来表示,输入状态 —> 吐出ui。

const ui = fn(state)
复制代码

react架构是什么?

可以分为如下三层:

  1. scheduler(调度器):用来分发优先级更高的任务。

  2. render阶段(协调器):找出哪些节点发生了变化,并且给相应的fiber打上标签。

  3. commit阶段(渲染器):将打好标签的节点渲染到视图上。遍历effectList执行对应的dom操作或部分生命周期

流程图 (36).jpg
  1. 输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次更新需求(目的是要更新DOM节点).

  2. 注册调度任务: react-reconciler收到更新需求之后, 并不会立即构造fiber树, 而是去调度中心scheduler注册一个新任务task, 即把更新需求转换成一个task.

  3. 执行调度任务(输出): 调度中心scheduler通过任务调度循环来执行task

    1. fiber构造循环是task的实现环节之一, 循环完成之后会构造出最新的 fiber 树.

    2. commitRoot是task的实现环节之二, 把最新的 fiber 树最终渲染到页面上, task完成.

主干逻辑就是输入到输出这一条链路, 为了更好的性能(如批量更新, 可中断渲染等功能), react在输入到输出的链路上做了很多优化策略, 任务调度循环和fiber构造循环相互配合就可以实现可中断渲染.

流程图 (39).jpg

ReactElement, Fiber, DOM 三者的关系

上面我们大概提及了一下react的架构和更新的粗略流程,考虑到本文的重点是Render阶段发生了啥,接下来上重量级嘉宾JSX,ReactElement, Fiber, DOM。以下面这个jsx代码为例,讲解三者的关系

function Test() {const [showName, setShowName] = useState(true);return (<div><div>今天肯德基疯狂星期八,和我一起玩彩虹六?</div><ul><li>抱枕一号</li>{showName && <li>抱枕二号</li>}</ul><divonClick={() => {setShowName(false);}}>点击让高启强少一个小弟</div></div>);
}
复制代码

createElement源码

所有采用JSX语法书写的节点, 都会被编译器转换, 最终会以React.createElement(...)的方式, 创建出来一个与之对应的ReactElement对象.

这也是为什么在每个使用JSX的JS文件中,你必须显式的声明 import React from 'react';(17版本后不需要)否则在运行时该模块内就会报未定义变量 React的错误。

ReactElement数据结构和内存结构(结合上面jsx示例代码)

数据结构
export type ReactElement = {// 用于辨别ReactElement对象形式$$typeof: any,// 内部属性type: any, // 表明其种类key: any,ref: any,props: any,// ReactFiber 记录创建本对象的Fiber节点, 还未与Fiber树关联之前, 该属性为null_owner: any,// __DEV__ dev环境下的一些额外信息, 如文件路径, 文件名, 行列信息等_store: {validated: boolean, ...},_self: React$Element<any>,_shadowChildren: any,_source: Source,
};
复制代码
内存结构

流程图 (21).jpg

Fiber 对象数据结构

数据结构
export type Fiber = {|tag: WorkTag,key: null | string, // 和ReactElement组件的 key 一致.elementType: any,//一般来讲和ReactElement组件的 type 一致 比如div ultype: any, // 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新stateNode: any, // 真实DOM是谁return: Fiber | null, //爹是谁child: Fiber | null, //孩子是谁sibling: Fiber | null, //兄弟是谁index: number, ref:| null| (((handle: mixed) => void) & { _stringRef: ?string, ... })| RefObject, //指向在ReactElement组件上设置的 refpendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中.memoizedState: any, // 用于输出的state, 最终渲染所使用的statedependencies: Dependencies | null, // 该fiber节点所依赖的(contexts, events)等mode: TypeOfMode, // 二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点. 与react应用的运行模式有关(有ConcurrentMode, BlockingMode, NoMode等选项).// 优先级相关lanes: Lanes, // 本fiber节点的优先级childLanes: Lanes, // 子节点的优先级alternate: Fiber | null, // 双fiber缓存 指向内存中的另一个fiber, 每个被更新过fiber节点在内存中都是成对出现(current和workInProgress)
|};
复制代码
内存结构

流程图 (22).jpg

ReactElement, Fiber, DOM 三者的关系

流程图 (23).jpg

React的启动过程发生了啥

接下来介绍的都是当前稳定版legacy 模式

ReactDOM.render(<App />, document.getElementById('root'), dom => {});
复制代码

在没有进入render阶段(react-reconciler包)之前,reactElement(<App/>)和 DOM 对象div#root之间没有关联。

流程图 (33).jpg

在react初始化的时候,会创建三个全局对象,在三个对象创建完毕的时候,react初始化完毕。

  1. ReactDOMRoot对象

    1. 属于react-dom包,该对象暴露有render,unmount方法, 通过调用该实例的ReactDOM.render方法, 可以引导 react 应用的启动.

  2. fiberRoot对象

    1. 属于react-reconciler包,在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态,

    2. 其大部分实例变量用来存储fiber构造循环过程的各种状态,react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑。

  3. HostRootFiber对象

    1. 属于react-reconciler包,这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是HostRoot.

这 3 个对象是 react 体系得以运行的基本保障, 除非卸载整个应用,否则不会再销毁

流程图 (34).jpg

此刻内存中各个对象的引用情况表示出来,此时reactElement(<App/>)还是独立在外的, 还没有和目前创建的 3 个全局对象关联起来

流程图 (35).jpg

到此为止, react内部经过一系列运转, 完成了初始化。

三、render阶段发生了啥

以下所有示例按照下面的代码 请注意

class App extends React.Component {
state = {list: ['A', 'B', 'C'],
};
onChange = () => {this.setState({ list: ['C', 'A', 'X'] });
};
componentDidMount() {console.log(`App Mount`);
}
render() {return (<><Header key='d' /><button key='e'>change</button><div className="content" key='f'>{this.state.list.map(item => (<p key={item}>{item}</p>))}</div></>);}
}class Header extends React.PureComponent {
render() {return (<><h1>title</h1><h2>title2</h2></>);}
}
复制代码

双缓冲fiber技术

在上文我们梳理了ReactElement, Fiber, DOM三者的关系, fiber树的构造过程, 就是把ReactElement转换成fiber树的过程. 但是在这个过程中, 内存里会同时存在 2 棵fiber树:

  • 其一: 代表当前界面的fiber树(已经被展示出来, 挂载到fiberRoot.current上). 如果是初次构造(初始化渲染), 页面还没有渲染, 此时界面对应的 fiber 树为空(fiberRoot.current = null).

  • 其二: 正在构造的fiber树(即将展示出来, 挂载到HostRootFiber.alternate上, 正在构造的节点称为workInProgress). 当构造完成之后, 重新渲染页面, 最后切换fiberRoot.current = workInProgress, 使得fiberRoot.current重新指向代表当前界面的fiber树.

React入口初始化内存情况

在进入react-reconciler包之前,也就是还没render时, 内存状态图如下,和上面启动过程的图对应:

流程图 (24).jpg

fiber 树构造方式

  1. 初次创建: 在React应用首次启动时, 界面还没有渲染, 此时并不会进入对比过程, 相当于直接构造一棵全新的树.

  2. 对比更新: React应用启动后, 界面已经渲染. 如果再次发生更新, 创建新fiber之前需要和旧fiber进行对比. 最后构造的 fiber 树有可能是全新的, 也可能是部分更新的.

在深度优先遍历中, 每个fiber节点都会经历 2 个阶段:

  1. 探寻阶段 beginWork

  2. 回溯阶段 completeWork

beginWork探寻阶段发生了什么源码地址[2]

  1. 创建节点:根据 ReactElement对象创建所有的fiber节点, 最终构造出fiber树形结构(设置returnsibling指针)

  2. 给节点打标签:设置fiber.flags(二进制形式变量, 用来标记 fiber节点 的增,删,改状态, 等待completeWork阶段处理)

  3. 设置真实DOM的局部状态:设置fiber.stateNode局部状态(如Class类型节点: fiber.stateNode=new Class())

completeWork回溯阶段发生了什么源码地址[3]

  1. 调用completeWork

    1. fiber节点(tag=HostComponent, HostText)创建 DOM 实例, 设置fiber.stateNode局部状态(如tag=HostComponent, HostText节点: fiber.stateNode 指向这个 DOM 实例).

    2. 为 DOM 节点设置属性, 绑定事件(合成事件原理).

    3. 设置fiber.flags标记

  2. 把当前 fiber 对象的副作用队列(firstEffectlastEffect)添加到父节点的副作用队列之后, 更新父节点的firstEffectlastEffect指针.

  3. 识别beginWork阶段设置的fiber.flags, 判断当前 fiber 是否有副作用(增,删,改), 如果有, 需要将当前 fiber 加入到父节点的effects队列, 等待commit阶段处理.

初次创建

这有一个动画 具体如果想看流程图可以点击[4]

初始化fiber.gif

下面标注了生成时期的 beginWorkcompleteWork 执行过程

// 将最新的fiber树挂载到root.finishedWork节点上 下面绿色粗线表示指针const finishedWork: Fiber = (root.current.alternate: any);root.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入commit阶段commitRoot(root);
复制代码

动画演示了初次创建fiber树的全部过程, 跟踪了创建过程中内存引用的变化情况. fiber树构造循环负责构造新的fiber树, 构造过程中同时标记fiber.flags, 最终把所有被标记的fiber节点收集到一个副作用队列中, 这个副作用队列被挂载到根节点上(HostRootFiber.alternate.firstEffect). 此时的fiber树和与之对应的DOM节点都还在内存当中, 等待commitRoot阶段进行渲染

流程图 (32).jpg

对比更新的时候发生了什么

1.优化原则
  1. 只对同级节点进行对比,如果DOM节点跨层级移动,则react不会复用

  • 我们可以从同级的节点数量将Diff分为两类:

    - 当newChild类型为JSX对象、number、string,代表同级只有一个节点
    - 当newChild类型为Array,同级有多个节点

不同类型的元素会产出不同的结构,会销毁老的结构,创建新的结构

可以通过key标示移动的元素

类型一致的节点才有继续diff的必要性

  • 单节点对应演示,可以去浏览器的Elements->Properties查看

单节点.jpg
  • 多节点对应演示

image.png
diff算法介绍

1.单节点

  1. 如果是新增节点, 直接新建 fiber, 没有多余的逻辑

  2. 如果是对比更新

  • 如果keytype都相同,则复用

  • 否则新建

单节点的逻辑比较简明, 源码[5]

2.多节点

  1. 多节点一般会存在两轮遍历,第一轮寻找公共序列,第二轮遍历剩余非公共序列

  2. 第一次循环 源码[6]

  • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。

  • key相同type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历

  1. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。

  2. let i = 0,遍历newChildren,将newChildren[i]oldFiber比较,判断DOM节点是否可复用。

  3. 如果可复用,i++,继续比较newChildren[i]oldFiber.sibling,可以复用则继续遍历。

  4. 如果不可复用,分两种情况:

image.png

image.png
  1. 第二次循环: 遍历剩余非公共序列, 优先复用 oldFiber 序列中的节点。

  • 如果newChildrenoldFiber同时遍历完,diff结束

  • 如果 newChildren没遍历完,oldFiber遍历完,意味着没有可以复用的节点了,遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement

  • 如果newChildren遍历完,oldFiber没遍历完,意味着有节点被删除了,需要遍历剩下的oldFiber,依次标记Deletion

  • 如果newChildrenoldFiber都没遍历完 (重点)源码[7]

    - 先去`声明map数据结构`,遍历一遍老节点,把老fiber的key做映射 \{元素的key:老的fiber节点\},
    - 继续遍历新`jsx`,如果`map`有`key`,会把`key`从`map`中删除,说明可以复用,把当前节点标记为`更新`。新地位高的不动,新地位低的动(中间插入链表比链表屁股插入费劲)所以地位低的动动。
    - `lastPlaceIndex`指针,指向最后一个不需要动的老节点的`key`。每次新jsx复用到节点,`lastPlaceIndex`会指向老节点的最后一个成功复用的老`fiber`节点。如果新复用的节点key小于`lastPlaceIndex`,说明老`fiber`节点的顺序在新`jsx`之前,需要挪动位置接到新`jsx`节点后面。
    - 如果`jsx`没有复用的老`fiber`,直接插入新的
    - `map`中只剩还没被复用的节点,等着新的`jsx`数组遍历完,`map`里面的`fiber`节点全部设置为删除

image.png

image.png

下面动画展示了fiber的对比更新过程 每一张流程图链接[8]

fiber对比更新.gif

流程图 (28).jpg

四、检验学习成果

为什么网页会变成那个样子?

import React, { useState } from "react";function Demo2() {const [count, setCount] = useState(0);return (<div><button onClick={() => setCount((i) => i + 1)}>点击Count+1</button><h3 key={count}>大{count}</h3><h2 key={count}>舌{count}</h2><h1 key={count}>头{count}</h1></div>);
}export default Demo2;
复制代码

流程图 (29).jpg

流程图 (30).jpg

流程图 (38).jpg

image.png

五、参考

7km:7kms.github.io/react-illus…[9]

冴羽:juejin.cn/post/716098…[10]

卡颂:react.iamkasong.com/preparation…[11]

xiaochen1024.com/article\_ite…[12]

如果有错误的话欢迎大家帮忙指正嗷!!!强烈推荐7km的图解react! 谢谢大家~~~~

关于本文

作者:抱枕同学

https://juejin.cn/post/7202085514400038969

Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。“分享、点赞、在看” 支持一波												

深入理解Render阶段Fiber树的初始化与更新相关推荐

  1. 刻意练习-理解哈夫曼树构建哈夫曼表C语言

    理解哈夫曼树构建哈夫曼表 一.哈夫曼树的作用 哈夫曼树是一个二叉树,是可以将一些字节重新编码 ,而且能够使用最少的空间.所以也叫最优二叉树. 比如这段字符串 damainnnnnnnnnnnnnnnn ...

  2. React16版本中render阶段放弃了使用递归

    React16版本中render阶段放弃了使用递归 在16之前的版本中采用递归执行.递归耗内存,它使用 JavaScript 自身的执行栈,更新一旦开始,中途就无法中断.程序就会一直占用线程,又因为j ...

  3. ie 不执行回调函时_javascript引擎执行的过程的理解--执行阶段

    一.概述 js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章我们介绍了语法分析和预编译阶段,那么我们先做个简单概括,如下: 1.语法分析: 分别对加载完成的代码块进行语法检验 ...

  4. 【数据竞赛】Kaggle GM秘技:树模型初始化技巧

    作者:  尘沙樱落 树模型初始化技巧 大家都知道神经网络训练的提升分数的技巧之一就是: 依据验证集合的效果,来调整learning rate的大小,从而获得更好的效果; 但我们在训练树模型的时候却往往 ...

  5. javascript引擎执行的过程的理解--执行阶段

    一.概述 js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章我们介绍了语法分析和预编译阶段,那么我们先做个简单概括,如下: 1.语法分析: 分别对加载完成的代码块进行语法检验 ...

  6. Linux内核深入理解系统调用(1):初始化-入口-处理-退出

    Linux内核深入理解系统调用(1):初始化-入口-处理-退出 rtoax 2021年3月 1. Linux 内核系统调用简介 这次提交为 linux内核解密 添加一个新的章节,从标题就可以知道, 这 ...

  7. b+树的增删改查_考研计算机 | 如何理解m阶B树?

    2021计算机考研:如何理解m阶B树?对m阶B树定义的理解一棵m阶的B树满足下列条件:1.每个结点至多有m棵子树.2.除根结点外,其它每个分支至少有m/2棵子树.3.根结点至少有两棵子树(除非B树只有 ...

  8. kafka 两段式提交_如何理解两阶段提交?

    在分布式系统中,为了让每个节点都能够感知到其他节点的事务执行状况,需要引入一个中心节点来统一处理所有节点的执行逻辑,这个中心节点叫做协调者(coordinator),被中心节点调度的其他业务节点叫做参 ...

  9. 数据结构之线段树入门(单点更新区间查询)

    线段树是学习数据结构必须学习的一种数据结构,在ACM,蓝桥等比赛中是经常出现的.利用线段树解题,会使得题目简单易理解.而且线段树是数据结构中比较基础而且用的很多的一种. 线段树定义 线段树是一种二叉搜 ...

最新文章

  1. vim进阶之202007命令记录
  2. iOS开发--线程通信
  3. 通过 GitExtensions 来使用 Git 子模块功能
  4. cvs有机添加剂检测_美国ECI CVS 电镀添加剂分析仪QL-10EX
  5. ubuntu16.04源码安装opencv3.4.0
  6. 【黄金分割点】与【斐波那契数列】
  7. extract进程 oracle,OracleGoldenGate系列:Extract进程的恢复原理
  8. DML、DDL的概念与区别
  9. 用Node.JS分析steam所有的游戏!
  10. 软件安全测试方案设计模板-homin
  11. 【已解决】Instances currently registered with Eureka中Application为UNKNOWN
  12. 洛谷(Python) P3717 [AHOI2017初中组]cover
  13. java异常以及处理
  14. 管理部门使用计算机属于固定资产核算吗,固定资产核算管理内容
  15. 天载配资关注这个转折点机会
  16. 踏上Oracle ebs的道路
  17. 帮老婆系列-关于计算Excel表去除指定时间段后的时间差
  18. 程序员多数性功能不行_不会盲打的程序员和不懂技术的 CTO
  19. mplayer - Linux下的电影播放器
  20. 领导层必看:要想公司管理好,办公软件少不了

热门文章

  1. mybatis流式查询
  2. 2023最新SSM计算机毕业设计选题大全(附源码+LW)之java装修服务分析系统03563
  3. Java多线程 解决private对象逸出--返回副本
  4. 第二章.线性回归以及非线性回归—LASSO算法
  5. 用Visio画时序分析波形图
  6. 【免费】中国省级行政单位ISO 3166-2对照表
  7. 用wireshark抓包分析TLS协议
  8. 计算机测控技术论文,测控技术与仪器论文.doc
  9. 惠普打印机墨盒更换教程_惠普彩色打印机如何换墨盒 惠普打印机墨盒更换方法【介绍】...
  10. Linux学习笔记8—进程间通信