How React Works (一)首次渲染

How React Works (一)首次渲染

1、前言

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

  1. 首次渲染
  2. 事件机制
  3. 更新流程
  4. 调度机制

2、核心类型解析

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

1. ReactElement

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

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

key type desc
$$typeof Symbol|Number 对象类型标识,用于判断当前Object是否一个某种类型的ReactElement
type Function|String|Symbol|Number|Object 若是当前ReactElement是是一个ReactComponent,那这里将是它对应的Constructor;而普通HTML标签,通常都是String
props Object ReactElement上的全部属性,包含children这个特殊属性

2. ReactRoot

  当前放在ReactDom.js内部,能够理解为React渲染的入口。咱们调用ReactDom.render以后,核心就是建立一个 ReactRoot ,而后调用 ReactRoot 实例的render方法,进入渲染流程的。数组

key type desc
render Function 渲染入口方法
_internalRoot FiberRoot 根据当前DomContainer建立的一个FiberTree的根

3. FiberRoot

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

key type desc
current (HostRoot)FiberNode 指向当前已经完成的Fiber Tree 的Root
containerInfo DomContainer 根据当前DomContainer建立的一个FiberTree的根
finishedWork (HostRoot)FiberNode|null 指向当前已经完成准备工做的Fiber Tree Root

current、finishedWork,都是一个(HostRoot)FiberNode,究竟是为何呢?先卖个关子,后面将会讲解。数据结构

4. FiberNode

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

key type desc
实例相关 --- ---
tag Number FiberNode的类型,能够在packages/shared/ReactTypeOfWork.js中找到。当前文章 demo 能够看到ClassComponent、HostRoot、HostComponent、HostText这几种
type Function|String|Symbol|Number|Object 和ReactElement表现一致
stateNode FiberRoot|DomElement|ReactComponentInstance FiberNode会经过stateNode绑定一些其余的对象,例如FiberNode对应的Dom、FiberRoot、ReactComponent实例
Fiber遍历流程相关
return FiberNode|null 表示父级 FiberNode
child FiberNode|null 表示第一个子 FiberNode
sibling FiberNode|null 表示牢牢相邻的下一个兄弟 FiberNode
alternate FiberNode|null Fiber调度算法采起了双缓冲池算法,FiberRoot底下的全部节点,都会在算法过程当中,尝试建立本身的“镜像”,后面将会继续讲解
数据相关
pendingProps Object 表示新的props
memoizedProps Object 表示通过全部流程处理后的新props
memoizedState Object 表示通过全部流程处理后的新state
反作用描述相关
updateQueue UpdateQueue 更新队列,队列内放着即将要发生的变动状态,详细内容后面再讲解
effectTag Number 16进制的数字,能够理解为经过一个字段标识n个动做,如Placement、Update、Deletion、Callback……因此源码中看到不少 &=
firstEffect FiberNode|null 与反作用操做遍历流程相关 当前节点下,第一个须要处理的反作用FiberNode的引用
nextEffect FiberNode|null 表示下一个将要处理的反作用FiberNode的引用
lastEffect FiberNode|null 表示最后一个将要处理的反作用FiberNode的引用

5. Update

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

key type desc
tag Number 当前有0~3,分别是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
payload Function|Object 表示这个更新对应的数据内容
callback Function 表示更新后的回调函数,若是这个回调有值,就会在UpdateQueue的反作用链表中挂在当前Update对象
next Update UpdateQueue中的Update之间经过next来串联,表示下一个Update对象

6. UpdateQueue

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

key type desc
baseState Object 表示更新前的基础状态
firstUpdate Update 第一个 Update 对象引用,整体是一条单链表
lastUpdate Update 最后一个 Update 对象引用
firstEffect Update 第一个包含反作用(Callback)的 Update 对象的引用
lastEffect Update 最后一个包含反作用(Callback)的 Update 对象的引用

3、代码样例

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

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));



//App.js
import 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;

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

  从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,后续正式进入算法执行阶段。

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

  因为本次是初始化,因此须要调用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,后续进入下一个阶段。

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

  所谓提交阶段,就是实际执行一些周期函数、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. 改变 workInProgress/alternate/finishedWork 的身份

  虽然是短短的一行代码,但这个十分重要,因此单独标记:

root.current = finishedWork;

  这意味着,在 DomElement 反作用处理完毕以后,意味着以前讲的缓冲树已经完成任务,翻身当主人,成为下次修改过程的current。再来看一个全貌:

4. 提交装载、变动后的生命周期调用操做

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

4.1 ClassComponent

  若是当前节点的 effectTag 有 Update 的标志位,则须要执行对应实例的生命周期方法。在初始化阶段,因为当前的 Component 是第一次渲染,因此应该执行componentDidMount,其余状况下应该执行componentDidUpdate

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

4.2 HostRoot

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

4.3 HostComponent

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

7、小结

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

  1. 建立基础对象: ReactRoot、FiberRoot、(HostRoot)FiberNode
  2. 建立 HostRoot 的镜像,经过镜像对象来作初始化
  3. 初始化过程,经过 ReactElement 引导 FiberNode Tree 的建立
  4. 父子 FiberNode 经过childreturn链接
  5. 兄弟 FiberNode 经过sibling链接
  6. FiberNode Tree 建立过程,深度优先,到底以后建立兄弟节点
  7. 一旦到达叶子节点,就开始建立 FiberNode 对应的 实例,例如对应的 DomElement 实例、ReactComponent 实例,并将实例经过FiberNode.stateNode建立关联。
  8. 若是当前建立的是 ReactComponent 实例,则会调用调用getDerivedStateFromPropscomponentWillMount方法
  9. DomElement 建立以后,若是 FiberNode 子节点中有建立好的 DomElement,就立刻 append 到新建立的 DomElement 中
  10. 构建完成整个FiberNode Tree 后,对应的 DomElement Tree 也建立好了,后续进入提交过程
  11. 在建立 DomElement Tree 的过程当中,同时会把当前的反作用不断往上传递,在提交阶段里面,会找到这种标记,并把刚建立完的 DomElement Tree 装载到容器 DomElement中
  12. 双缓冲的两棵树 FiberNode Tree 角色互换,原来的 workInProgress 转正
  13. 执行对应 ReactComponent 的装载后生命周期方法componentDidMount
  14. 其余回调调用、autoFocus 处理

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

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

相关文章
相关标签/搜索