json react 展示工具_React 入门儿

本文深入探讨了React的工作原理,从框架的意义出发,解释了React的哲学、虚拟DOM以及首次渲染的流程。通过一个简单的调试仓库,作者展示了React如何创建虚拟DOM元素,并详细阐述了从`createReactElement`到`enqueueUpdate`再到`Fiber`架构的工作过程。此外,文章还介绍了React的事件委托机制,解析了事件注入和事件处理的流程,以及如何通过`setState`更新组件状态。最后,讨论了React的时间调度机制,解释了为何时间片能提高性能,并概述了任务调度器的工作方式。
摘要由CSDN通过智能技术生成
共计 10137 字,阅读时长约 25 分钟

谁都没有看见过风,更不用说你和我了。但是当纸币在飘的时候,我们知道那是风在数钱。

React 影响着我们工作的方方面面,我们每天都在使用它,只窥其表却难以窥其里。正所谓看不如写,本篇文章的目的就是从原理层面探究 React 是如何工作的。

工具

在写文章之前,为了方便理解,我准备了一个懒人调试仓库 simple_react[1] ,这个仓库将 benchmark 用例(只有两个 ^ ^)和 React 源码共同放在 src 文件夹中,通过 snowpack 进行热更新,可以直接在源码中加入 log 和 debuger 进行调试。当然这里的“源码”并不是真的源码,因为 React 源码中充斥着巨量的 dev 代码和不明确的功能函数,所以我对源码进行了整理,用 typescript 对类型进行了规范,删除了大量和核心流程无关的代码(当然也误删了一些有关的 ^ ^)。

如果你只是希望了解 React 的运行流程而不是写一个可以用的框架的话,那么这个仓库完全可以满足你学习的需要。当然,这个仓库基于 React16.8 ,虽然这个版本并不包括当前的航道模型 Lane 等新特性,但是是我个人认为比较稳定且更适合阅读的一个版本。

(如果希望调试完整的源码,也可以参考 拉取源码[2] 通过 yarn link 来进行 debug)

文章结构

  1. fiber 架构设计及首次渲染流程
  2. 事件委托机制
  3. 状态的更新
  4. 时间片

在了解 React 是如何工作之前,我们应该确保了解几点有关 React 的基础知识。

Why Framework

首先,我们需要知道使用框架对于开发的意义是什么。如果我们还处于远古时期使用纯 JS 的阶段,每次数据的改变都会引发组件的展示状态改变,因此我们需要去手动的操作 DOM 。如果在某一秒内,数据异步的连续改变了几十次,根据展示逻辑我们也需要连续对 DOM 进行几十次修改。频繁的 DOM 操作对网页性能的影响是很大的,当然,创建 DOM 元素和修改 DOM 元素的属性都不过分消耗性能,主要在于每次将新的 DOM 插入 document 都会导致浏览器重新计算布局属性,以及各个视图层、合并、渲染。所以,这样的代码性能是十分低下的。

可以试想这样一个场景。对于一个前端列表组件而言,当存在 3 条数据的时候展示 3 条,当存在 5 条数据的时候展示 5 条。也就是说 UI 的呈现在某种程度上必然会和数据存在某种逻辑关系。如果 JS 能够感知到关键数据的改变,使用一种高效的方式将 DOM 改写成与数据相对应的状态。那么于开发者而言,就可以专注于业务逻辑和数据的改变,工作效率也会大幅提高。

所以, 框架 最核心的功能之一就是 高效地 达成 UI 层和数据层的统一。

React 哲学

React 本身并不是框架, React 只是一个 JavaScript 库,他的作用是通过组件构建用户界面,属于 MVC 应用中的 View 视图层。React 通过 props 和 state 来简化关键数据的存储,对于一个 react 组件函数而言,在 1 秒内可能被执行很多次。而每一次被执行,数据被注入 JSX , JSX 并不是真实的 DOM ,在 React 中会被转换成 React.createElement(type, props, children) 函数,执行的结果就是 ReactElement 元素 ,也即是 虚拟 DOM ,用来描述在浏览器的某一帧中,组件应该被呈现为什么样子。

Virtual Dom

VirtualDom 并非 React 专属,就像 redux 也可以在非 React 环境下使用一样,它们只是一种设计的思路。

事实上, React 在使用 fiber 架构之前的 Virtual Dom 和 diff 过程要相对直观一些。但是在引入了 fiber 架构之后整个流程变得冗长,如果单纯想了解 VirtualDom 和 diff 过程的原理也可以通过 simple-virtual-dom[3] 这个仓库来学习。

VirtualDom 的本质是利用 JS 变量 对真实 DOM 进行抽象,既然每一次操作 DOM 都可能触发浏览器的重排消耗性能,那么就可以使用 VirtualDom 来缓存当前组件状态,对用户交互和数据的变动进行批次处理,直接计算出每一帧页面应该呈现的最终状态,而这个状态是以 JS 变量 的形式存在于内存中的。所以通过 VirtualDom 既能够保证用户看到的每一帧都响应了数据的变化,又能节约性能保证浏览器不出现卡顿。

第一次渲染 First Render

首先我们应该注意到 React(浏览器环境) 代码的入口 render 函数

ReactDOM.render(<App />, domContainer)

这个 render 过程中, React 需要做到的是根据用户创造的 JSX 语法,构建出一个虚拟的树结构(也就是 ReactElement 和 Fiber )来表示用户 期望中 页面中的元素结构。当然对于这个过程相对并不复杂(误),因为此时的 document 内还是一片虚无。就思路上而言,只需要根据虚拟 DOM 节点生成真实的 DOM 元素然后插入 document ,第一次渲染就算圆满完成。

createReactElement

通常我们会通过 Babel 将 JSX 转换为一个 JS 执行函数。例如我们在 React 环境下用 JSX 中写了一个标题组件

Class Component

那么这个组件被 Babel 转换之后将会是

React.createElement('h1', { className: 'title' }, [
React.createElement('div', null, [ 'Class Component' ]
])

传统编译讲究一个 JSON 化,当然 JSX 和 React 也没有什么关系, JSX 只是 React 推荐的一种拓展语法。当然你也可以不用 JSX 直接使用 React.createElement 函数,但是对比上面的两种写法你就也能知道,使用纯 JS 的心智成本会比简明可见的 JSX 高多少。我们可以看出, React.createElement 需要接收 3 个参数,分别是 DOM 元素的标签名,属性对象以及一个子元素数组,返回值则是一个 ReactElement 对象。

事实上, JSX 编译后的 json 结构本身就是一个对象,即使不执行 React.createElement 函数也已经初步可以使用了。那么在这个函数中我们做了什么呢。

一个 ReactElement 元素主要有 5 个关键属性,我们都知道要构建成一个页面需要通过 html 描述元素的类型和结构,通过 style 和 class 去描述元素的样式呈现,通过 js 和绑定事件来触发交互事件和页面更新。

所以最重要的是第一个属性,元素类型 type 。如果这个元素是一个纯 html 标签元素,例如 div ,那么 type 将会是字符串 div ,如果是一个 React 组件,例如

function App() {
    
return (
<div>Hello, World!div>
)
}

那么 type 的值将会指向 App 函数,当然 Class 组件 也一样(众所周知 ES6 的 Class 语法本身就是函数以及原型链构成的语法糖)

第二个属性是 props ,我们在 html 标签中写入的大部分属性都会被收集在 props 中,例如 id 、 className 、 style 、 children 、点击事件等等。

第三个第四个属性分别是 keyref ,其中 key 在数组的处理和 diff 过程中有重要作用,而 ref 则是引用标识,在这里就先不做过多介绍。

最后一个属性是 $$typeof ,这个属性会指向 Symbol(React.element) 。作为 React 元素的唯一标识的同时,这个标签也承担了安全方面的功能。我们已经知道了所谓的 ReactElement 其实就是一个 JS 对象。那么如果有用户恶意的向服务端数据库中存入了某个有侵入性功能的 伪 React 对象,在实际渲染过程中被当做页面元素渲染,那么将有可能威胁到用户的安全。而 Symbol 是无法在数据库中被存储的,换句话说, React 所渲染的所有元素,都必须是由 JSX 编译的拥有 Symbol 标识的元素。(如果在低版本不支持 Symbol 的浏览器中,将会使用字符串替代,也就没有这层安排保护了)

ok,接下来回到 render 函数。在这个函数中到底发生了什么呢,简单来说就是创建 Root 结构。

7031b9638f4c8a51fc84ba269252f1d5.png

enqueueUpdate

从设计者的角度,根据 单一职责原则开闭口原则 需要有与函数体解耦的数据结构来告诉 React 应该怎么操作 fiber 。而不是初次渲染写一套逻辑,第二次渲染写一套逻辑。因此, fiber 上有了更新队列 UpdateQueue 和 更新链表 Update 结构

如果查看一下相关的定义就会发现,更新队列 updateQueue 是多个更新组成的链表结构,而 update 的更新也是一个链表,至于为什么是这样设计,试想在一个 Class Component 的更新函数中连续执行了 3 次 setState ,与其将其作为 3 个更新挂载到组件上,不如提供一种更小粒度的控制方式。一句话概括就是, setState 级别的小更新合并成一个状态更新,组件中的多个状态更新在组件的更新队列中合并,就能够计算出组件的新状态 newState

对于初次渲染而言,只需要在第一个 fiber 上,挂载一个 update 标识这是一个初次渲染的 fiber 即可。

// 更新根节点
export function ScheduleRootUpdate (
current: Fiber,
element: ReactElement,
expirationTime: number,
suspenseConfig: SuspenseConfig | null,
callback?: Function
) {
// 创建一个update实例
const update = createUpdate(expirationTime, suspenseConfig)
// 对于作用在根节点上的 react element
update.payload = {
element
}

// 将 update 挂载到根 fiber 的 updateQueue 属性上
enqueueUpdate(
current,
update
)

ScheduleWork(
current,
expirationTime
)
}

Fiber

作为整个 Fiber 架构 中最核心的设计, Fiber 被设计成了链表结构。

  • child 指向当前节点的第一个子元素
  • return 指向当前节点的父元素
  • sibling 指向同级的下一个兄弟节点

如果是 React16 之前的树状结构,就需要通过 DFS 深度遍历来查找每一个节点。而现在只需要将指针按照 child → sibling → return 的优先级移动,就可以处理所有的节点

e6407842f5e7946104107ff2d5596d6e.png

这样设计还有一个好处就是在 React 工作的时候只需要使用一个全局变量作为指针在链表中不断移动,如果出现用户输入或其他优先级更高的任务就可以 暂停 当前工作,其他任务结束后只需要根据指针的位置继续向下移动就可以继续之前的工作。指针移动的规律可以归纳为 自顶向下,从左到右 。

康康 fiber 的基本结构

45b2bc6e68780fff987d1b12784c423b.png

其中

  • tag fiber 的类型 ,例如函数组件,类组件,原生组件, Portal 等。
  • type React 元素 类型 详见上方 createElement。
  • alternate 代表双向缓冲对象(看后面)。
  • effectTag 代表这个 fiber 在下一次渲染中将会被如何处理。例如只需要插入,那么这个值中会包含 Placement ,如果需要被删除,那么将会包含 Deletion 。
  • expirationTime 过期时间,过期时间越靠前,就代表这个 fiber 的优先级越高。
  • firstEffectlastEffect 的类型都和 fiber 一样,同样是链表结构,通过 nextEffect 来连接。代表着即将更新的 fiber 状态
  • memorizeStatememorizeProps 代表在上次渲染中组件的 props 和 state 。如果成功更新,那么新的 pendingProps 和 newState 将会替代这两个变量的值
  • ref 引用标识
  • stateNode 代表这个 fiber 节点对应的真实状态
    • 对于原生组件,这个值指向一个 dom 节点(虽然已经被创建了,但不代表就被插入了 document )
    • 对于类组件,这个值指向对应的类实例
    • 对于函数组件,这个值指向 Null
    • 对于 RootFiber,这个值指向 FiberRoot (如图)

接下来是初次渲染的几个核心步骤,因为是初次渲染,核心任务就是将首屏元素渲染到页面上,所以这个过程将会是同步的。

PrepareFreshStack

因为笔者是土货没学过英语,百度了下发现是 准备干净的栈 的意思。结合了下流程,可以看出这一步的作用是在真正工作之前做一些准备,例如初始化一些变量&#x

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值