- Published on
深入React - part 1
- Authors
- Name
- Deng Hua
目录
前情
《深入React》系列内容将由浅入深介绍React的工作原理,讲解中将使用以下Demo,在进入正文前,可以先通览熟悉下Demo。
组件 与 元素
react组件用于描述UI界面,通常是一个javascript
函数,返回一个 React Element
,一个个 React Element
构成了element tree
(元素树), 通常我们使用JSX
语法来编写这些元素,UI由组件产生,本质上可以将组件视为一个"蓝图"或"模板"。
react通过组件创建一个或多个组件实例。
每次我们在代码中的某个地方使用该组件时都会执行此操作,例如示例代码中的<Tab>
组件。
在代码中,<Tab>
组件出现了三次,因此<tab>
的三个实例将被插入到"元素树"中,react会调用<Tab>
函数组件三次,每个组件一次。
实际上,每个实例都拥有自己的state
和props
,也有自己的生命周期。它可以“存活”一段时间,直到最终“死亡”(卸载)。
我们都知道,JSX
语法本质上其实是在编写javascript。JSX
实际上会编译为多个React.createElement
函数来调用,当调用时,将返回一个 React Element
。
React Element
是一个很大的react保留在内存中的不可变javascript对象。包含了创建DOM元素所需的所有信息,最终 React Element
将被"转换"为实际的DOM元素。
最后由浏览器绘制到屏幕上。
React Element
我们可以在JSX
中打印出 React Element
。
console.log(<DifferentContent />);
当在JSX
中看到<component></component>
这样的语法时,react在内部就会调用对应的组件函数,返回一个 React Element
。
我们查看控制面板:
可以看到一些属性,如type
属性的值对应了我们的组件名称ƒ DifferentContent()
。因为没有传递props
所以为空。
我们试着传递一些props
:
console.log(<DifferentContent test={23} />);
现在props
属性也有内容了。
我们看到有一个奇怪的属性: $$typeof
,实际上它是react实现的一个安全功能,为了保护我们免受XSS
跨站脚本攻击,它是一个symbol
类型。
react甚至允许我们这样调用组件函数
function DifferentContent() {
return (
<div className='tab-content'>
<h4>I'm a DIFFERENT tab, so i reset state</h4>
</div>
)
}
console.log(<DifferentContent test={23} />);
console.log(DifferentContent());
查看控制台,会发现也会有两个 React Element
被打印出来。
第二个 React Element
它的type
属性值为div
,不再是函数组件名称。并且props
属性也不一样,包含了className
。
这意味着,react不再将其视为react函数组件,它只是一个原始的 React Element
。
所以,请不要将react组件作为函数直接调用。
React渲染流程概览
通过前两个部分,我们大概知道了react组件是如何渲染到画面的。我们已经知道react组件都会被转化为React.createElement
被调用,从而创建 React Element
。
但是对于 React Element
最终到变为UI界面的过程,还是非常模糊。之后将根据以下几个阶段细分说明。
1. RENDER IS TRIGGERD —— 触发渲染
新的渲染大部分都是通过更新状态触发的(如setState),当状态变化时会进入渲染阶段。
2. RENDER PHASE —— 渲染阶段
在渲染阶段中,react调用组件函数。react需要明白它应该如何更新DOM,来反映最新的变化,而变化信息就在组件中。
但是它不会立即去更改DOM,react对 Render 的定义与传统的 DOM Render 不一样。
在react中,Render 不是更新DOM,或者在屏幕上显示元素。渲染阶段从始至终只发生在react核心内部,目前它不会产生任何浏览器UI上的视觉变化。
当react知道如何更新DOM后,进入提交阶段。
3. COMMIT PHASE —— 提交阶段
在这个阶段中,新元素可能会被放置在DOM中,已经存在的元素可能会被更新或者删除。实际上,这个提交阶段更像是我们传统上所说的渲染的概念所做的事。
4. BROWSER PAINT —— 浏览器绘制
最后,浏览器会注意到DOM已经更新,重新绘制,这里就和react无关了。
现在详细介绍各个阶段。
RENDER IS TRIGGERD —— 触发渲染
只有两种方式可以触发 Render
应用程序首次运行,也就是Initial-Render(初始渲染)
一个或多个组件中发生了状态更新,也就是常说的Re-Render(重渲染)
总结: 渲染过程实际上是被触发的,这是对于整个应用来说,而不是仅仅是某个组件。但这不意味着整个DOM更新,在react的渲染阶段中,只涉及调用组件函数创建React Element
。
Re-Render(重渲染)实际上不是在状态更新之后立即触发的,它被设计为在Javascript引擎的空闲时间运行(requestIdleCallback
)。
RENDER PHASE —— 渲染阶段
这里引入一个新概念: component tree(组件树),React官方称之为渲染树,本质是对函数组件的抽象表达。
注意文档里的: 渲染树仅由 React 组件 组成
在渲染阶段的开始,react将遍历整个组件树,获取所有触发了Re-Render的组件,并调用这些组件函数。
还记得吗,调用组件函数将返回新的 React Element
,这些新的react元素共同组成所谓的Virtual DOM。
Virtual DOM
首次渲染时,react会从根节点开始遍历整个组件树,并挨个调用,最终创建了一个大的React Element Tree
,这就是我们所说的虚拟DOM。
虚拟DOM是一个由所有 React Element
组成的树,来自组件树中的所有组件(调用)。虚拟DOM的创建过程相比传统DOM更快速,内存开销更小。毕竟它只是一个javascript对象。
现在,假设组件D中将有一个状态更新,组件D将触发Re-Render
,此时react将重新调用组件d函数。
并将组件D新的 React Element
插入到在新的React Element Tree
中,最终新的React Element Tree
又会生成新的虚拟DOM。
每当react重新Re-Render
时,对应组件的所有子组件也会被Re-Render
,无论其中的props
有没有变化,如果更新的组件返回一个或多个组件,这些嵌套的组件也将在组件树中重新渲染。
这听起来不合理,因为react不知道组件中的更新如何影响子组件,所以默认情况下,react会重新渲染所有。不过此时不意味整个DOM也更新了,目前还只是一个被重新创建的虚拟DOM。
Fiber 与 Fiber Tree
接下来,这个被创建的虚拟DOM,将与之前的虚拟DOM进行重新比对,这个比对过程在react中,称为(Reconcilication - 协调),其中会用到差异比较算法(Diff)。
在React v16中旧的React架构(Stack Reconciler)被重写后,现在的架构被官方称为 Fiber
。
- 什么是 Fiber ?
Fiber不是计算机术语中的新名词,他的中文翻译叫做纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。
React Fiber可以理解为:React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
这个比对过程的结果,将是一个更新后的Fiber Tree
,最终它会被用于正确的渲染DOM树。
- 什么是 Fiber Tree ?
在React15及之前,是采用堆栈协调(Stack Reconciler)的架构创建虚拟DOM,通过递归调用组件函数来得到要返回的
React Element
,这个过程遇到子组件就得再去调用子组件的函数,从而形成调用栈的结构),这个过程是同步的,如果组件树很深,将会占用主线程很多时间,这会产生相应的一些问题例如造成卡顿页面、用户的事件没有响应……等等。为了解决这个问题,React 想要将这种无法中断的更新重构成非同步可以中断的更新,而曾经依靠的 Virtual DOM 数据结构明显是无法满足这个条件的,于是全新的React Fiber 架构也就随之诞生了。所以通常在 React 16 之后的 Reconciler 也被称为 Fiber Reconciler, 而 Virtual DOM 这个名词 React 官方也有提到说要怕混搞,在 Fiber 架构出现后会尽量避免使用,可以将它看成是 React Fiber 同样要创建一个虚拟的树状结构,但结构跟以前的虚拟 DOM 树的版本已经不一样了,所以通常会命名为 Fiber 树让开发者搞混。
在应用程序的首次运行时,Raect Fiber
将对整个React Element Tree
生成Fiber Tree
。
Fiber Tree
是一种特殊的树结构,和React Element
不同,Fiber Tree
在Re-Render
中不会每次都被重新创建。
Fiber
树永远都不会被销毁,它是一种可变的数据结构,一旦在首次运行中创建了它,之后只是在Reconcilication过程中被更新。
更重要的是,我们在屏幕上看到的任何组件的实际state
和props
,都存储在Fiber Tree
中对应的Fiber Node
内,每个Fiber
单元还将包含一个工作队列,用于更新state,运行副作用,执行DOM更新等。
- 什么是 Fiber Node ?
在协调期间,每个
React Element
的数据都会合并到 Fiber 节点树中。每个React Element
都有一个对应的Fiber Node
。也是Fiber Tree
的组成单元。每个Fiber节点保存了各自组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。也保存了更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。
我们可以看一下
Fiber Node
的属性定义:function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // 作为静态数据结构的属性 this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // 用于连接其他Fiber节点形成Fiber树 this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; // 作为动态的工作单元的属性 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; // 调度优先级相关 this.lanes = NoLanes; this.childLanes = NoLanes; // 指向该fiber在另一次更新时对应的fiber this.alternate = null; }
在这张图中,我们看到Fiber Tree
以一种奇怪的方式的方式排列 React Element
对于每一个子节点,都有一个"link"链接到父节点(App <-> Video
, Modal <-> Overlay <-> H3
)
而所有的子节点也和同级兄弟节点有链接(Video <-> Modal <-> Btn
,H3 <-> button
)。
这种结构就是链表结构。
每个Fiber节点有个对应的React Element
,多个Fiber节点靠如下三个属性连接形成树:
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
关于具体的链接方式,这里先略过,可查看参考。
可参考文章:
An Introduction to React Fiber - The Algorithm Behind React
Fiber 为什么是 React 性能的一个飞跃?/ 链表结构
Fiber的Reconcilication过程
以这段代码为例:
应用中,有一个showModal
的state,当前设置为true
。showModal
的状态会影响<Modal>
的存在与否和<Btn>
的显示文字。
<App>
<Video />
{showModal && (
<Modal>
<Overlay>
<h3>Rate course!</h3>
<button>5 🌟</button>
</Overlay>
</Modal>
)}
<Btn>
{showModal ? 'Rate' : 'Hide'}
</Btn>
</App>
然后我们将showModal
的值更新为false
,这将触发Re-Render
,这会创建一个新的Fiber Tree
,也叫WorkInProgress Tree
。在这个新的Fiber Tree
中,<Modal>
和它的子组件消失了。因为showModal
为false
时,它们不显示。
- 什么是 WorkInProgress Tree ?
workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。
然后,这个新的Fiber Tree
需要与之前的Fiber Tree
进行比较,替换。这个"之前的Fiber Tree
"也有一个新名词,叫CurrentFiber Tree
。
- 什么是 CurrentFiber Tree ?
currentFiber tree 表示上次渲染构建的 Fiber Tree。在每一次更新完成后 workInProgress tree 都会替换之前的 currentFiber tree。
可以理解为之前内存中存在的Fiber Tree
动图就是WorkInProgress Tree
的生成过程:
Fiber
遍历一步一步的遍历整颗树,并分析当前新的Fiber Tree
之间需要更改的地方,基于新的Fibe
来更新Fiber Tree
(替换)。
其中一步一步的比较元素的过程,称为 —— Diffing
<Btn>
组件有一些新的文本需要更新,将会被标记为DOM update。<Modal>
,<H3>
和<button>
组件它们将被标记为DOM delete。
一旦这个比对过程结束,所有的这些变化都会被集中到一个列表内,称为effects list
,将在下一阶段(COMMIT PHASE
提交阶段)被使用。
新 workInProgress tree 的创建过程中,会同 currentFiber tree的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber tree对应的节点对象,减少新创建对象带来的开销。
参考:
COMMIT PHASE —— 提交阶段
提交阶段,这个阶段 React 会遍历更新队列,将其中需要的变更「一次性」更新到 DOM 上(插入,删除,更新DOM元素)。
React通过渲染过程中最后产生的effects list
将它们一个接一个地应用于实际的DOM元素。
提交阶段将是同步执行的,无法异步执行,无法被打断,这是必须的,保证DOM不会只显示部分的最新结果,确保UI界面始终保持一致。
最后一旦提交阶段结束,当前的WorkInProgress Tree
将成为下一个渲染周期的CurrentFiber Tree
,Fiber Tree
自创建后不会被销毁,不会从零开始创建,它们只会不断的比对更新自身。
提交阶段已经结束,此时新的DOM已经绘制完毕,浏览器会在空闲时间时重新绘制UI。
我们都知道,DOM的重新绘制是由浏览器负责的,确切的说是浏览器的渲染进程负责。
而渲染阶段是由React负责,涉及到状态更新,Diffing,Fiber树等操作。那么提交阶段是不是也是由React负责呢?
实际上并不是。负责提交阶段的实际上是一个单独的库 —— ReactDOM
。
React本身不会接触DOM,它根本不知道渲染阶段的结果以及实际提交和需要绘制的地方。React被设计成独立于平台使用,因此React可以与不同的平台结合使用。如React Native,Remoetion等。
参考:
React Fiber很难?六个问题助你理解 React Fiber
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
In-depth explanation of state and props update in React
The how and why on React’s usage of linked list in Fiber to walk the component’s tree