永利网投正规网页永利网投正规网页

永利皇宫在线手机版客服
永利皇宫在线手机版首页

How React Works (一)首次渲染

How React Works (一)首次渲染

一、前言

  本文将会通过一个简单的例子,结合React源码(v 16.4.2)来说明 React 是如何工作的,并且帮助读者理解 ReactElement、Fiber 之间的关系,以及 Fiber 在各个流程的作用。看完这篇文章有助于帮助你更加容易地读懂 React 源码。初期计划有以下几篇文章:

    首次渲染事件绑定更新流程调度机制

二、核心类型解析

    在正式进入流程讲解之前,先了解一下 React 源码内部的核心类型,有助于帮助我们更好地了解整个流程。为了让大家更加容易理解,后续的描述只抽取核心部分,把 ref、context、异步、调度、异常处理 之类的简化掉了。  

1. ReactElement

  我们写 React 组件的时候,通常会使用JSX来描述组件。<p></p>这种写法经过babel转换后,会变成以 React.createElement(type, props, children)形式。而我们的例子中,type会是两种类型:functionstring,实际上就是Appconstructor方法,以及其他HTML标签。

  而这个方法,最终是会返回一个 ReactElement ,他是一个普通的 Object ,不是通过某个 class 实例化二来的,大概看看即可,核心成员如下:

keytypedesc
$$typeofSymbol|Number对象类型标识,用于判断当前Object是否一个某种类型的ReactElement
typeFunction|String|Symbol|Number|Object如果当前ReactElement是是一个ReactComponent,那这里将是它对应的Constructor;而普通HTML标签,一般都是String
propsObjectReactElement上的所有属性,包含children这个特殊属性

2. ReactRoot

  当前放在ReactDom.js内部,可以理解为React渲染的入口。我们调用ReactDom.render之后,核心就是创建一个 ReactRoot ,然后调用 ReactRoot 实例的render方法,进入渲染流程的。

keytypedesc
renderFunction渲染入口方法
_internalRootFiberRoot根据当前DomContainer创建的一个FiberTree的根

3. FiberRoot

  FiberRoot 是一个 Object ,是后续初始化、更新的核心根对象。核心成员如下:

keytypedesc
current(HostRoot)FiberNode指向当前已经完成的Fiber Tree 的Root
containerInfoDomContainer根据当前DomContainer创建的一个FiberTree的根
finishedWork(HostRoot)FiberNode|null指向当前已经完成准备工作的Fiber Tree Root

current、finishedWork,都是一个(HostRoot)FiberNode,到底是为什么呢?先卖个关子,后面将会讲解。

4. FiberNode

  在 React 16之后,Fiber Reconciler 就作为 React 的默认调度器,核心数据结构就是由FiberNode组成的 Node Tree 。先参观下他的核心成员:

keytypedesc
实例相关------
tagNumberFiberNode的类型,可以在packages/shared/ReactTypeOfWork.js中找到。当前文章 demo 可以看到ClassComponent、HostRoot、HostComponent、HostText这几种
typeFunction|String|Symbol|Number|Object和ReactElement表现一致
stateNodeFiberRoot|DomElement|ReactComponentInstanceFiberNode会通过stateNode绑定一些其他的对象,例如FiberNode对应的Dom、FiberRoot、ReactComponent实例
Fiber遍历流程相关
returnFiberNode|null表示父级 FiberNode
childFiberNode|null表示第一个子 FiberNode
siblingFiberNode|null表示紧紧相邻的下一个兄弟 FiberNode
alternateFiberNode|nullFiber调度算法采取了双缓冲池算法,FiberRoot底下的所有节点,都会在算法过程中,尝试创建自己的“镜像”,后面将会继续讲解
数据相关
pendingPropsObject表示新的props
memoizedPropsObject表示经过所有流程处理后的新props
memoizedStateObject表示经过所有流程处理后的新state
副作用描述相关
updateQueueUpdateQueue更新队列,队列内放着即将要发生的变更状态,详细内容后面再讲解
effectTagNumber16进制的数字,可以理解为通过一个字段标识n个动作,如Placement、Update、Deletion、Callback……所以源码中看到很多 &=
firstEffectFiberNode|null与副作用操作遍历流程相关 当前节点下,第一个需要处理的副作用FiberNode的引用
nextEffectFiberNode|null表示下一个将要处理的副作用FiberNode的引用
lastEffectFiberNode|null表示最后一个将要处理的副作用FiberNode的引用

5. Update

  在调度算法执行过程中,会将需要进行变更的动作以一个Update数据来表示。同一个队列中的Update,会通过next属性串联起来,实际上也就是一个单链表。

keytypedesc
tagNumber当前有0~3,分别是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
payloadFunction|Object表示这个更新对应的数据内容
callbackFunction表示更新后的回调函数,如果这个回调有值,就会在UpdateQueue的副作用链表中挂在当前Update对象
nextUpdateUpdateQueue中的Update之间通过next来串联,表示下一个Update对象

6. UpdateQueue

  在 FiberNode 节点中表示当前节点更新、更新的副作用(主要是Callback)的集合,下面的结构省略了CapturedUpdate部分

keytypedesc
baseStateObject表示更新前的基础状态
firstUpdateUpdate第一个 Update 对象引用,总体是一条单链表
lastUpdateUpdate最后一个 Update 对象引用
firstEffectUpdate第一个包含副作用(Callback)的 Update 对象的引用
lastEffectUpdate最后一个包含副作用(Callback)的 Update 对象的引用

三、代码样例

  本次流程说明,使用下面的源码进行分析

//index.jsimport React from "react";import ReactDOM from "react-dom";import "./index.css";import App from "./App";ReactDOM.render(<App />, document.getElementById("root"));//App.jsimport React, { Component } from "react";import "./App.css";class App extends Component { constructor() { super(); this.state = { msg:"init", }; } render() { return ( <div className="App"> <p className="App-intro"> To get started, edit <code>{this.state.msg}</code> and save to reload. </p> <button onClick={() => { this.setState({msg: "clicked"}); }}>hehe </button> </div> ); }}export default App;

四、渲染调度算法 - 准备阶段

  从ReactDom.render方法开始,正式进入渲染的准备阶段。

1. 初始化基本节点

  创建 ReactRoot、FiberRoot、(HostRoot)FiberNode,建立他们与 DomContainer 的关系。

2. 初始化(HostRoot)FiberNodeUpdateQueue

  通过调用ReactRoot.render,然后进入packages/react-reconciler/src/ReactFiberReconciler.jsupdateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate一系列方法调用,为这次初始化创建一个Update,把<App />这个 ReactElement 作为 Update 的payload.element的值,然后把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。

然后调用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot,期间主要是提取当前应该进行初始化的 (HostFiber)FiberNode,后续正式进入算法执行阶段。

五、渲染调度算法 - 执行阶段

  由于本次是初始化,所以需要调用packages/react-reconciler/src/ReactFiberScheduler.jsrenderRoot方法,生成一棵完整的FiberNode Tree finishedWork

1. 生成 (HostRoot)FiberNode 的workInProgress,即current.alternate

  在整个算法过程中,主要做的事情是遍历 FiberNode 节点。算法中有两个角色,一是表示当前节点原始形态的current节点,另一个是表示基于当前节点进行重新计算的workInProgress/alternate节点。两个对象实例是独立的,相互之前通过alternate属性相互引用。对象的很多属性都是先复制再重建的。

第一次创建结果示意图:

  这个做法的核心思想是双缓池技术(double buffering pooling technique),因为需要做 diff 的话,起码是要有两棵树进行对比。通过这种方式,可以把树的总体数量限制在2,节点、节点属性都是延迟创建的,最大限度地避免内存使用量因算法过程而不断增长。后面的更新流程的文章里,会了解到这个双缓冲怎么玩。

2. 工作执行循环

示意代码如下:

nextUnitOfWork = createWorkInProgress( nextRoot.current, null, nextRenderExpirationTime,);....while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}

刚刚创建的 FiberNode 被作为nextUnitOfWork,从此进入工作循环。从上面的代码可以看出,在是一个典型的递归的循环写法。这样写成循环,一来就是和传统的递归改循环写法一样,避免调用栈不断堆叠以及调用栈溢出等问题;二来在结合其他Scheduler代码的辅助变量,可以实现遍历随时终止、随时恢复的效果。

我们继续深入performUnitOfWork函数,可以看到类似的代码框架:

const current = workInProgress.alternate;//...next = beginWork(current, workInProgress, nextRenderExpirationTime);//...if (next === null) { next = completeUnitOfWork(workInProgress);}//...return next;

从这里可以看出,这里对 workInProgress 节点进行一些处理,然后会通过一定的遍历规则返回next,如果next不为空,就返回进入下一个performUnitOfWork,否则就进入completeUnitOfWork

3. beginWork

  每个工作的对象主要是处理workInProgress。这里通过workInProgress.tag区分出当前 FiberNode 的类型,然后进行对应的更新处理。下面介绍我们例子里面可以遇到的两种处理比较复杂的 FiberNode 类型的处理过程,然后再单独讲解里面比较重要的processUpdateQueue以及reconcileChildren过程。

3.1 HostRoot - updateHostRoot

  HostRoot,即文中经常讲到的 (HostRoot)FiberNode,表示它是一个 HostRoot 类型的 FiberNode ,代码中通过FiberRoot.tag表示。

  前面讲到,在最开始初始化的时候,(HostRoot)FiberNode 在初始化之后,初始化了他的updateQueue,里面放了准备处理的子节点。这里就做两个动作:

处理更新队列,得出新的state - processUpdateQueue方法创建或者更新 FiberNode 的child,得到下一个工作循环的入参(也是FiberNode) - ChildReconciler方法

  通过这两个函数的详细内容属于比较通用的部分,将在后面单独讲解。

3.2 ClassComponent - updateClassComponent

  ClassComponent,即我们在写 React 代码的时候自己写的 Component,即例子中的App

3.2.1 创建ReactComponent实例阶段

  对于尚未初始化的节点,这个方法主要是通过FiberNode.type这个 ReactComponent Constructor 来创建 ReactComponent 实例并创建与 FiberNode 的关系。

(ClassComponent)FiberNode 与 ReactComponent 的关系示意图:

  初始化后,会进入实例的mount过程,即把 Component render之前的周期方法都调用完。期间,state可能会被以下流程修改:

调用getDerivedStateFromProps调用componentWillMount -- deprecated处理因上面的流程产生的Update所调用的processUpdateQueue

3.2.2 完成阶段 - 创建 child FiberNode

  在上面初始化Component实例之后,通过调用实例的render获取子 ReactElement,然后创建对应的所有子 FiberNode 。最终将workInProgress.child指向第一个子 FiberNode。

3.4 处理节点的更新队列 - processUpdateQueue 方法

  在解释流程之前,先回顾一下updateQueue的数据结构:

  从上面的结构可以看出,UpdateQueue 是存放整个 Update 单向链表的容器。里面的 baseState 表示更新前的原始 State,而通过遍历各个 Update 链表后,最终会得到一个新的 baseState。

  对于单个 Update 的处理,主要是根据Update.tag来进行区分处理。

ReplaceState:直接返回这里的 payload。如果 payload 是函数,则使用它的返回值作为新的 State。CaptureUpdate:仅仅是将workInProgress.effectTag设置为清空ShouldCapture标记位,增加DidCapture标记位。UpdateState:如果payload是普通对象,则把他当做新 State。如果 payload 是函数,则把执行函数得到的返回值作为新 State。如果新 State 不为空,则与原来的 State 进行合并,返回一个新对象。ForceUpdate:仅仅是设置 hasForceUpdate为 true,返回原始的 State。

  整体而言,这个方法要做的事情,就是遍历这个 UpdateQueue ,然后计算出最后的新 State,然后存到workInProgress.memoizedState中。

3.5 处理子FiberNode - reconcileChildren 方法

  在 workInProgress 节点自身处理完成之后,会通过props.children或者instance.render方法获取子 ReactElement。子 ReactElement 可能是对象数组字符串迭代器,针对不同的类型进行处理。

下面通过 ClassComponent 及其 数组类型 child的场景来讲解子 FiberNode 的创建、关联流程(reconcileChildrenArray方法):

  在页面初始化阶段,由于没有老节点的存在,流程上就略过了位置索引比对、兄弟元素清理等逻辑,所以这个流程相对简单。

  遍历之前render方法生成的 ReactElement 数组,一一对应地生成 FiberNode。FiberNode 有returnFiber属性和sibling属性,分别指向其父亲 FiberNode和紧邻的下一个兄弟 FiberNode。这个数据结构和后续的遍历过程相关。

  现在,生成的FiberNode Tree 结构如下:

  图中的两个(HostComponent)FiberNode就是刚刚生成的子 FiberNode,即源码中的<p>...</p><button>...</button>。这个方法最后返回的,是第一个子 FiberNode,就通过这种方式创建了(ClassComponent)FiberNode.child与第一个子 FiberNode的关系。

  这个时候,再搬出刚刚曾经看过的代码:

const current = workInProgress.alternate;//...next = beginWork(current, workInProgress, nextRenderExpirationTime);//...if (next === null) { next = completeUnitOfWork(workInProgress);}//...return next;

  意味着刚刚返回的 child 会被当做 next 进入下一个工作循环。如此往复,会得到下面这样的 FiberNode Tree :

  生成这棵树之后,被返回的是左下角的那个 (HostText)FiberNode。而重新进入beginWork方法后,由于这个 FiberNode 并没有 child ,根据上面的代码逻辑,会进入completeUnitOfWork方法。

注意:虽然说本例子的 FiberNode Tree 最终形态是这样子的,但实际上算法是优先深度遍历,到叶子节点之后再遍历紧邻的兄弟节点。如果兄弟节点有子节点,则会继续扩展下去。

4. completeUnitOfWork

  进入这个流程,表明 workInProgress 节点是一个叶子节点,或者它的子节点都已经处理完成了。现在开始要完成这个节点处理的剩余工作。  

4.1 创建DomElement,处理子DomElement 绑定关系

  completeWork方法中,会根据workInProgress.tag来区分出不同的动作,下面挑选2个比较重要的来进一步分析:

4.1.1 HostText

  此前提到过,FiberNode.stateNode可以用于存放 DomElement Instance。在初始化过程中,stateNode 为 null,所以会通过document.createTextNode创建一个 Text DomElement,节点内容就是workInProgress.memoizedProps。最后,通过__reactInternalInstance$[randomKey]属性建立与自己的 FiberNode的联系。

4.1.2 HostComponent

  在本例子中,处理完上面的 HostText 之后,调度算法会寻找当前节点的 sibling 节点进行处理,所以进入了HostComponent的处理流程。

  由于当前出于初始化流程,所以处理比较简单,只是根据FiberNode.tag(当前值是code)来创建一个 DomElement,即通过document.createElement来创建节点。然后通过__reactInternalInstance$[randomKey]属性建立与自己的 FiberNode的联系;通过__reactEventHandlers$[randomKey]来建立与 props 的联系。

  完成 DomElement 自身的创建之后,如果有子节点,则会将子节点 append 到当前节点中。现在先略过这个步骤。

  后续,通过setInitialProperties方法对 DomElement 的属性进行初始化,而<code>节点的内容、样式、class、事件 Handler等等也是这个时候存放进去的。

  现在,整个 FiberNode Tree 如下:

  经过多次循环处理,得出以下的 FiberNode Tree:

  之后,回到红色箭头指向的 (HostComponent)FiberNode,可以分析一下之前省略掉的子节点处理流程。

  在当前 DomElement 创建完毕后,进入appendAllChildren方法把子节点 append 到当前 DomElement 。由上面的流程可以知道,可以通过 workInProgress.child -> workInProgress.child.sibling -> workInProgress.child.sibling.sibling ....找到所有子节点,而每个节点的 stateNode 就是对应的 DomElement,所以通过这种方式的遍历,就可以把所有的 DomElement 挂载到 父 DomElement中。

  最终,和 DomElement 相关的 FiberNode 都被处理完,得出下面的FiberNode 全貌:

4.2 将当前节点的 effect 挂在到 returnFiber 的 effect 末尾

  在前面讲解基础数据结构的时候描述过,每个 FiberNode 上都有 firstEffect、lastEffect ,指向一个Effect(副作用) FiberNode链表。在处理完当前节点,即将返回父节点的时候,把当前的链条挂接到 returnFiber 上。最终,在(HostRoot)FiberNode.firstEffect 上挂载着一条拥有当前 FiberNode Tree 所有副作用的 FiberNode 链表。

5. 执行阶段结束

  经历完之前的所有流程,最终 (HostRoot)FiberNode 也被处理完成,就把 (HostRoot)FiberNode 返回,最终作为finishedWork返回到 performWorkOnRoot,后续进入下一个阶段。

六、渲染调度算法 - 提交阶段

  所谓提交阶段,就是实际执行一些周期函数、Dom 操作的阶段。

  这里也是一个链表的遍历,而遍历的就是之前阶段生成的 effect 链表。在遍历之前,由于初始化的时候,由于 (HostRoot)FiberNode.effectTagCallback(初始化回调)),会先将 finishedWork 放到链表尾部。结构如下:

每个部分提交完成之后,都会把遍历节点重置到finishedWork.firstEffect

1. 提交节点装载( mount )前的操作

  当前这个流程处理的只有属于 ReactComponent 的 getSnapshotBeforeUpdate方法。  

2. 提交端原生节点( Host )的副作用(插入、修改、删除)

  遍历到某个节点后,会根据节点的 effectTag 决定进行什么操作,操作包括插入( Placement )修改( Update )删除( Deletion )

  由于当前是首次渲染,所以会进入插入( Placement )流程,其余流程将在后面的《How React Works(三)更新流程》中讲解。

2.1 插入流程( Placement )

  要做插入操作,必先找到两个要素:父亲 DomElement ,子 DomElement。

2.1.1 找到相对于当前 FiberNode 最近的父亲 DomElement

  通过FiberNode.return不断往上找,找到最近的(HostComponent)FiberNode、(HostRoot)FiberNode、(HostPortal)FiberNode节点,然后通过(HostComponent)FiberNode.stateNode(HostRoot)FiberNode.stateNode.containerInfo(HostPortal)FiberNode.stateNode.containerInfo就可以获取到对应的 DomElement 实例。  

2.1.2 找到相对于当前 FiberNode 最近的所有游离子 DomElement

  实际上,把目标是查找当前 FiberNode底下所有邻近的 (HostComponent)FiberNode、(HostText)FiberNode,然后通过 stateNode 属性就可以获取到待插入的 子DomElement 。

  所谓所有邻近的,可以通过这幅图来理解:

  图中红框部分FiberNode.stateNode,就是要被添加到父亲 DomElement的 子 DomElement。

  遍历顺序,和之前的生成 FiberNode Tree时顺序大致相同:

a) 访问child节点,直至找到 FiberNode.type 为 HostComponent 或者 HostRoot 的节点,获取到对应的 stateNode ,append 到 父 DomElement中。

b) 寻找兄弟节点,如果有,就访问兄弟节点,返回 a) 。

c) 如果没有兄弟节点,则访问 return 节点,如果 return 不是当前算法入参的根节点,就返回a)。

d) 如果 return 到根节点,则退出。

3. 提交装载、变更后的生命周期调用操作

  在这个流程中,也是遍历 effect 链表,对于每种类型的节点,会做不同的处理。

3.1 ClassComponent

  如果当前节点的 effectTag 有 Update 的标志位,则需要执行对应实例的生命周期方法。在初始化阶段,由于当前的 Component 是第一次渲染,所以应该执行componentDidMount,其他情况下应该执行componentDidUpdate

  之前讲到,updateQueue 里面也有 effect 链表。里面存放的就是之前各个 Update 的 callback,通常就来源于setState的第二个参数,或者是ReactDom.rendercallback。在执行完上面的生命周期函数后,就开始遍历这个 effect 链表,把 callback 都执行一次。

3.2 HostRoot

  操作和 ClassComponent 处理的第二部分一致。

3.3 HostComponent

  这部分主要是处理初次加载的 HostComponent 的获取焦点问题,如果组件有autoFocus这个 props ,就会获取焦点。    

七、小结

  本文主要讲述了ReactDom.render的内部的工作流程,描述了 React 初次渲染的内在流程:

    创建基础对象: ReactRoot、FiberRoot、(HostRoot)FiberNode创建 HostRoot 的镜像,通过镜像对象来做初始化初始化过程,通过 ReactElement 引导 FiberNode Tree 的创建父子 FiberNode 通过childreturn连接兄弟 FiberNode 通过sibling连接FiberNode Tree 创建过程,深度优先,到底之后创建兄弟节点一旦到达叶子节点,就开始创建 FiberNode 对应的 实例,例如对应的 DomElement 实例、ReactComponent 实例,并将实例通过FiberNode.stateNode创建关联。如果当前创建的是 ReactComponent 实例,则会调用调用getDerivedStateFromPropscomponentWillMount方法DomElement 创建之后,如果 FiberNode 子节点中有创建好的 DomElement,就马上 append 到新创建的 DomElement 中构建完成整个FiberNode Tree 后,对应的 DomElement Tree 也创建好了,后续进入提交过程在创建 DomElement Tree 的过程中,同时会把当前的副作用不断往上传递,在提交阶段里面,会找到这种标记,并把刚创建完的 DomElement Tree 装载到容器 DomElement中执行对应 ReactComponent 的装载后生命周期方法componentDidMount

 下一篇文章将会描述 React 的事件机制(但据说准备要重构),希望我不会断耕。

写完第一篇,React 版本已经到了 16.5.0 ……

, 1, 0, 9);

欢迎阅读本文章: 苗秀梅

永利皇宫在线正规网页

永利皇宫在线手机版客服