本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
大厂技术 高级前端 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 架构是什么?
可以分为如下三层:
scheduler(调度器):用来分发优先级更高的任务。
render 阶段(协调器):找出哪些节点发生了变化,并且给相应的 fiber 打上标签。
commit 阶段(渲染器):将打好标签的节点渲染到视图上。遍历 effectList 执行对应的 dom 操作或部分生命周期
流程图 (36).jpg
输入: 将每一次更新 (如: 新增, 删除, 修改节点之后) 视为一次更新需求(目的是要更新 DOM 节点).
注册调度任务: react-reconciler 收到更新需求之后, 并不会立即构造 fiber 树, 而是去调度中心 scheduler 注册一个新任务 task, 即把更新需求转换成一个 task.
执行调度任务 (输出): 调度中心 scheduler 通过任务调度循环来执行 task
fiber 构造循环是 task 的实现环节之一, 循环完成之后会构造出最新的 fiber 树.
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>
<div
onClick={() => {
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 ul
type: 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组件上设置的 ref
pendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动
memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中
updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中.
memoizedState: any, // 用于输出的state, 最终渲染所使用的state
dependencies: 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 初始化完毕。
ReactDOMRoot对象属于
react-dom包,该对象暴露有 render,unmount 方法, 通过调用该实例的ReactDOM.render方法, 可以引导 react 应用的启动.fiberRoot对象属于
react-reconciler包, 在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态,其大部分实例变量用来存储
fiber构造循环过程的各种状态,react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑。HostRootFiber对象属于
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 class 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 树构造方式
初次创建: 在
React应用首次启动时, 界面还没有渲染, 此时并不会进入对比过程, 相当于直接构造一棵全新的树.对比更新:
React应用启动后, 界面已经渲染. 如果再次发生更新, 创建新fiber之前需要和旧fiber进行对比. 最后构造的 fiber 树有可能是全新的, 也可能是部分更新的.
在深度优先遍历中, 每个fiber节点都会经历 2 个阶段:
探寻阶段
beginWork回溯阶段
completeWork
beginWork探寻阶段发生了什么源码地址 [2]
创建节点:根据
ReactElement对象创建所有的fiber节点, 最终构造出 fiber 树形结构 (设置return和sibling指针)给节点打标签:设置
fiber.flags(二进制形式变量, 用来标记 fiber 节点 的增, 删, 改状态, 等待completeWork阶段处理)设置真实 DOM 的局部状态:设置
fiber.stateNode局部状态 (如 Class 类型节点:fiber.stateNode=new Class())
completeWork回溯阶段发生了什么源码地址 [3]
调用
completeWork给
fiber节点 (tag=HostComponent, HostText) 创建 DOM 实例, 设置fiber.stateNode局部状态 (如tag=HostComponent, HostText节点: fiber.stateNode 指向这个 DOM 实例).为 DOM 节点设置属性, 绑定事件 (
合成事件原理).设置
fiber.flags标记把当前
fiber对象的副作用队列 (firstEffect和lastEffect) 添加到父节点的副作用队列之后, 更新父节点的firstEffect和lastEffect指针.识别
beginWork阶段设置的fiber.flags, 判断当前fiber是否有副作用 (增, 删, 改), 如果有, 需要将当前fiber加入到父节点的effects队列, 等待commit阶段处理.
初次创建
这有一个动画 具体如果想看流程图可以点击 [4]
初始化 fiber.gif
下面标注了生成时期的 beginWork 和 completeWork 执行过程
// 将最新的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. 优化原则
- 只对同级节点进行对比,如果 DOM 节点跨层级移动,则 react 不会复用
我们可以从同级的节点数量将 Diff 分为两类:
- 当newChild类型为JSX对象、number、string,代表同级只有一个节点 - 当newChild类型为Array,同级有多个节点
不同类型的元素会产出不同的结构,会销毁老的结构,创建新的结构
可以通过 key 标示移动的元素
类型一致的节点才有继续 diff 的必要性
单节点对应演示, 可以去浏览器的Elements->Properties查看
单节点. jpg
多节点对应演示
image.png
diff 算法介绍
单节点
如果是新增节点, 直接新建 fiber, 没有多余的逻辑
如果是对比更新
如果
key和type都相同,则复用否则新建
单节点的逻辑比较简明, 源码 [5]
多节点
多节点一般会存在两轮遍历,第一轮寻找公共序列,第二轮遍历剩余非公共序列
第一次循环 源码 [6]
key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。key相同type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历
如果
newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用。如果可复用,
i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历。如果不可复用,分两种情况:
image.png
image.png
- 第二次循环: 遍历剩余
非公共序列, 优先复用 oldFiber 序列中的节点。
如果
newChildren与oldFiber同时遍历完,diff 结束如果
newChildren没遍历完,oldFiber遍历完,意味着没有可以复用的节点了,遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。如果
newChildren遍历完,oldFiber没遍历完,意味着有节点被删除了,需要遍历剩下的oldFiber,依次标记Deletion。如果
newChildren与oldFiber都没遍历完(重点)源码 [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」即可。
“分享、点赞、在看” 支持一波