文章目录
前言
可能一直在写代码的路上,大多数时候就是个工具人。今天一起看看react18的实现原理,持续学习中。
React 初始化过程
react 在渲染过程中要做很多事,不会直接通过初始元素直接渲染。还有虚拟节点。除了初始元素能生成虚拟节点外,还有哪些能生成虚拟节点?
节点类型
- DOM 节点:虚拟 DOM 节点。当初始元素的 type 为字符串的时候 react 就会创建虚拟 DOM 节点
- 组件节点:当初始元素的 type 为函数或是类的时候,react 就会创建虚拟组件节点
- 文本节点:直接书写字符串或者数字,react 会创建文本节点
- 空节点:react 代码中的三目表达式 a>b?1:false,用来条件渲染,当为 false 时不会渲染。其实遇到字面量 null,false,true,undefined 在 react 中均会被创建为一个空节点,在渲染时遇到空节点将什么都不会渲染
- 数组节点:不是直接渲染数组本身,当 react 遇到数组时会创建数组节点,但是不会直接进行渲染,而是将数组的每一项拿出来,根据不同的节点类型去做相应的事情。所以数组里的每一项只能是上面这五种节点类型
react 工作是通过初始元素或者可以生成虚拟节点的东西生成虚拟节点,然后针对不同的节点类型去做不同的事情最终生成 DOM 挂载到页面上。
首次渲染阶段
1.初始元素—DOM 节点
针对初始元素的 type 属性为字符串时,react 会通过 document.createElement 创建真实 DOM。因为初始元素的 type 为字符串,所以会根据 type 属性创建不同的真实 DOM。创建完真实 DOM 后会立即设置该真实 DOM 的属性,比如直接在 jsx 中可以直接书写 className,style 等等都会作用到真实的 DOM 上。
2.初始元素—组件节点
如果初始元素的 type 属性是一个 class 类或者 function 函数时,那么会创建一个组件节点。所以针对类或函数组件,处理是不同的。
函数组件
对于函数组件会直接调用函数,将函数的返回值进行递归处理(看看是什么节点类型,然后去做对应的事情,所以一定要返回能生成虚拟节点的东西),最终生成一颗 vDom 树。
类组件
对于类组件而言相对会麻烦
-
首先创建类的实例(调用 constructor)
-
调用生命周期方法 static.getDerivedStateFromProps
-
调用生命周期方法 render,根据返回值递归处理,跟函数组件处理返回值一样,最终生成一颗 vDom 树
-
将组件的生命周期方法 componentDidMount 加入到队列中等待真实 DOM 挂载到页面后执行(前面说 render 是一个递归处理,所以如果一个组件存在父子关系的时候,那么肯定要等到子组件渲染完父组件才能走出 render,所以子组件的 compontDidMount 一定是比父组件先入队列的,肯定先运行)
3.文本节点
针对文本节点,会直接通过 document.createTexNode 创建真实的文本节点
4.空节点
如果生成的是空节点,那么将什么都不会做
5.数组节点
react 不会直接渲染数组,而是将数组的每一项拿出来遍历,根据不同的节点类型去做相应的事情,直到递归处理完数组里的每一项
总结:处理完所有的节点后,我们的 vDom 树和真实 DOM 也会创建好,react 会将 vDom 树保存起来,方便后续使用。然后将真实 DOM 都挂载到页面上。
React 更新过程
更新场景
1.组件更新(setState)
经常用 setState 来重新设置组件的状态进行重新渲染。使用 setState 只会更新调用此方法的类。不会设计到兄弟节点以及父级节点。影响范围仅仅是自己的子节点,步骤如下:
- 运行当前类组件的生命周期方法 static.getDerivedStateFromProps。根据返回值合并当前组件的状态
- 运行当前类组件的生命周期方法 shouldComponentUpdate。如果该方法返回 false,直接终止更新流程
- 运行当前类组件的生命周期方法 render,得到一个新的 vDom 树,进入新旧两棵树的对比更新
- 将当前类组件的生命周期方法 getSnapshotBeforeUpdate 加入执行队列,等待将来执行
- 将当前类组件的生命周期方法 componentDidUpdate 加入执行队列,等待将来执行
- 重新生成 vDom 树
- 执行队列,此队列存放的是更新过程设计到原本存在的类组件生命周期方法 getSnapshotBeforeUpdate
- 根据 vDom 树更新真实 DOM
- 执行队列,此队列存放的是更新过程中所涉及到原本存在的生命周期方法 componentDidUpdate
- 执行队列,此队列存放的是更新过程中所有卸载类组件的生命周期方法 componentWillUnMount
2.根节点更新(ReactDOM.createRoot().render)
在 ReactDOM 的新版中,不在直接使用 ReactDOM.render 进行更新,而是通过 createRoot(要控制的 DOM 区域)的返回值来调用 render
对比更新过程(diff)
对比更新就是将新 vDom 树和之前首次渲染过程中保存的老 vDom 树对比发现差异后做一系列操作的过程。
React 的 diff 算法将之前的复杂度 O(n^3)降为了 O(n),它做了以下几个假设
- 假设此次更新的节点层级不会发生移动(直接找到就树中的位置对比)
- 兄弟节点之间通过 key 进行唯一标识
- 如果新旧节点类型不相同,那么它认为就是一个新的结构,比如之前就是初始元素 div 现在变成初始元素 span 那么它会认为整个结构全部变了,无论嵌套了多深也会全部丢弃重新创建
key 作用
为了通过旧节点,寻找对应的新节点进行对比提高节点的复用率。
React Fiber 架构
单线程 CPU 调度策略
1.先到先得(First-Come-First-Server,FCFS)
最简单调度策略,简单说就是没有调度。谁先来谁就先执行,如果中间某些进程因为 I/O 阻塞,这些进程会挂起移回就绪队列(重新排队)
2.轮转调度
基于时钟的抢占策略,也是抢占策略中最简单的一种:公平的给每一个进程一定的执行时间,当时间消耗完毕或阻塞,操作系统就会调度其他进程,将执行权抢占过来
要点是确定合适的时间片长度:太长了,长进程霸占太久资源,其他进程得不到响应(等待时间过长),太短了,进程抢占和切换都是需要成本的,而且成本不低,时间片太短,时间浪费给在上下文切换上,导致进程干不了什么实事。
因此,时间片的长度最好符合大部分进程完成一次典型交互所需的时间
3.最短进程优先(Shortest Process Next,SPN)
按照进程的预估执行时间对进程进行优先级排序,先执行完短进程,后执行长进程,这是一种非抢占策略
SPN 缺点:如果系统有大量短进程,那么长进程可能会饥渴得不到响应
4.最短剩余时间(Shortest Remaining Time,SRT)
5.最高响应比优先(HRRN)
6.反馈法
分片设计
前端如何解决
- 优化每个任务,让它有多快就多快,挤压 cpu 运算量
- 快速响应用户,让用户觉得够快,不能阻塞用户的交互(React 分片)
- 尝试 Worker 多线程
Vue 选择的是 1,使用模板让它有了