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

本系列文章总共三篇:javascript

课前小问题

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

目录结构

顶层目录

React 采用 monorepo 的管理方式。仓库中包含多个独立的包,以便于更改能够一块儿联调,而且问题只会出如今同一地方。html

  • 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,好比:java

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

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

渲染器

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

reconciler

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

scheduler

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

事件系统

react 本身实现了一套事件系统,和原生的 DOM 事件系统相比减小了内存消耗,抹平了浏览器差别,那么 react 是如何作到的呢,主要是采用了如下策略:npm

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

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

内置对象

FiberRoot

FiberRoot 是整个应用的入口对象,它是一个 javascript 对象,内部记录了不少和应用更新相关的全局信息,好比要挂载的 container。react-native

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 是如何计算出来的呢,能够参考以下的过程:

若是没有看懂也没有关系,我这里计算了几种状况下的 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

总体流程

前期准备阶段

前期准备阶段所作的事情归纳起来就三点:

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

schedule

见下方

render

见下方

commit

见下方

setState

总体流程

前期准备阶段

前期准备阶段所作的事情归纳起来就三点:

  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)。

总体流程

核心方法

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)。

总体流程

核心方法

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

总体流程

核心方法

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…

流程图

React@16.8.6源码浅析——事件机制(注入)

核心方法

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 而不是它本身,这样有利于减小内存开销提升性能,而对于交互类型和非交互类型的事件会绑定不一样的事件处理函数。

流程图

React@16.8.6源码浅析——事件机制(监听)

核心方法

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,表示在捕获阶段触发仍是在冒泡阶段触发。

触发

流程图

React@16.8.6源码浅析——事件机制(触发)

核心方法

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 的事件对象没法保留?

事件对象

流程图
React@16.8.6源码浅析——事件机制(事件对象)

源码地址

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-…