React 设计原理 Part 1 理念篇

Posted by CodingWithAlice on November 13, 2023

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 前端框架的实现原理

FC310B50-5005-44B2-A32E-B2C574328BB5

A1D0DB7F-1C92-431A-B562-556B306A0E63

C102C8F5-4CCF-449E-879D-749B6B124A1B

框架比较 原理(原理图如上) 变量追踪 更新
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

19FFA6ED-4CC8-435D-AAAD-EEE47AD5D66E

补充知识点说明:

  • fiberNode 中的 tag:表示fiber 类型(Class Component/Host Component/…)
  • DFS:遍历时使用深度优先搜索算法,生成 WIP FiberNode,连接起来构成 WIP FiberTree
  • FiberRootNode:负责管理该应用的全局事宜(两棵 tree 之间切换、任务过期信息、任务调度)
  • HostRoot:代表“应用在宿主环境挂载的根节点”
  • HostRootFiber:Current FiberTree 的根节点,当前有且仅有一个,对应“首屏渲染时仅有根节点的空白页面”【React 有内部优化路径,“只有唯一文本节点”的 FiberNode 不会生成独立的 FiberNode。】

补充:

  • 显卡的职责是:合成图像并写入后缓冲区
  • 对于生产者和消费者供需不一致的场景,双缓存是很好的优化手段

之前看 React 开发者写的解析 fiber 的文章,笔记如图:

81A577A2-C767-4ACE-849A-71226663D410

7FA11363-83D0-4B31-8970-B6639F43BF72

5236A8E8-FD3F-40FF-BBEC-9B65A1E04E95