React 或者说框架的意义是为了提高代码的可维护性,而不是为了提高性能的,现在所做的提升性能的操作,只是在可维护性的基础上对性能的优化
react中的vdom实际上就是通过 property来进行一个attributes的一个判断,来看它是否有变化
一、导学
react非常纯粹,核心 api 就是 setState,其它所有的内容都围绕组件化来设计,一切基于组件,没有directive 双向绑定和其它API,如果要改变一个ui,就只能通过 setState来进行修改
思想超前:react在16版本引入了 Fiber 这个概念,从根本上解决了 js单线程运行如果计算量太大的话,导致动画卡帧和交互卡顿的问题。react使用 Fiber 重构,前后花了三年
核心为:Fiber、Update、Schedule 这几个概念
react是一个非常纯粹的框架,它所有的核心都是服务于应用的整体更新的,只有从更新到结束,这一个流程。
在react中创建和更新的过程实际上都是一样的,即在创建完更新之后,进入队列中,然后就会进行整个应用的更新
二、基础 API
React主要API介绍,在这里你能了解它的用法,为下一章源码分析打基础。
1、准备工作
- 源码地址以及目录结构
- 源码地址:https://github.com/facebook/react 一般是 github 的源码地址。然后 下载对应版本的源码,就可以来进行查看了
- 主要需要查看的就是 packages 这个文件夹,文件夹中的主要包如下:
- events:这个文件夹 就是 react 中的事件系统
- react: 这个就是 react包 的核心代码。主要是用来 定义节点和表现行为的包
- react-dom: 这个就是 reactDom包 的核心代码。主要是 定义在dom上面如何进行渲染、更新操作
- react-reconciler: 这个包是 reactDom 重度依赖的一个包。用来搞什么调度之类的 一个包
- scheduler: 这个也是 react核心的一个包,实现了 调度 即 异步渲染
Flow Type:这东西和TypeScript很像,只不过强制性没有TypeScript高,也是搞类型的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n5HWYuti-1644729961876)(9089AE138BED4E6187DF7BEFFBF94C0F)]
2、JSX到JavaScript的转换
https://babeljs.io/repl/ 这个网址,可以让我们直接写入 jsx,然后会用babel直接解析出 jsx
JSX在转换的时候,如果标签名为大写,则会当作组件(即直接作为变量传入进去); 如果标签名为小写,会被当作一个原生的DOM节点(即直接作为字符串传入进去),如下
// 下面为 jsx
function Comp() {return <a>1</a>}
<Comp id="div" key="key">
<span>1</span>
<span>1</span>
</Comp>
// 下面为用babel把jsx解析之后,生成的js
function Comp() {return React.createElement("a", null, "1");}
React.createElement(Comp, { // 这里大写的标签名; 是直接作为一个变量传入进去,代表一个组件
id: "div",
key: "key"
},
React.createElement("span", null, "1"),
React.createElement("span", null, "1")
);
// 下面为jsx
function comp() {return <a>1</a>}
<comp id="div" key="key">
<span>1</span>
<span>1</span>
</comp>
// 下面为用babel把jsx解析之后,生成的js
function comp() {return React.createElement("a", null, "1");}
React.createElement("comp", { // 这里小写的标签名; 是直接作为一个字符串传入进去,代表DOM节点
id: "div",
key: "key"
},
React.createElement("span", null, "1"),
React.createElement("span", null, "1")
);
3、react-element
react 这个包的入口文件是 react.js
ReactElement.js 这个文件是用来写 上面的createElement这些方法的
import {
createElement,
createFactory,
cloneElement,
isValidElement,
} from './ReactElement';
4、react-component
这个就是 react中的组件,就是使用class组件的时候,需要 extends 的那个 Component
ReactElement.js 这个文件是用来写 上面的createElement这些方法的
5、react-ref
这个就是 react中的组件,就是使用class组件的时候,需要 extends 的那个 Component
三、React中的更新
主要讲解React创建更新中的主要两种方式ReactDOM.render和setState,他们具体做了什么。
这一章主要讲 创建调度,并且进行到更新阶段的过程
1、react(dom-render)
源码在 react-dom 包里面的 src文件夹里面的 client 里面的 ReactDOM.js
- react中创建节点和更新节点的API
- ReactDOM.render 创建节点
- setState 更新节点
- forceUpdate 更新节点
- ReactDOM.render 需要做的事情
- 创建ReactRoot:这个是包含整个react 最顶点的一个对象
- 在创建ReactRoot的过程中,也要 创建FiberRoot; FiberRoot创建过程中 也会去初始化一个 Fiber 对象; 和RootFiber
- 创建更新:创建更新之后,就可以进入更新调度的阶段了,更新调度的阶段就下一章再讲了
2、react(fiber-root)
- FiberRoot: 是整个应用的起点。
- 包含应用挂载的目标节点。即 从 ReactRoot 传进来的节点
- 记录整个应用更新过程的各种信息
3、react(fiber)
- Fiber: 是react 16之后的核心对象。每一个ReactElement都会对应一个Fiber对象
- 记录节点的各种状态:比如当前节点什么属性什么的,都是存在 Fiber 对象里面;简单说就是存放在Fiber的UpdateQueue属性中。然后在Fiber更新之后,才会更新到class组件的state或者props上面去
- 串联整个应用形成树结构:在ReactElement中是通过props.children来串联每个节点; 而 在Fiber中 通过 return、child、sibling 这三个属性来串联所有节点,Fiber构成的为一个单链表树结构
return:Fiber
: 指向当前Fiber对象的父节点,最顶层的FiberRoot对象的return指向null,用来在处理完这个节点之后向上返回child:Fiber
: 指向当前Fiber对象的第一个子节点,单链表树结构;sibling:Fiber
: 指向当前Fiber对象的下一个兄弟节点,兄弟节点的return指向同一个父节点- 可以说 Fiber树和之前的虚拟dom vnode的结构是不一样的,虚拟dom vnode是通过props.children来构建出节点树;而Fiber是通过Fiber单链表来构建出节点树
- 遍历方法:实际上是通过 先序遍历来做的(根左右)。先找到一个根节点,然后通过根节点child找到这个根节点的左叶子节点,找到左叶子节点之后,再通过左叶子的sibing找到这个左叶子的右边一个兄弟节点,最后通过右叶子的return从这个最右边节点返回之前的那个根节点,接着通过根节点的sibing继续找到根节点的兄弟节点
如下图,为一个Fiber Tree的结构; div节点为一个Fiber,这个div Fiber有一个child属性指向input Fiber节点; 然后input Fiber节点有个sibling属性指向List Fiber节点; input Fiber节点和List Fiber节点都有一个return属性指向div Fiber节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BIeLgpqV-1644729961878)(CC4F59FC314E453E907CF0CC1A0CD6A9)]
4、react(update-and-updateQueue)
Update: 用于记录组件状态的改变,存放于UpdateQueue中
UpdateQueue: 这个是Fiber对象的一个属性,即当前Fiber对象的更新都存放在这里面;UpdateQueue是一个单向链表结构,一次更新的过程中,一个UpdateQueue可能会存在多个Update,最后根据UpdateQueue中所有的Update来计算出最后的结果
5、different-expirtation-time
- expiration time: 这个过期时间在 同步任务和 异步任务还有指定context的任务中,都是可以存在的,并且代表的含义都不同,如下
- sync(同步任务中): 即同步更新的时候,是直接执行的。同步的情况下,就是 有外部强制的情况下,或者有任务正在更新的时候,任务就会被同步执行
- async(异步任务中): 即异步更新的时候,异步任务中的过期时间,即如果一个异步任务超出了这个时间,但是还没被执行的话,这个异步任务就会被强制执行。
- 简单说就是,在react中异步任务的优先级是比较低的,所以这个异步任务是可以被打断的,
- 但是为了防止这个异步任务一直被打断,所以有了这个expirationTime,表示在这个时间之前这个异步任务都可以被打断,但是如果过了这个expirationTime了,这个任务还没被执行,它就会被强制执行
6、react(setState-forceUpdate)
- setState和forceUpdate: 这两个加上 上面的那个ReactDom.reader,就是react中更新节点的方法
- 它们都是给节点的Fiber创建更新的;
- 对于ReactDom.reader来说,它是一个整体的初始化渲染
- 对于setState和forceUpdate来说,它们都是针对某个component来进行更新渲染的
- setState和forceUpdate,它们更新的类型是不一样的
四、Fiber Scheduler
创建更新之后,找到Root然后进入调度,同步和异步操作完全不同,实现更新分片的性能优化
这就是react最核心的部分了。对应的包就是 react-reconciler,里面就是Fiber了,Fiber就是一个更新和调度的过程
1、React16之前和React16的区别
- React 16 之前和React16比较: react 16中,react团队用Fiber重写了react的核心
- 比较一: 是否能中断
- react 16之前的虚拟dom,任务一旦执行,就 无法中断,必须更新完整个树,然后才能执行之后的代码,js 线程一直占用主线程,就会导致浏览器卡顿;
- react 16的Fiber 的特性就是 时间分片(time slicing)和中断(supense)。时间分片为,用requestIdleCallback来获取浏览器的空闲时间片,然后在这个空闲时间片中来执行任务;中断即 执行任务的时候可以中断,把权限还给浏览器。从而提升了用户交互和浏览器动画的优先级,
- React 16 之前和React16 源码层面就是干了一件递归改循环的事情。
所以 fiber所谓的性能提升只是将reconciliation中diff过程的优先级往后调了,只在浏览器空闲时执行,从而提升了用户交互、动画渲染相关的优先级
2、Virtual DOM Tree和Fiber tree的关系
- React 在 调用render函数进行渲染的时候时,会通过 React.createElement 创建一颗 Element 树,这颗树就是 Virtual DOM Tree;然后 再 给这个VDOM的每个节点加上一些上下文信息就变成了Fiber Node(每个 Fiber Node 节点与 Virtual Dom节点 一一对应),最后将 Fiber Node 链接起来就为 Fiber Tree 了。
- Fiber Tree 是一个链表结构(而不像vdom那样是一个树结构),而这个链表结构在Fiber Node之间是通过return、child、sibling这三个属性来进行连接的;
- 在后续调用setState进行更新的过程中,每次重新渲染都会重新创建 Virtual DOM Tree,; 但是 Fiber Tree不会被重新创建(只有在初始的时候才创建一次),而是使用Virtual DOM Tree的数据来更新自己的属性。
3、react更新流程概览
- Fiber 的工作流程:这个执行的目的就是,让react中低优先级的异步任务更新,不会阻塞浏览器的高优先级的动画的更新,能够保证浏览器动画流畅性
- (1)首先 在执行 ReactDOM.render() 或 setState 的时候,来创建一个更新队列UpdateQueue
- 第一部分从 ReactDOM.render() 方法开始,把接收的 React Element 先构建为VDOM,然后再转换为 Fiber 节点,并为其设置优先级,创建 Update,加入到更新队列(Update Queue),等待调度
- (2)然后,进行requestWork,判断 UpdateQueue(更新队列)中的任务是同步还是异步,可能是用expirationTime来判断的
- 如果是同步任务,则立即执行,执行完了就行了
- 如果是异步任务,再判断当前任务是否过期,如果过期了也立即执行;如果没过期则进入 调度的流程。
- <1>首先使用 requestIdleCallback API(如果浏览器不支持需要polyfill)让浏览器先执行它自己高优先级的任务,然后在浏览器空闲的时候再来获取时间片;最后 在获取到空闲时间片之后,再来执行react低优先级的异步任务,也就是进入 下面的reconcile阶段
- <<1>>浏览器在执行react异步任务的时候,会传入一个deadline对象,这个参数也是表示一个时间,代表react异步任务可以执行多久(为了避免react任务执行过久造成卡顿),如果超过了这个时间,react就会进行中断当前Fiber节点的更新任务(因为Fiber节点是一个一个的执行的,所以可以中断),把控制权还给浏览器。然后又判断当前浏览器是否有空闲时间片,再进行这个循环
- <<2>>而具体的异步任务 就是,从根节点开始遍历 Fiber Node,构建 WokeInProgress Tree,并且生成 effectList。这个阶段可以理解为就是 Diff 的过程
- <2>最后,把UpdateQueue中的所有任务全部执行完了之后,进入commit阶段根据 EffectList 更新 真实DOM,这个阶段不可中断。
- commit 阶段会执行如下的声明周期方法,如下
getSnapshotBeforeUpdate\componentDidMount\componentDidUpdate\componentWillUnmount
4、Fiber tree的Diff操作
diff的过程是从之前的递归树状结构,变成一个循环遍历链表的形式
- 双缓存技术:Fiber采用了一种双缓存技术(double buffering)来操作diff;即在内存中同时维护两颗Fiber Tree
- <1>第一颗为当前页面上显示的树,被称为 current Fiber Tree;初始的时候创建的就是这个 current Fiber Tree
- <2>第二颗为正在内存中构建的,页面更新之后即将显示在页面中树,被称为WorkInProgress Tree
- WorkInProgress Tree 构造完毕后,得到的就是新的 Fiber Tree,然后根节点把 current 指针指向WorkInProgress Tree,WorkInProgress Tree就变成了current Fiber Tree,以前的current Fiber Tree就丢弃了就好了。React应用的根节点通过current指针在不同Fiber树的rootFiber间切换来实现Fiber树的切换
- <3>每个 Fiber上都有个alternate属性,指向它们可以相互替换的那个Fiber,比如 current Fiber的alternate属性就指向WorkInProgress Fiber,而WorkInProgress Fiber的alternate属性就指向current Fiber
- 构建WorkInProgress Tree:创建 WorkInProgress Tree 的过程也是一个 Diff 的过程,diff过程中生成的 Effect List,就是Diff过程中计算出来的变更。diff是深度优先遍历。
- <1>单个节点diff过程:diff操作都是在节点树的同一层级中进行的。目的是找到可以复用的节点,如果没找到可复用的节点,就创建一个新节点
- 新老节点的对比,我们以新节点为标准
- key 和节点类型都相同:表示可以复用旧的节点,直接退出当前循环返回复用的节点
- key 相同但 节点类型 不同:无法复用旧节点并且旧节点的兄弟节点也不可复用,则把旧节点和它的兄弟节点全部删除
- key 不同:无法复用旧节点,但是其兄弟可能还是可以复用上次的,所以只这里只是删除旧的节点,然后继续调用while循环
- 到最后如果都没有找到可以复用的节点,那么就创建一个新的Fiber节点来返回
- <2>整体流程:
- 构建的时候是从链表头部的最外层节点开始构建的
- 外层节点构建完成后,再去构建它的子节点
- 当子节点构建完成后,要指定它们之间的关系
- 关系计算逻辑:
- 只有第一个子级才算是父级的子级(父级 Fiber 对象的 child 只会存储第一个子级)
- 第二个子级是第一个子级的下一个兄弟节点,依次类推(兄弟子级也会存储它们的父级)
- 构建完成后,再判断父级下的第一个子级是否还有子级
- 如果有则继续向下构建
- 如果没有判断下一个兄弟节点是否有子级
- 如果都没有,则向上判断父级的兄弟节点
- 最终又回到最外层的节点
5、batchedUpdates
- 简单说就是 在执行一个函数之前,要设置一个变量为 isBatchingUpdates=true,就代表在 batchedUpdates机制中; 而在这个函数执行完了之后, isBatchingUpdates就会被设置为false
- 如果 isBatchingUpdates=true,即在这个batchUpdates机制中的时候,就会进入异步调度的流程,就是上面的那个
- 如果 isBatchingUpdates=false,就代表是一个同步的任务
- 而,一般情况下,放到setTimeout等中去执行的任务,就不是isBatchingUpdates机制中的任务了
// 下面为一个react代码
state = {number:0}
handleClick = () => {
// 直接执行go函数,此时在batchupdate机制中;不会调用一个setState就直接改变值,而是等最后批量一起执行,所以 后面的go函数 全部输出 0
this.go();
// 用了个异步执行go函数,此时不在batchupdate机制中;那么会调用一个setState就直接改变值,所以这种方式性能很差,输出 1,2,3
setTimeout(()=>{
this.go();
},4);
}
go = () => {
const num = this.state.number;
this.setState({number: num + 1});
console.log(this.state.number);
this.setState({number: num + 1});
console.log(this.state.number);
this.setState({number: num + 1});
console.log(this.state.number);
}
6、reactScheduler
这个是react中调度的包
维护时间片、模拟requestIdleCallback、调度列表和超时判断
7、performWork
是否有 deadline的区分、循环渲染root的条件、超过时间片的处理
8、renderRoot
renderRoot需要做的事情就是:使用workLoop函数进行循环Fiber节点并且更新Fiber节点; 如果更新过程中出现错误,那么就捕获错误并处理错误; 最后再进行 commit
- 调用workLoop进行循环更新Fiber节点: 遍历整个Fiber tree,然后把组件或者DOM对应的Fiber节点拿出来,单一的进行更新,这是一个循环的操作,需要把这个 Fiber tree 都遍历一遍
- workLoop: 就是把整棵树,每个节点都进行遍历,并且拿出来单独进行更新;因为Fiber节点如果有更新的话,它的updateQueue这个属性上面是会有内容的;
- 每个节点在更新完之后,会返回它的第一个child
- 到最后 return 的为null了,代表当前的节点为FiberRoot节点,就退出workLoop循环
- 最后执行完了之后,就可以把Fiber树变成真正的DOM树,来进行更新了
- 更新过程中的错误捕获:即如果在上面workLoop的过程中,更新一个节点报错了,那么就不会执行它下面的所有子节点了,而是继续执行另外的节点
五、各类组件的Update
讲解10多种不同类型的组件的更新过程,以及如何遍历节点形成新的Fiber树
在update的过程中,不涉及真正的dom操作,只有到 completeUnitOfWork 的时候,才涉及真正的dom操作
1、入口和优化
判断组件的更新是否可以优化,根据节点类型分发处理,根据expirationTime等信息判断是否可以跳过
2、reconcilerChildren-array
这一章讲的是 react中key的左右,还是很重要的;因为react中的Fiber用的key和vue中虚拟dom用的key不一样
react里面,依然是 用相同的顺序来遍历新老的Fiber节点,来看它们的key是否相同,如果相同就复用老的Fiber节点,如果不相同就跳出当前循环
createChild()函数,可以用来创建Fiber节点
六、完成节点任务
完成节点更新之后完成节点的创建,并提供优化到最小幅度的DOM更新列表
这一章实际上是 diff 操作
1、completeUnitOfWork的整体流程和意义
根据是否中断调用不同的处理方法、判断是否有兄弟节点来进行不同的操作、完成节点之后赋值effect链
2、更新DOM时进行的diff判断
实际上就是调用diffProperties()函数,计算需要更新的内容(这里就是一个diff操作,即对比不同的vdom,来判断当前节点是否需要更新,根据不同的节点需要做不同的操作)
七、commitRoot
根据更新列表最小幅度的改变DOM,实现UI的更新。
这里就是讲 真实DOM操作了,挂载真实DOM的阶段就是 commit 阶段
commit阶段是不可以被中断的
预备工作、三个循环、善后工作、
八、总结
Fiber带来的提升:更新粒度的细分、异步更新模式、提供了Hooks实现的基础
如何看源码:跟踪调试看大致流程、理解功能概念、写demo验证自己想法