React 设计原理 Part 1 理念篇
本文主要是对《React 设计原理》一书的笔记和总结,方便之后回顾只看笔记,不再重新细读书籍。
目录
前端框架原理
1.1、当前主流的两种描述 UI 的方案
1.2、如何组织UI和逻辑
1.3、如何在组件间传递数据
1.4、前端框架分类依据
1.5、AOT-预编译
1.6、Virtual DOM
1.7、前端框架的实现原理(总结 table)
React概念
2.1、React 重运行时,是如何实现快速响应的?
2.2、瓶颈解决方案
2.3、为什么使用新架构?原理?
2.4、React 迭代历史
2.5、Fiber详细介绍
前端框架原理
本章主要从宏观层面叙述了为什么前端划分了多种不同框架,以及各框架之间是怎么实现渲染更新的。
所有的现代前端框架实现原理:UI=f(state)【框架内部运行机制为根据当前状态渲染视图】
- 第一步:根据自变量(state)变化计算出 UI 变化(主流技术:VDOM)
- 第二步:根据 UI 变化执行具体的宿主环境 API
1.1 当前主流的两种描述 UI 的方案
- 方案1 JSX:核心是扩展ES(ECMScript)语法,使它能够描述UI(React)
- JSX本质 是Facebook/Meta 提出的类XML语法的ECMScript语法糖
- 方案2 模板语法:核心是扩展HTML,使它能够描述逻辑(Vue)
1.2 如何组织UI和逻辑
- 方式1:自变量变化,直接导致 UI 变化
- 方式2:自变量变化,导致“无副作用”的因变量变化,进而 UI 变化
- 方式3:自变量变化,导致“有副作用”的因变量变化,产生副作用
副作用定义:函数调用过程中掺杂的外部可观察的变化 - 是函数式编程的概念
1.3 如何在组件间传递数据
- 方式1:UI
- 组件的自变量或因变量通过UI传递给另一个组件,作为其自变量
- 方式2:store(多个框架都有对应的状态管理库,such as Redux)
state(状态):组件内部定义的自变量
props(属性):其他组件传递而来的自变量
1.4 前端框架分类依据
- 根据自变量建立对应关系的抽象层级:React(应用级)、Vue(组件级)、Svelte(元素级)
useState:定义组件内部的自变量
useReducer:UseState本质是内置 render 的 useReducer,useReducer 也相当于组件内部定义的自变量
useContext:是 React 中 store 的实现,用于跨层级讲其他组件的自变量传递给当前组件
useMemo:采用“缓存的方式”定义组件内部的“无副作用因变量”
useCallback:采用“缓存的方式”定义组件内部的“无副作用因变量”,缓存的值为函数形式
useEffect:定义组件内部“有副作用的因变量”
useRef:用于在组件多次 render 之间缓存一个“引用类型的值”
1.5 AOT-预编译
粒度越细的更新,AOT优势越大
针对当前主要的两种描述 UI 方式来讲:
- 方式1 JSX:对“JSX描述UI”的框架收益少,因为ES 的灵活性太高,难以解析
- 方式2 模版语法:对“模版语法描述 UI”的框架,提升收益大(例如Vue3对静态、动态节点的分析,跳过)
编译有两个不同时机执行:
- 时机1AOT(预编译) - 代码在构建时,宿主环境获得的是编译后的代码(可以直接被执行)
- 时机2 JIT(即时编译) - 代码在宿主环境执行时,代码在宿主环境中编译并执行
两者区别:使用 JIT 的应用在首次加载时慢于使用 AOT 的应用;体积也可能大于AOT 应用(运行时会增加编译器代码)
1.6 Virtual DOM
是实现“根据自变量(state)变化计算出 UI 变化”的一种主流技术,主要概括为两个步骤
- 步骤一:将元素描述的 UI 转化 为“VDOM描述的 UI”
- 步骤二:对比前后 VDOM 描述的 UI,计算出 UI 发生变化的部分
计算出变化的部分后,根据 UI 变化执行到具体的宿主环境 API - VDOM 对接不同的宿主环境即可实现“一个框架,多端渲染”
落实到各框架中实现如下;
Vue:Vue 使用模板语法描述 UI,模板语法编译为 render 函数,执行两步:
- 第一步:render 函数执行后 返回 VDOM 描述的 UI
- 第二步:将变化前后的 VDOM 描述的 UI 进行比较,计算出 UI 变化的部分 - patch
React:React 使用 JSX 描述 UI,JSX 编译为 creatElement 方法,执行两步:
- 第一步:createElement 执行后 返回 React Element 描述的 UI
- 第二步:将变化后 React Element 描述的 UI 和变化前 FiberNode 描述的 UI 进行比较,计算出变化的部分,生成此次更新 - FiberNode 描述的UI
1.7 前端框架的实现原理
框架比较 | 原理(原理图如上) | 变量追踪 | 更新 |
---|---|---|---|
Svelte:一款重度依赖 AOT 的元素级框架 | Svelte 会根据需要引入运行时代码(不需要的运行时代码不被引入,例如只有静态节点时,不引入 p 方法、store 相关代码) 元素级:一个自变量变化,对应p方法中一个 if 操作 DOM 语句 极致的编译时框架 |
追踪所有变量声明:使用 instance 方法管理变量,返回值就是组件对应的 ctx,保证独立的上下文 | 1、在 ctx 保存自变量的值 2、标记 dirty 元素 3、利用微任务调度更新 4、执行 p 方法(操作 DOM) 直接建立自变量与元素的对应关系 |
Vue3 | 组件级:effect 回调函数更新对应的是 组件 UI 更新 | 使用 effect 订阅,自变量变化后,Vue3执行“订阅其变化的 effect 回调函数” watchEffect首次执行/依赖的自变量变化,执行更新 AOT:通过 PatchFlags 标记 VNode 是否为可变的 |
1、调用组件的 render ,生成对应 VNode 2、与上一次更新的 VNode 同时传入 patch ,找到变化,最终执行对应的 DOM 操作 |
React | 全量引入运行时代码,重运行时框架 极致的运行时框架 应用级:每次更新都是从应用的根结点开始,遍历整个应用 |
(内部有优化机制) |
React概念
带着问题来阅读,本章疑问:1、为什么React16要使用fiber重构?有什么优点?
解答:重构的原因主要是旧架构无法实现 Time Slice
那为什么要实现 时间切片?是因为前端当前有两大瓶颈:
- CPU瓶颈(根本原因是,渲染流水线和js执行同时作为宏任务,如果js执行时间过长,渲染流水线图片绘制低于60帧-屏幕刷新率,就会掉帧、卡顿)
- I/O 瓶颈(虽然下载速度无法控制,但是 React 希望通过划分优先级,和统一调度来优化,该方案的核心需要可中断更新,即时间切片)
React 在运行时将 VDOM 的执行过程拆分成一个一个独立宏任务,每个宏任务执行时间限制在一定范围内。
- CPU瓶颈 - 通过将一个长宏任务,拆解成多个短宏任务,不影响渲染流水线
- I/O瓶颈 - 实现可中断更新,在一个短宏任务完成前,下一个短宏任务开始前,检查是否应该中断
2、fiber 是怎么实现的?
在 render 阶段,任务是可以随时中止的,根据当前的可用时间处理一个或多个 fiber 节点,得益于 fiber 对象中存储的元素上下文信息以及构成的链表结构,使其能够将执行到一半的工作仍保存在内存的链表中。在重新获得控制权后,又可以根据保存在内存中的上下文信息快速找到停止的fiber节点,然后继续工作执行工作单元。
render 阶段产生的结果是一个 标记了 flags/effect-list 的节点树 wip(workInProgress)。
2.1 React 重运行时,是如何实现快速响应的?
影响快速响应的问题归结为两类场景:
- CPU 瓶颈 - 大量计算操作/设备性能不足导致的掉帧和卡顿
- I/O 瓶颈 - 数据延迟
事件循环核心:在宏任务结束前,线程会遍历其微任务队列执行。
浏览器渲染核心:渲染流水线和执行 js 同时作为宏任务,如果 js 执行时间过长,渲染流水线绘制图片的速度跟不上屏幕刷新频率,会造成掉帧和卡顿,是 CPU 瓶颈的根本原因 。
2.2 瓶颈解决方案【解答问题-优点】
CPU 瓶颈解决方案:减少运行时代码执行流程
- Svelte、Vue3 的努力方向:利用 AOT 在编译时减少运行时代码流程;
- React 在 运行时 找解决方案:将 VDOM 的执行过程拆分成一个个独立的宏任务,将每个宏任务执行时间限制在一定范围内(5ms) -> 即 Time Slice(时间切片)
- 结果:一个“会造成掉帧的长宏任务”被拆解为多个“不会造成掉帧的短宏任务”
I/O 瓶颈解决方案:用户对不同操作交互的卡顿感感知程度不同
- React:1、划分优先级 2、统一调度 3、可中断更新(在一个短宏任务完成前,下一个短宏任务开始前,检查是否应该中断) -> 需要 Time Slice 实现(时间切片)
2.3 为什么使用新架构?原理?
【解答问题-为什么】:重构的原因主要是旧架构无法实现 Time Slice
Fiber 架构三个主流程:Scheduler(调度器);Reconciler(协调器 - render 阶段);Render(渲染器 - commit 阶段)
- Scheduler 和 Reconciler 都在内存中进行,不会更新宿主环境 UI
- Scheduler 和 Reconciler 都随时可能由于以下原因被打断:
- 原因1:其他更高优先级任务需要执行
- 原因2:当前 Time Slice 没有剩余时间
- 原因3:发生错误
Time Slice 实现原理:新架构 Fiber 中,Recondiler(协调器)每次循环都会调用 shouldYield 判断当前 Time Slice 是否有剩余时间,没有剩余时间则暂停更新流程,将主线程交给渲染流水线,等待下一个宏任务再继续执行。
2.4 React 迭代历史
React 在v18 中不再提供多种开发模式,而是以 “是否使用并发特性” 作为 “是否开启并发更新” 的依据
之前为了渐进式更新,提供三种开发模式(StrictMode:对“不符合并发更新规范的代码”提示 Warning)
- Legacy 模式,通过
ReactDOM.render(<App/>, rootNode)
创建的应用默认该模式,默认关闭 StrictMode。【新架构,未开启并发更新,无新功能,例如 v16、v17】 - Blocking模式,通过
ReactDOM.createBlockingRoot(rootNode).render(<App/>)
创建的应用默认该模式,默认开启 StrictMode【新架构,未开启并发更新,有部分新功能,例如 Automatic Batching】 - Concurrent模式,通过
ReactDOM.createRoot(rootNode).render(<App/>)
创建的应用默认该模式,默认开启 StrictMode【新架构,开启并发更新】
2.5 Fiber详细介绍
React 中的三种节点
- React Element(React 元素)
- React Component(React 组件)- 函数、class类
- FiberNode(Fiber 架构的节点)- 多个属性用于表示各种信息
- alternate - 指向“另一个缓冲区中对应的 FiberNode”
Fiber 架构由多个 FiberNode 组成树状结构,FiberNode 之间通过 return、child、sibling 连接。
Fiber 中同时存在两棵树:
- 一棵是“真实UI对应的 Fiber Tree”(前缓冲区)
- 另一棵是“正在内存中构建的 Fiber Tree”(后缓冲区)
mount 时 Fiber Tree 的构建
- step1:创建 FiberRootNode
- step2:创建 fiberNode(tag:),代表 HostRoot,即 HostRootFiber(FiberRootNode.current)
- step3:从 HostRootFiber 开始,以 DFS 的顺序生成 FiberNode
- step4:在遍历过程中,为 FiberNode 标记 effectTag 表示不同副作用
当 WIP FiberTree 生成完毕,FiberRootNode会被传递给 Renderer-commit阶段(执行 side-effects-list【在18改为 flags】);
Renerder-commit 完成工作后,WIP FiberTree 已经渲染到宿主环境中,FiberRootNode.current 切换指向 WIP
补充知识点说明:
- fiberNode 中的 tag:表示fiber 类型(Class Component/Host Component/…)
- DFS:遍历时使用深度优先搜索算法,生成 WIP FiberNode,连接起来构成 WIP FiberTree
- FiberRootNode:负责管理该应用的全局事宜(两棵 tree 之间切换、任务过期信息、任务调度)
- HostRoot:代表“应用在宿主环境挂载的根节点”
- HostRootFiber:Current FiberTree 的根节点,当前有且仅有一个,对应“首屏渲染时仅有根节点的空白页面”【React 有内部优化路径,“只有唯一文本节点”的 FiberNode 不会生成独立的 FiberNode。】
补充:
- 显卡的职责是:合成图像并写入后缓冲区
- 对于生产者和消费者供需不一致的场景,双缓存是很好的优化手段
之前看 React 开发者写的解析 fiber 的文章,笔记如图: