React@16.8.6原理浅析(源码浅析)

3,108 阅读32分钟

本系列文章总共三篇:

课前小问题

  1. 为什么有时连续多次 setState只有一次生效?
  2. 执行完setState获取state的值能获取到吗?
  3. setState是同步的还是异步的?
  4. 有些属性为什么无法从props里面获取到(如 ref)?
  5. 受控表单组件如果设置了value就无法输入内容是什么原因?
  6. 为何 react 的事件对象无法保留?

目录结构

顶层目录

React 采用 monorepo 的管理方式。仓库中包含多个独立的包,以便于更改可以一起联调,并且问题只会出现在同一地方。

  • packages 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)。如果你需要修改源代码, 那么每个包的 src 子目录是你最需要花费精力的地方。
  • fixtures 包含一些给贡献者准备的小型 React 测试项目。
  • build 是 React 的输出目录。源码仓库中并没有这个目录,但是它会在你克隆 React 并且第一次构建它之后出现。
  • 还有一些其他的顶层目录,但是它们几乎都是工具类的,并且在贡献代码时基本不会涉及。

20191230134804.png

packages

  • react、react-dom 就不说了
  • react-reconciler 是 16.x 中新实现的 fiber reconciler 的核心代码
  • scheduler 是 react 调度模块的核心代码,之前是放在 react-reconciler 中的,后来独立了出来
  • events 是和 react 事件相关的代码
  • shared 是不同 packages 公用的一些代码
  • 其它 packages 我们这里不做探讨

QQ截图20191230204140.png

核心模块

React “Core” 中包含所有全局 React API,比如:

  • React.createElement()
  • React.Component
  • React.Children

React 核心只包含定义组件必要的 API。它不包含协调算法或者其他平台特定的代码。它同时适用于 React DOM 和 React Native 组件。React 核心代码在源码的 packages/react 目录中。在 npm 上发布为 react 包。相应的独立浏览器构建版本称为 react.js,它会导出一个称为 React 的全局对象。

渲染器

React 最初只是服务于 DOM,但是这之后被改编成也能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。
渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。
渲染器同样位于 packages/ 目录下:

reconciler

即便 React DOM 和 React Native 渲染器的区别很大,但也需要共享一些逻辑。特别是协调算法需要尽可能相似,这样可以让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工作一致。
为了解决这个问题,不同的渲染器彼此共享一些代码。我们称 React 的这一部分为 “reconciler”。当处理类似于 setState() 这样的更新时,reconciler 会调用树中组件上的 render(),然后决定是否进行挂载,更新或是卸载操作。
Reconciler 没有单独的包,因为他们暂时没有公共 API。相反,它们被如 React DOM 和 React Native 的渲染器排除在外。
这部分源码在 /packages/react-reconciler

scheduler

在上一篇中我说在 react 中从产生更新到最终操作DOM这之间可以叫做 reconciliation(协调)的过程,其实这中间还可以再进行细分,其中产生的更新会放在一个更新队列里,如何调度这些更新让它们进行下一步任务这个部分叫做 scheduler,而 react 采用叫做 Cooperative Scheduling (合作式调度)的方式来调度任务,简单来说就是充分利用浏览器的空闲时间来执行任务,有空闲时间就执行对应的任务,没有就把执行权交给浏览器,在浏览器中就是通过 requestIdleCallback 这个 API 来实现的,但是因为这个 API 存在的一些问题以及浏览器的兼容性问题,所以 react 通过 requestAnimationFrame、setTimeout 和 MessageChannel 来模拟了 requestIdleCallback 的行为。现在 react 把这部分代码单独拎出来作为一个 package。
这部分源码在 /packages/scheduler 中。

事件系统

react 自己实现了一套事件系统,和原生的 DOM 事件系统相比减少了内存消耗,抹平了浏览器差异,那么 react 是如何做到的呢,主要是采用了以下策略:

  • 采用事件委托的方式将事件都注册在 document 对象上
  • react 内部创建了一个自己的事件对象 SyntheticEvent (合成事件),将原生事件进行了封装,我们在 react 中操纵的实际上是这个封装的对象
  • react 内部通过对象池的形式来创建和销毁事件对象

这部分的源码在 /packages/events 中。

内置对象

FiberRoot

FiberRoot 是整个应用的入口对象,它是一个 javascript 对象,内部记录了很多和应用更新相关的全局信息,比如要挂载的 container。

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  // 当前应用对应的Fiber对象,是Root Fiber
  this.current = null;
  // root节点,render方法接收的第二个参数
  this.containerInfo = containerInfo;
  this.pendingChildren = null;
  this.pingCache = null;
  // finishedWork 对应的过期时间
  this.finishedExpirationTime = NoWork;
  // 完成 reconciliation 阶段的 RootFiber 对象,接下来要进入 commit 阶段
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.firstBatch = null;
  this.callbackNode = null;
  this.callbackExpirationTime = NoWork;
  this.firstPendingTime = NoWork;
  this.lastPendingTime = NoWork;
  this.pingTime = NoWork;

  if (enableSchedulerTracing) {
    this.interactionThreadID = unstable_getThreadID();
    this.memoizedInteractions = new Set();
    this.pendingInteractionMap = new Map();
  }
}

Fiber

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  // 标记不同的组件类型
  this.tag = tag;
  // ReactElement里面的key
  this.key = key;
  // ReactElement.type,也就是我们调用`createElement`的第一个参数
  this.elementType = null;
  // 异步组件resolved之后返回的内容,一般是`function`或者`class`
  this.type = null;
  // 跟当前Fiber对象对应的那个 element(DOM、class实例等)
  this.stateNode = null;

  // Fiber
  // 指向 parent fiber
  this.return = null;
  // 指向第一个 child
  this.child = null;
  // 指向兄弟节点
  this.sibling = null;
  // 数组中节点的索引,在 diff 算法中进行比对
  this.index = 0;

  this.ref = null;

  // 新的变动带来的新的props
  this.pendingProps = pendingProps;
  // 上一次渲染完成之后的props
  this.memoizedProps = null;
  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  this.updateQueue = null;
  // 上一次渲染的时候的state
  this.memoizedState = null;
  this.contextDependencies = null;

  this.mode = mode;

  // Effects
  // 用来记录自身的 Effect
  this.effectTag = NoEffect;
  // 单链表用来快速查找下一个side effect
  this.nextEffect = null;

  // 子树中第一个side effect
  this.firstEffect = null;
  // 子树中最后一个side effect
  this.lastEffect = null;

  // 代表任务在未来的哪个时间点应该被完成
  this.expirationTime = NoWork;
  // 子树中的最早过期时间
  this.childExpirationTime = NoWork;

  // 和它对应的 Fiber 对象
  // current <=> workInProgress
  this.alternate = null;
}

ExpirationTime

在上一篇中我们说到为了将任务排出优先级 react 最开始只是定死了几个 Priority(优先级)变量,但是这样会出现饥饿问题,低优先级的任务可能一直被打断,后来 react 引入了 expirationTime(过期时间)的概念,这样即使是低优先级的任务只要过期时间一到也能强制立即执行,那么 expirationTime 是如何计算出来的呢,可以参考如下的过程:

在线地址:www.processon.com/view/link/5…

如果没有看懂也没有关系,我这里计算了几种情况下的 expirationTime,你可以找找规律:

image.png

我们可以发现对于同步任务比如 ReactDOM.render 来说,expirationTime 就是很大的整数(32位系统中的最大整数),如果是低优先级的异步任务那么计算出来的时间以 25 为基数进行增长,而如果是高优先级的异步任务(比如用户交互)计算出来的时间是一 10 为基数进行增长,且相同的 currentTime 高优先级的 expirationTime 要大于低优先级的 expirationTime,react 这么做的目的:一是让 25/10 ms 以内触发的更新能有相同的过期时间,这样就可以批量更新以提升性能;二是让高优先级的任务过期时间大于低优先级以提高它的优先级。

核心 API

createElement

React.createElement(
  type,
  [props],
  [...children]
)

createElement 是 react 中创建一个 element 的方法,它可以创建一个指定类型的元素,类型参数可以是元素 DOM 标签字符串,或是一个 react component 类型(类或函数)或是 Fragment 类型。

源码

createElement 源码位于 /react/src/ReactElement.js 中

export function createElement(type, config, children) {
	// 初始化变量
  const props = {};
  // ...

  // 步骤一:初始化属性
  // 将 config 上面定义的属性定义到 props 上
  // 注意:排除了 RESERVED_PROPS 里面的属性名(key,ref等)
  if (config != null) {
    // ...
  }

  // 步骤二:将 children 挂载到 props.children 上
  // 如果是多个 children 就将其转换为数组
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    // ...
    props.children = childArray;
  }

  // 步骤三:解析 defaultProps
  if (type && type.defaultProps) {
    // ...
  }
  
  // 步骤四:将处理好的变量传给 ReactElement 构造函数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
const ReactElement = function (type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    // 这个标签允许我们唯一地将其标识为一个React元素
    ?typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  return element;
};

本节解决的问题

  1. 有些属性为什么无法从props里面获取到(如 ref)

ReactDOM.render

ReactDom.render 的源代码位于 /react-dom/src/client/ReactDOM.js

整体流程

在线地址:www.processon.com/view/link/5…

前期准备阶段

前期准备阶段所做的事情概括起来就三点:

  1. 创建 fiberRoot 和 rootFiber 对象
  2. 计算 expirationTime
  3. 创建 update 并将更新放入队列中

在线地址:www.processon.com/view/link/5…

schedule

见下方

render

见下方

commit

见下方

setState

整体流程

在线地址:www.processon.com/view/link/5…

前期准备阶段

前期准备阶段所做的事情概括起来就三点:

  1. 计算 expirationTime
  2. 创建 update 并将更新放入队列中

schedule

需要注意的是如果是通过 react element 上绑定的事件函数里面调用的 setState 方法,会在执行 setState 方法之前设置 workPhase = BatchedEventPhase;,所以在 scheduleUpdateOnFiber 方法中会进入下图的分支。

image.png

并且在 setState 执行完之后才会调用 flushSyncCallbackQueue 执行更新,此时采用调用 renderRoot
image.png

而如果不是通过事件机制调用的 setState 会立即执行 flushSyncCallbackQueue,就会立即 renderRoot
image.png

详情见下方

render

见下方

commit

见下方

本节解决的问题

  1. 为什么有时连续多次 setState只有一次生效?
  2. 执行完setState获取state的值能获取到吗?
  3. setState是同步的还是异步的?

Schedule

概述

找到触发更新节点对应的 fiberRoot 节点,然后调对该节点的更新,分为两种情况:同步和异步,同步又可以分为两种:是否是 LegacyUnbatchedPhase,如果是就不需要调度直接进入下一阶段(render phase),如果不是就放到下一帧立即执行,对于异步任务则需要根据优先级算出一个过期时间,然后再和队列里排队的任务进行比较找出马上要过期的那个任务在下一帧进入下一个阶段执行(render phase)。

整体流程

在线地址:www.processon.com/view/link/5…

核心方法

scheduleWork

  • 判断嵌套更新,超过 50 次的嵌套更新就报错
  • 找到 fiberRoot 对象并设置 expirationTime
  • 判断是否有高优先级的任务打断当前任务
  • 根据当前 expirationTime 是否等于 Sync 分为两个大的阶段假设我们就把它们叫做同步阶段和异步阶段
    • 同步阶段又可以分为两种情况
      • workPhase 等于 LegacyUnbatchedPhase 时调用 renderRoot
      • 其它 workPhase 调用 scheduleCallbackForRoot,并且当 workPhase 为 NotWorking 时调用 flushSyncCallbackQueue
    • 异步阶段通过 getCurrentPriorityLevel 获取 priorityLevel,然后调用 scheduleCallbackForRoot

流程图:React@16.8.6原理解读——调度(一)

scheduleCallbackForRoot

  • 判断当前 root.callbackNode 是否比新传入的任务优先级低,如果优先级低那么取消那个任务
  • 如果新任务的 expirationTime 是 Sync 就调用 scheduleSyncCallback
  • 如果新任务的 expirationTime 不是 Sync 就计算出还剩多长时间任务过期(timeout)然后调用 scheduleCallback

流程图:React@16.8.6源码解析——调度(二)

scheduleSyncCallback

将传入的 callback 放入 syncQueue 中,然后调用 Scheduler_scheduleCallback 设置优先级为 Scheduler_ImmediatePriority,callback 为 flushSyncCallbackQueueImpl
流程图:React@16.8.6源码解析——调度(二)

scheduleCallback

将传入的 reactPriorityLevel 转换为 schedule 中的 priorityLevel 然后调用 Scheduler_scheduleCallback
流程图:React@16.8.6源码解析——调度(二)

Scheduler_scheduleCallback(unstable_scheduleCallback)

根据传入的 priorityLevel 和 timeout 计算出新的 expirationTime,根据新的 expirationTime 和传入的 callback 创建一个 newNode,然后看看当前第一个等待调度的任务(firstCallbackNode)是否是空,如果是空就把 newNode 作为 firstCallbackNode 然后调用 scheduleHostCallbackIfNeeded,否则就比较 newNode 的过期时间是否是当前列表中最早的,如果是也把它设置为 firstCallbackNode 然后执行 scheduleHostCallbackIfNeeded
流程图:React@16.8.6源码解读——调度(三)

scheduleHostCallbackIfNeeded

如果 firstCallbackNode 不为空就执行 requestHostCallback(flushWork, expirationTime);
流程图:React@16.8.6源码解读——调度(三)

requestHostCallback

设置 scheduledHostCallback 为传入的 callback 如果当前有一个 callback 正在执行或过期时间小于 0 则立即调用 port.postMessage 表示立即执行 scheduledHostCallback 并传入是否超时(didTimeout)否则调用 requestAnimationFrameWithTimeout(animationTick);
流程图:React@16.8.6源码解读——调度(四)

requestAnimationFrameWithTimeout

模拟 requestAnimationFrame 在下一帧执行传入的 callback,因为 requestAnimationFrame 在页签是后台运行时不执行,所以又通过 setTimeout 设置了一个了一个定时器来解决这个问题,如果 requestAnimationFrame 生效了就取消定时器,反之亦然。
流程图:React@16.8.6源码解读——调度(四)

animationTick

如果 scheduledHostCallback 不为空就接着调用 requestAnimationFrameWithTimeout 安排下一帧的任务 否则就是没有等待的任务了就退出,计算出下一帧运行的时间(nextFrameTime),如果小于 8ms 就设置为 8ms ,接着计算出当前帧的过期时间(frameDeadline)如果有任务就接着调用 port.postMessage。
流程图:React@16.8.6源码解读——调度(四)

port.onmessage

port.postMessage 后就可以被 port.onmessage 接收到,收到之后判断当前帧是否还有剩余时间,如果没有检查下要执行的任务(scheduledHostCallback)是否超时,超时就设置 didTimeout = true 没超时就接着调用 requestAnimationFrameWithTimeout,然后退出;如果剩余时间还有就执行 scheduledHostCallback(didTimeout)。
流程图:React@16.8.6源码解读——调度(四)

flushWork

它接受的参数就是 port.onmessage 传入的 didTimeout,如果 didTimeout 为真(说明当前帧没有时间了)判断第一个要执行的任务 (firstCallbackNode)的 expirationTime 是否小于当前时间,小于的话就不断执行 flushFirstCallback 直到 firstCallbackNode 为空或 firstCallbackNode.expirationTime 大于等于当前时间;如果 didTimeout 为假(说明当前帧还有时间)那就不断执行 flushFirstCallback 直到 firstCallbackNode 为空或当前帧已经没有剩余时间了,最后无论是上面何种情况都会再执行 scheduleHostCallbackIfNeeded 判断一下是否还有需要执行的任务。
流程图:React@16.8.6源码解读——调度(六)

flushFirstCallback

执行链表里的第一个任务(firstCallbackNode)并传入是否超时(didUserCallbackTimeout),这里 ImmediatePriority 也会当做超时,firstCallbackNode 可能会再返回一个 callback,将新的回调函数插入到列表中,根据它的到期时间排序,如果新的回调是列表中优先级最高的就调用 scheduleHostCallbackIfNeeded 安排下一次执行。
流程图:React@16.8.6源码解析——调度(五)

flushSyncCallbackQueueImpl

对于 scheduleSyncCallback 来说最终执行的** **scheduledHostCallback 就是 flushSyncCallbackQueueImpl
这个方法中就是循环执行 syncQueue 数组中的任务。
流程图:React@16.8.6源码解析——调度(五)

flushSyncCallbackQueue

还记得最开始如果处于同步阶段并且 workPhase 为 NotWorking 时执行完 scheduleCallbackForRoot 就会调用这个方法,这个方法首先去调用 Scheduler_cancelCallback 取消 immediateQueueCallbackNode,接着会执行 flushSyncCallbackQueueImpl 也就是上面那个方法,immediateQueueCallbackNode 的 callback 对应的就是 flushSyncCallbackQueueImpl,所以我认为这个方法就是立即调用 flushSyncCallbackQueueImpl 去执行 syncQueue 中的回调任务而不是等待下一帧执行。

Render(reconciliation)

概述

从 rootFiber 开始循环遍历 fiber 树的各个节点,对于每个节点会根据节点类型调用不同的更新方法,比如对于 class 组件会创建实例对象,调用 updateQueue 计算出新的 state,执行生命周期函数等,再比如对于 HostComponent 会给它的 children 创建 fiber 对象,当一侧子树遍历完成之后会开始执行完成操作,即创建对应 dom 节点并添加到父节点下以及设置父节点的 effect 链,然后遍历兄弟节点对兄弟节点也执行上述的更新操作,就这样将整棵树更新完成之后就可以进入下一阶段(commit phase)。

整体流程

在线地址:www.processon.com/view/link/5…

核心方法

renderRoot

renderRoot 是整个 render phase 的核心方法,是整个阶段的入口方法。
进入方法后,首先会判断如果新的 fiberRoot 和 之前正在处理的 fiberRoot(workInProgressRoot)不一致或当前的 expirationTime 并不等于正在执行渲染的任务的 expirationTime(renderExpirationTime),那么就执行 prepareFreshStack,如果 isSync 为真( expirationTime === Sync 或 priorityLevel === ImmediatePriority)并且 是异步任务并且还过期了,那么立即执行 renderRoot,传入的 expirationTime 是当前时间;如果 isSync 不为真说明当前任务没有超时,那么设置 currentEventTime = NoWork; 这样下一次请求 currentTime 时就可以得到一个新的时间。
接下来就进入到重头戏,如果 isSync 为真就调用 workLoopSync 否则调用 workLoop,这两个方法我们会在下面单独讲解,这里先暂且不表,接下来就如果两个方法中有报错就执行异常处理,没有报错判断 workInProgress 是否还有,如果还有说明还有任务要做就接着执行 renderRoot,如果任务顺利完成就进入到下一阶段也就是 commit 阶段,调用 commitRoot。
流程图:React@16.8.6源码浅析——渲染阶段(renderRoot)

prepareFreshStack

这个方法是任务开始之前的一些准备工作,之前一直好奇 workInProgress 是那里初始化的,其实就是在这里,这里会调用 createWorkInProgress 根据 rootFiber 拷贝出一个 workInProgress 的 fiber 对象来,接着还会设置一些其它全局变量。

workLoopSync

workLoopSync 比较简单内部循环调用 performUnitOfWork,判断条件是 performUnitOfWork 的返回值 workInProgress 是否为空。

workLoop

workLoop 和 workLoopSync 比较类似,区别就是循环的终止条件新增了 shouldYield,shouleYield 方法判断当前是否应该被打断,如果当前任务没有超时并且任务的时间片已经不够用了就会被打断,这时候 workLoop 循环就会终止。

prerformUnitOfWork

performUnitOfWork 是 workLoopSync 和 workLoop 两个方法都会调用的方法,在其内部会调用 beginWork 方法,beginWork 方法会返回下一个要执行的任务(next),如果 next 为空表示已经遍历到叶子节点了,则调用 completeUnitOfWork 可以执行完成逻辑了,关于这块的执行细节可以参考上一篇。

beginWork

beiginWork 方法接收之前完成的 fiber 节点(current),正在执行的 fiber 节点(workInProgress)和当前的 expirationTime。
如果是初次渲染,设置 didReceiveUpdate 为 false。
如果并非初次渲染,判断是否 props 有变化或 context 有变化,如果有变化则将 didReceiveUpdate 设置为 true,否则判断 workInProgress 的 expirationTime 是否小于传入的 expirationTime(renderExpirationTime),小于的话设置 didReceiveUpdate 为 false,并且根据 workInProgress 的类型对不同类型的元素做了一些处理就退出了,因为当前当前任务没有需要执行的更新。
接着对于初次渲染或有更新的情况,我们再次根据 workInProgress 的类型去调用不同类型元素的更新方法,比如对于 ClassComponent 会调用 updateClassComponent,该方法会返回下一个要执行 performUnitOfWork 的节点,也就是它的子节点。
流程图:React@16.8.6源码浅析——渲染阶段(beginWork)

completeUnitOfWork

当 beginWork 返回的节点(next)为空时,就会调用 completeUnitOfWork,说明已经将遍历到该组件的叶子了,接下来会向上找到父节点执行完成操作,然后遍历兄弟节点,整个遍历的流程可以参考前一篇的内容。
已进入该方法首先会设置 workInProgress 为传入的节点,然后进入一个循环,首先判断一下 workInProgress 是否被标记了 Incomplete(有异常),如果没有异常就执行 completeWork,如果 completeWork 返回的结果(next)不为空,就会直接 return next 让 performUnitOfWork 去处理,如果 next 为空就给父节点(returnFiber)挂载 effect,将当前节点和其子树的 effect 挂载到父节点上;如果有异常会执行 unwindWork 方法,unwindWork 也返回一个节点(next),如果 next 不为空就直接 return 交给 performUnitOfWork 去处理,然后清空其父节点的 effect 链只标记一个 Incomplete effect。
接着判断是否有兄弟节点(siblingFiber),如果有就返回让 performUnitOfWork 去执行,否则设置 workInProgress 为父节点(returnFiber)继续执行循环,知道 workInProgress 为空,此时说明已经遍历到根节点了,标记 workInProgressRootExitStatus = RootCompleted 说明根节点已经完成了,接下来就要可以进入 commit 阶段了,最后返回 null。
流程图:React@16.8.6源码浅析——渲染阶段(completeUnitOfWork)

completeWork

这个方法内部就是一个 switch 根据 workInProgress.tag 对不同类型的节点执行不同的方法,其中最常用的就是对 HostComponent 的操作,对于 HostComponent 如果是初次挂载会通过 createInstance 方法创建 dom 节点,然后通过 appendAllChildren 方法将创建好的 dom 节点挂载到父节点上,然后会调用 finalizeInitialChildren 方法跟 dom 节点绑定事件,将属性设置到对应的 dom 节点上(比如 style),然后判断如果是表单元素是否设置了 autoFocus 如果设置了就给 workInProgress 标记 update;对于 HostText 也是先判断是否是初次挂载如果是就通过 createTextInstance 创建 text 节点并赋值给 workInProgress.stateNode 如果是更新流程就调用 updateHostText 给 workInProgress 标记一个 update。
流程图:React@16.8.6源码浅析——渲染阶段(completeWork)
**

Commit

概述

提交阶段主要做的事情就是对 render 阶段产生的 effect 进行处理,处理分为三个阶段

  • 阶段一:在 dom 操作产生之前,这里主要是调用 getSnapshotBeforeUpdate 这个生命周期方法
  • 阶段二:处理节点的增删改,对于删除操作需要做特殊处理要同步删除它的子节点并且调用对应的生命周期函数
  • 阶段三:dom 操作完成之后还需要调用对应的生命周期函数,并且执行 updateQueue 中的 callback

整体流程

在线地址:www.processon.com/view/link/5…

核心方法

commitRoot

commitRoot 接受 fiberRoot 对象,然后调用 commitRootImpl 方法并把 fiberRoot 对象传递给它,如果执行的过程中有 Passive effect 产生就会调用 flushPassiveEffects 去执行这些 effect,Passive effect 和 hooks 有关,这里暂且不表。
流程图:React@16.8.6源码浅析——提交阶段(commitRoot)

commitRootImpl

首先根据传入的 fiberRoot 获取到 finishedWork 也就是之前阶段完成的 rootFiber,然后重置一些变量,接着处理 effect 链,如果 rootFiber 也有 effect,那也需要加到 effect 链上,接着通过三个循环来分别处理这些 effect:

  • 第一个循环对每一个 effect 调用了 commitBeforeMutationEffects 方法
  • 第二个循环对每一个 effect 调用了 commitMutationEffects 方法
  • 第三个循环对每一个 effect 调用了 commitLayoutEffects 方法

最后调用 flushSyncCallbackQueue 如果 syncQueue 还有其它任务则执行它们
流程图:React@16.8.6源码浅析——提交阶段(commitRoot)

commitBeforeMutationEffects

该方法会判断 effect 上是否有 Snapshot,Snapshot 会在 render 阶段判断 class 组件是否有 getSnapshotBeforeUpdate 这个生命周期时加上,如果有就调用 commitBeforeMutationEffectOnFiber,在这个方法里会判断 fiber 的类型,如果是 function 组件会调用 commitHookEffectList (这里我不太明白为什么 function 组件会有 Snapshot),如果是 class 组件就会执行 getSnapshotBeforeUpdate 这个方法并将返回的结果设置到实例的 __reactInternalSnapshotBeforeUpdate 属性上,这个再 componentDidUpdate 的时候会用到。
流程图:React@16.8.6源码浅析——提交阶段(commitBeforeMutationEffects)

commitMutationEffects

在该方法中会对一些和 dom 操作相关的 effect 进行执行:

  • ContentReset:表示重置节点中的文本
  • Ref:之前设置的 ref 要解除关联
  • Placement:找到其兄弟节点和父节点,如果父节点是 dom 节点那么就通过 insertBefore 将目标节点插入到兄弟节点之前,如果父节点不是 dom 节点(HostRoot/HostPortal)就找到它们对应的 container 再执行 inserBefore。
  • PlacementAndUpdate:说明既有 Placement 又有 Update,先调用 Placement 对应的操作,然后调用 commitWork,commitwork 会对 dom 节点进行属性的设置。
  • Update:调用 commitWork。
  • Deletion:删除操作稍显复杂,因为删除的节点其下可能还有其它节点,所以需要遍历其子树执行删除操作,内部是一个递归的过程,对于 dom 节点会调用 removeChild,对于 class 组件会先解除 ref 的引用(safelyDetachRef)然后调用 componentWillUnmount。

注:

  • 关于 Placement 对于父节点不是 dom 节点的插入,可以参考这个流程图
  • 删除操作的遍历方式类似树的深度优先遍历

流程图:React@16.8.6源码浅析——提交阶段(commitMutationEffects)

commitLayoutEffects

该方法是整个 commit 阶段最后一个循环执行的方法,内部主要调用两个方法 commitLayoutEffectOnFiber 和 commitAttachRef,第一个方法内部是一个 switch 对于不同的节点进行不同的操作:

  • ClassComponent:执行 componentDidMount 或 componentDidUpdate,最后调用 commitUpdateQueue 处理 update,这里不同于 processUpdateQueue,这里主要处理 update 上面的 callback,比如 setState 方法的第二个参数或是生成异常 update 对应的 callback(componentDidCatch)
  • HostRoot:也会调用 commitUpdateQueue,因为 ReactDOM.render 方法的第三个参数也可以接受一个 callback
  • HostComponent:判断如果有 autoFocus 则调用 focus 方法来获取焦点
  • 其它类型暂且不表

至于 commitAttachRef 方法其实就是将节点的实例对象挂载到 ref.current 上
流程图:React@16.8.6源码浅析——提交阶段(commitLayoutEffects)

合成事件

注入

注入阶段是在 ReactDOM.js 文件一加载就去执行的,主要目的就是创建三个全局变量一边以后使用

源码地址

github.com/LFESC/react…

流程图

在线地址:www.processon.com/view/link/5…

核心方法

injectEventPluginOrder
该方法接受一个字符串数组,该数组定义了要注入事件插件的顺序,然后调用 recomputePluginOrdering 方法,该方法会按照传入的插件顺序往 plugins 数组中添加 plugin,并且对 plugin 中的每个事件调用 publishEventForPlugin 方法,下面的代码以 change plugin 为例以便你可以直观的了解该方法所做的事情。

const publishedEvents = pluginModule.eventTypes;
    // const eventTypes = {
    //   change: {
    //     phasedRegistrationNames: {
    //       bubbled: 'onChange',
    //       captured: 'onChangeCapture',
    //     },
    //     dependencies: [
    //       TOP_BLUR,
    //       TOP_CHANGE,
    //       TOP_CLICK,
    //       TOP_FOCUS,
    //       TOP_INPUT,
    //       TOP_KEY_DOWN,
    //       TOP_KEY_UP,
    //       TOP_SELECTION_CHANGE,
    //     ],
    //   },
    // };
    for (const eventName in publishedEvents) {
      invariant(
        publishEventForPlugin(
          publishedEvents[eventName], // ChangeEventPlugin.eventTypes.change
          pluginModule, // ChangeEventPlugin
          eventName, // change
        ),
        'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
        eventName,
        pluginName,
      );
    }
  }

publishEventForPlugin
该方法首先会设置 eventNameDispatchConfigs 这个全局变量,接着遍历 phasedRegistrationNames 该对象存储的是你会在 react 元素上绑定的时间名,格式如下:

phasedRegistrationNames: {
  bubbled: 'onChange',
  captured: 'onChangeCapture',
}

bubbled 表示在冒泡阶段触发,captured 表示在捕获阶段触发,接着对每一项调用 publishRegistrationName 方法

publishRegistrationName
该方法会将传入的参数设置到 registrationNameModules 和 registrationNameDependencies 这两个全局变量上

全局变量
通过以上方法最终会形成如下的全局变量,我们以 ChangeEventPlugin 为例
ChangeEventPlugin:

const eventTypes = {
  change: {
    phasedRegistrationNames: {
      bubbled: 'onChange',
      captured: 'onChangeCapture',
    },
    dependencies: [
      TOP_BLUR,
      TOP_CHANGE,
      TOP_CLICK,
      TOP_FOCUS,
      TOP_INPUT,
      TOP_KEY_DOWN,
      TOP_KEY_UP,
      TOP_SELECTION_CHANGE,
    ],
  },
};

eventNameDispatchConfigs:

{
  change: ChangeEventPlugin.eventTypes.change,
  // ...other plugins
}

registrationNameModules:

{
  onChange: ChangeEventPlugin,
  onChangeCapture: ChangeEventPlugin
}

registrationNameDependencies:

{
  onChange: ChangeEventPlugin.eventTypes.change.dependencies,
  onChangeCapture: ChangeEventPlugin.eventTypes.change.dependencies
}

监听

监听阶段是在 render 阶段中的 completeWork 方法中会去对 HostComponent 调用 updateHostComponent 方法,在这个方法里面会对 dom 节点的 props 进行设置,其中就包括了事件相关的属性,我们在这里对事件进行绑定,绑定的节点是 document 而不是它自己,这样有利于减少内存开销提高性能,而对于交互类型和非交互类型的事件会绑定不同的事件处理函数。

流程图

在线地址:www.processon.com/view/link/5…

核心方法

finalizeInitialChildren
挂载事件的入口方法,在 render 阶段的 completeWork 中被调用,在该方法中会调用 setInitialProperties。

setInitialProperties
该方法会默认对一些 dom 节点绑定事件即使你没有设置事件,比如对 iframe 会绑定 load 事件,接着会执行 setInitialDOMProperties 方法。

setInitialDOMProperties
该方法会对 dom 节点上设置的 props 进行处理,这些 props 有 style、dangerouslySetInnerHTML、children 等,当然还有和事件相关的属性,还记得我们在前一节注入里面设置过的全局变量 registrationNameModules 吗,这里就派上用场了,可以通过它来判断是否有事件相关的属性该绑定了,如果有我们会调用 ensureListeningTo 方法。

ensureListeningTo
该方法会接受 reactDOM.render 接受的第二个参数 container 和事件名(比如 onClick),判断 container 是否是 document 或 DocumentFragment 节点,如果是就把它传递给 listenTo 方法,否则通过 element.ownerDocument 获取对应的 document 然后传递给 listenTo 方法。

listenTo
该方法首先会给传入的 element 创建一个 listeningSet,该对象用于存储该元素监听了哪些事件,接着通过我们在注入阶段生成的全局对象 registrationNameDependencies 获取要绑定的事件所依赖的其它事件(dependencies),对 dependencies 进行遍历,对需要在捕获阶段监听的事件调用 trapCapturedEvent,其它事件就调用 trapBubbledEvent 方法,最后将事件放入 listeningSet 中。

trapCapturedEvent/trapBubbledEvent
这两个方法都会调用 trapEventForPluginEventSystem,区别是 trapCapturedEvent 方法第三个参数会穿 true,trapBubbledEvent 第三个参数会传 false。

trapEventForPluginEventSystem
首先判断传入的事件是否是一个 Interactive 事件,也就是是否是用户交互相关的事件,如果是就将 dispatch 设置为 dispatchInteractiveEvent 否则设置为 dispatchEvent,然后根据第三个参数也就是 capture 来调用 addEventCaptureListener 或 addEventBubbleListener。

注:react 中定义的交互事件在这里可以看到

addEventCaptureListener/addEventBubbleListener
这两个方法都会调用 element.addEventListener 区别在于第三个参数一个是 true 一个是 false,表示在捕获阶段触发还是在冒泡阶段触发。

触发

流程图

在线地址:www.processon.com/view/link/5…

核心方法

dispatchInteractiveEvent
该方法会调用 discreteUpdates,该方法会调用 runWithPriority 并以 UserBlockingPriority 这个优先级去调用 dispatchEvent 方法。

dispatchEvent
该方法首先通过 getEventTarget 方法获取事件 target 对象(nativeEventTarget),注意 event target 对应的是事件触发的元素而不是事件绑定的元素,接着获取 target 对象对应的 fiber 对象(targetInst),如果自身找不到就向上寻找,如果发现该节点还没有挂载到 dom 上那就把 targetInst 设置为 null,最后调用 dispatchEventForPluginEventSystem 方法。

dispatchEventForPluginEventSystem
该方法先将上一步传入的和事件相关的参数(topLevelType,nativeEvent,targetInst)存储到一个对象上(bookKeeping),因为可能会多次创建该对象所以 react 这里使用了对象池的方式创建,然后调用了 batchedEventUpdates 方法传入 handleTopLevel 和 bookKeeping,执行完成之后把 bookkeeping 对象归还到对象池中。

batchedEventUpdates
该方法首先会判断 isBatching 这个变量是否为真,如果为真就直接执行接受的第一个方法,也就是上一步传入的 handleTopLevel,否则将 isBatching 置为 true,然后去执行 batchedEventUpdatesImpl 方法传入 handleTopLevel 和 bookkeeping,执行完成之后会执行 batchedUpdatesFinally 方法。

batchedEventUpdatesImpl
当我们看到一个方法后面有 Impl 很可能它是通过依赖注入来实现的,这里就是这样,它会根据平台来定义该方法的实现,在 dom 环境中我们实际调用的方法是 batchedEventUpdates,该方法判断当前的 workPhase 是否不是 NotWorking,如果不是 NotWorking 说明我们可能已经处于 batch 阶段,这个时候我们只需执行传入的方法然后退出,如果当前处于 NotWorking 状态我们将 workPhase 置为 BatchedEventPhase 然后执行传入的方法,执行完成之后恢复之前的 workPhase 然后执行 flushSyncCallbackQueue。

注:flushSyncCallbackQueue 该方法我们在上面讲过了

handleTopLevel
首先通过一个循环创建一个 ancestors 数组,一般来讲里面就只有一个对象就是 dom 节点对应的 fiber 对象,接着遍历这个数组,获取 eventTarget、事件名(topLevelType)和原生的事件对象(nativeEvent),将其传入 runExtractedPluginEventsInBatch 方法中。

runExtractedPluginEventsInBatch
该方法会首先调用 extractPluginEvents 去创建一个 event 对象,然后再调用 runEventsInBatch 方法执行它。

extractPluginEvents
该方法会遍历 plugins(就是注入阶段创建的 plugins),然后调用 plugin(比如说 ChangeEventPlugin)的 extractEvents 方法,最后将创建好的 events 返回。

注:event 对象的生成我们放到下一节来讲。

runEventsInBatch
该方法会调用 forEachAccumulated 传入要处理的 events 就是上一步传入的 events 和 executeDispatchesAndReleaseTopLevel 方法,forEachAccumulated 是一个工具方法,它的作用只是对 events 中的每一项调用 executeDispatchesAndReleaseTopLevel 方法。

executeDispatchesAndReleaseTopLevel
该方法什么都没做只是调用了 executeDispatchesAndRelease 方法并把 event 对象传给它。

executeDispatchesAndRelease
该方法会调用 executeDispatchesInOrder 然后判断 event 是否需要持久保留,如果不需要就释放掉它。

注:这里就可以解释第 6 个问题,为什么 event 对象无法保留,因为在事件处理函数执行完就把它销毁了,除非你手动调用 event.persist() 方法。 源码地址

executeDispatchesInOrder
终于到了事件最终执行的地方了,首先我们要获取 event 对象上的 dispatchListeners 和 dispatchInstances,然后遍历 dispatchListeners 判断 event 是否阻止冒泡了(isPropagationStopped)如果阻止冒泡了我们就跳出循环,如果没有阻止我们就调用 executeDispatch 方法传入对应的 listener(dispatchListeners[i])和 instance(dispatchInstances[i]),执行完后要将 dispatchListeners 和 dispatchInstances 清空。

executeDispatch
该方法首先获取事件类型(event.type),设置 event.currentTarget 为传入的 instance 对应的 dom 节点,然后调用 invokeGuardedCallbackAndCatchFirstError 方法传入 type、listener 和 event,其实该方法内部会做一些错误的捕获,本质上就是直接调用了 listener 并将 event 传入进去。

batchedUpdatesFinally
在 batchedEventUpdates 中执行完 batchedEventUpdatesImpl 就会执行 batchedUpdatesFinally,在这个方法中会首先判断 restoreQueue 或 restoreTarget  是否为空,如果不为空就说明有受控组件需要处理,然后调用 flushDiscreteUpdatesImpl 对应 dom 环境下就是 flushDiscreteUpdates 会立即执行更新,接着会调用 restoreStateIfNeeded 该方法会将受控组件的 value 设置为 props.value。

本节解决的问题

  1. 受控表单组件如果设置了value就无法输入内容是什么原因?
  2. 为何 react 的事件对象无法保留?

事件对象

流程图

在线地址:www.processon.com/view/link/5…

源码地址

github.com/LFESC/react…

核心方法

extractEvents
每一个 event plugin 都有一个 extractEvents 方法用来生成事件对象,我们以 ChangeEventPlugin 为例进行讲解。
首先获取对应的 dom 节点,生命两个变量 getTargetInstFunc, handleEventFunc,然后通过三个 if else 判断来给 getTargetInstFunc 赋值,这里的判断是判断当前 dom 节点应该使用什么事件,比如对于 select 元素应该使用 change 事件,那它对应的 getTargetInstFunc 就为 getTargetInstForChangeEvent
接着我们调用 getTargetInstFunc 这个方法,这个方法内部判断 event 事件是否是对应的事件,比如 getTargetInstForChangeEvent 判断事件名是否是 change,如果是就返回 targetInst(对应的 fiber 对象),然后判断返回的结果是否存在,如果存在就去执行 createAndAccumulateChangeEvent 创建 event 对象并返回,这里这么做事因为所有事件绑定都会去掉每一个 plugin 的 extractEvents 方法,所以需要在内部判断是否需要创建对应类型的 event 对象。

createAndAccumulateChangeEvent
该方法首先调用 SyntheticEvent.getPooled 方法创建一个 event 对象,创建的方式也采用对象池的方式,然后设置 event.type 为 change,然后调用 enqueueStateRestore 和 accumulateTwoPhaseDispatches 最后将 event 返回。

SyntheticEvent
在 SyntheticEvent.getPooled 中如果对象池中没有可用的对象就会调用合成事件(SyntheticEvent)构造函数来创建一个合成事件,这个事件对象是对原生事件对象的封装,它实现了原生对象的方法(preventDefault、stopPropagation)也添加了自己的一些方法(persist),你可以通过 nativeEvent 这个属性获取原生的事件对象。

enqueueStateRestore
将 target 放到 restoreQueue 数组中,设置 restoreTarget 为 target 以便以后可以恢复它的 value。

accumulateTwoPhaseDispatches
该方法会调用 forEachAccumulated 方法传入 event 和 accumulateTwoPhaseDispatchesSingle,其实我们之前讲过 forEachAccumulated 这个方法,这就是一个工具方法,它只是去调用 accumulateTwoPhaseDispatchesSingle 并把 event 传入进去。

accumulateTwoPhaseDispatchesSingle
该方法内部又调用了 traverseTwoPhase 传入的参数是 fiber(targetInst)、accumulateDirectionalDispatches 和 event。

traverseTwoPhase
该方法会从传入的 fiber 对象开始向上找到所有父节点为 HostComponent 的 fiber 节点放入 path 数组中,然后遍历 path 调用传入的方法(accumulateDirectionalDispatches),第一次遍历是从最后一个元素开始遍历,accumulateDirectionalDispatches 方法传入的第二个参数是 'captured',第二次遍历是从第一个元素开始遍历,传递的第二个参数是 'bubbled' 这两个遍历的顺序正好符合捕获和冒泡的顺序,所以执行 listeners 的时候就不需要判断哪个是捕获阶段哪个是冒泡阶段,直接按照数组的顺序执行即可,顺便一提第一个参数是遍历的那个 fiber 节点,第三个参数是 event 对象。

accumulateDirectionalDispatches
在这里我们终于要获取我们设置的事件处理函数了,首先我们调用 listenerAtPhase 来获取到 onChange 或 onChangeCapture 所绑定的事件处理函数(listener),然后将 listener 插入到 event._dispatchListeners,接着把对应的 fiber 对象插入到 event._dispatchInstances 中。

Github

包含带注释的源码、demos和流程图
github.com/kwzm/learn-…