react源码解析

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验证自己想法


版权声明:本文为rocktanga原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。