How React Works (一)首次渲染

一、前言   本文将会通过一个简单的例子,结合React源码(v 16.4.2)来说明 React 是如何工作的,并且帮助读者理解 ReactElement、Fiber 之间的关系,以及 Fiber 在各个流程的作用。看完这篇文章有助于帮助你更加容易地读懂 React 源码。初期计划有以下几篇文章: 首次渲染 事件绑定 更新流程 调度机制 二、核心类型解析      在正式进入流程讲解之前,先了解一下 React 源码内部的核心类型,有助于帮助我们更好地了解整个流程。为了让大家更加容易理解,后续的描述只抽取核心部分,把 ref、context、异步、调度、异常处理 之类的简化掉了。    1. ReactElement   我们写 React 组件的时候,通常会使用JSX来描述组件。

这种写法经过babel转换后,会变成以 React.createElement(type, props, children)形式。而我们的例子中,type会是两种类型:function、string,实际上就是App的constructor方法,以及其他HTML标签。   而这个方法,最终是会返回一个 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 ,是后续初始化、更新的核心根对象。核心成员如下: 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 。先参观下他的核心成员: 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部分 key type desc baseState Object 表示更新前的基础状态 firstUpdate Update 第一个 Update 对象引用,总体是一条单链表 lastUpdate Update 最后一个 Update 对象引用 firstEffect Update 第一个包含副作用(Callback)的 Update 对象的引用 lastEffect Update 最后一个包含副作用(Callback)的 Update 对象的引用 三、代码样例   本次流程说明,使用下面的源码进行分析 //index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render(, 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 (

To get started, edit {this.state.msg} and save to reload.

); } } export default App; 四、渲染调度算法 - 准备阶段   从ReactDom.render方法开始,正式进入渲染的准备阶段。 1. 初始化基本节点   创建 ReactRoot、FiberRoot、(HostRoot)FiberNode,建立他们与 DomContainer 的关系。 2. 初始化(HostRoot)FiberNode的UpdateQueue   通过调用ReactRoot.render,然后进入packages/react-reconciler/src/ReactFiberReconciler.js的updateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate一系列方法调用,为这次初始化创建一个Update,把这个 ReactElement 作为 Update 的payload.element的值,然后把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。 然后调用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot,期间主要是提取当前应该进行初始化的 (HostFiber)FiberNode,后续正式进入算法执行阶段。 五、渲染调度算法 - 执行阶段   由于本次是初始化,所以需要调用packages/react-reconciler/src/ReactFiberScheduler.js的renderRoot方法,生成一棵完整的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,即源码中的

...

。这个方法最后返回的,是第一个子 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 的属性进行初始化,而节点的内容、样式、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.effectTag为Callback(初始化回调)),会先将 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.render的 callback。在执行完上面的生命周期函数后,就开始遍历这个 effect 链表,把 callback 都执行一次。 4.2 HostRoot   操作和 ClassComponent 处理的第二部分一致。 4.3 HostComponent   这部分主要是处理初次加载的 HostComponent 的获取焦点问题,如果组件有autoFocus这个 props ,就会获取焦点。       七、小结   本文主要讲述了ReactDom.render的内部的工作流程,描述了 React 初次渲染的内在流程: 创建基础对象: ReactRoot、FiberRoot、(HostRoot)FiberNode 创建 HostRoot 的镜像,通过镜像对象来做初始化 初始化过程,通过 ReactElement 引导 FiberNode Tree 的创建 父子 FiberNode 通过child、return连接 兄弟 FiberNode 通过sibling连接 FiberNode Tree 创建过程,深度优先,到底之后创建兄弟节点 一旦到达叶子节点,就开始创建 FiberNode 对应的 实例,例如对应的 DomElement 实例、ReactComponent 实例,并将实例通过FiberNode.stateNode创建关联。 如果当前创建的是 ReactComponent 实例,则会调用调用getDerivedStateFromProps、componentWillMount方法 DomElement 创建之后,如果 FiberNode 子节点中有创建好的 DomElement,就马上 append 到新创建的 DomElement 中 构建完成整个FiberNode Tree 后,对应的 DomElement Tree 也创建好了,后续进入提交过程 在创建 DomElement Tree 的过程中,同时会把当前的副作用不断往上传递,在提交阶段里面,会找到这种标记,并把刚创建完的 DomElement Tree 装载到容器 DomElement中 双缓冲的两棵树 FiberNode Tree 角色互换,原来的 workInProgress 转正 执行对应 ReactComponent 的装载后生命周期方法componentDidMount 其他回调调用、autoFocus 处理  下一篇文章将会描述 React 的事件机制(但据说准备要重构),希望我不会断耕。https://www.cnblogs.com/lcllao/p/9642376.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信