本系列文章总共三篇:javascript
React 采用 monorepo 的管理方式。仓库中包含多个独立的包,以便于更改能够一块儿联调,而且问题只会出如今同一地方。html
packages
包含元数据(好比 package.json
)和 React 仓库中全部 package 的源码(子目录 src
)。若是你须要修改源代码, 那么每一个包的 src
子目录是你最须要花费精力的地方。fixtures
包含一些给贡献者准备的小型 React 测试项目。build
是 React 的输出目录。源码仓库中并无这个目录,可是它会在你克隆 React 而且第一次构建它以后出现。React “Core” 中包含全部全局 React
API,好比:java
React 核心只包含定义组件必要的 API。它不包含协调算法或者其余平台特定的代码。它同时适用于 React DOM 和 React Native 组件。React 核心代码在源码的 packages/react 目录中。在 npm 上发布为 react 包。相应的独立浏览器构建版本称为 react.js,它会导出一个称为 React 的全局对象。react
React 最初只是服务于 DOM,可是这以后被改编成也能同时支持原平生台的 React Native。所以,在 React 内部机制中引入了“渲染器”这个概念。
渲染器用于管理一棵 React 树,使其根据底层平台进行不一样的调用。
渲染器一样位于 packages/
目录下:git
ReactDOM
API,这在npm上做为 react-dom
包。这也能够做为单独浏览器版本使用,称为 react-dom.js
,导出一个 ReactDOM
的全局对象.即使 React DOM 和 React Native 渲染器的区别很大,但也须要共享一些逻辑。特别是协调算法须要尽量类似,这样可让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工做一致。
为了解决这个问题,不一样的渲染器彼此共享一些代码。咱们称 React 的这一部分为 “reconciler”。当处理相似于 setState()
这样的更新时,reconciler 会调用树中组件上的 render()
,而后决定是否进行挂载,更新或是卸载操做。
Reconciler 没有单独的包,由于他们暂时没有公共 API。相反,它们被如 React DOM 和 React Native 的渲染器排除在外。
这部分源码在 /packages/react-reconciler。github
在上一篇中我说在 react 中从产生更新到最终操做DOM这之间能够叫作 reconciliation(协调)的过程,其实这中间还能够再进行细分,其中产生的更新会放在一个更新队列里,如何调度这些更新让它们进行下一步任务这个部分叫作 scheduler,而 react 采用叫作 Cooperative Scheduling (合做式调度)的方式来调度任务,简单来讲就是充分利用浏览器的空闲时间来执行任务,有空闲时间就执行对应的任务,没有就把执行权交给浏览器,在浏览器中就是经过 requestIdleCallback 这个 API 来实现的,可是由于这个 API 存在的一些问题以及浏览器的兼容性问题,因此 react 经过 requestAnimationFrame、setTimeout 和 MessageChannel 来模拟了 requestIdleCallback 的行为。如今 react 把这部分代码单独拎出来做为一个 package。
这部分源码在 /packages/scheduler 中。算法
react 本身实现了一套事件系统,和原生的 DOM 事件系统相比减小了内存消耗,抹平了浏览器差别,那么 react 是如何作到的呢,主要是采用了如下策略:npm
这部分的源码在 /packages/events 中。json
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();
}
}
复制代码
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;
}
复制代码
在上一篇中咱们说到为了将任务排出优先级 react 最开始只是定死了几个 Priority(优先级)变量,可是这样会出现饥饿问题,低优先级的任务可能一直被打断,后来 react 引入了 expirationTime(过时时间)的概念,这样即便是低优先级的任务只要过时时间一到也能强制当即执行,那么 expirationTime 是如何计算出来的呢,能够参考以下的过程:
若是没有看懂也没有关系,我这里计算了几种状况下的 expirationTime,你能够找找规律:
咱们能够发现对于同步任务好比 ReactDOM.render 来讲,expirationTime 就是很大的整数(32位系统中的最大整数),若是是低优先级的异步任务那么计算出来的时间以 25 为基数进行增加,而若是是高优先级的异步任务(好比用户交互)计算出来的时间是一 10 为基数进行增加,且相同的 currentTime 高优先级的 expirationTime 要大于低优先级的 expirationTime,react 这么作的目的:一是让 25/10 ms 之内触发的更新能有相同的过时时间,这样就能够批量更新以提高性能;二是让高优先级的任务过时时间大于低优先级以提升它的优先级。
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;
};
复制代码
ReactDom.render 的源代码位于 /react-dom/src/client/ReactDOM.js
前期准备阶段所作的事情归纳起来就三点:
前期准备阶段所作的事情归纳起来就三点:
须要注意的是若是是经过 react element 上绑定的事件函数里面调用的 setState 方法,会在执行 setState 方法以前设置 workPhase = BatchedEventPhase;,因此在 scheduleUpdateOnFiber 方法中会进入下图的分支。
找到触发更新节点对应的 fiberRoot 节点,而后调对该节点的更新,分为两种状况:同步和异步,同步又能够分为两种:是不是 LegacyUnbatchedPhase,若是是就不须要调度直接进入下一阶段(render phase),若是不是就放到下一帧当即执行,对于异步任务则须要根据优先级算出一个过时时间,而后再和队列里排队的任务进行比较找出立刻要过时的那个任务在下一帧进入下一个阶段执行(render phase)。
将传入的 callback 放入 syncQueue 中,而后调用 Scheduler_scheduleCallback 设置优先级为 Scheduler_ImmediatePriority,callback 为 flushSyncCallbackQueueImpl
流程图:React@16.8.6源码解析——调度(二)
将传入的 reactPriorityLevel 转换为 schedule 中的 priorityLevel 而后调用 Scheduler_scheduleCallback
流程图:React@16.8.6源码解析——调度(二)
根据传入的 priorityLevel 和 timeout 计算出新的 expirationTime,根据新的 expirationTime 和传入的 callback 建立一个 newNode,而后看看当前第一个等待调度的任务(firstCallbackNode)是不是空,若是是空就把 newNode 做为 firstCallbackNode 而后调用 scheduleHostCallbackIfNeeded,不然就比较 newNode 的过时时间是不是当前列表中最先的,若是是也把它设置为 firstCallbackNode 而后执行 scheduleHostCallbackIfNeeded
流程图:React@16.8.6源码解读——调度(三)
若是 firstCallbackNode 不为空就执行 requestHostCallback(flushWork, expirationTime);
流程图:React@16.8.6源码解读——调度(三)
设置 scheduledHostCallback 为传入的 callback 若是当前有一个 callback 正在执行或过时时间小于 0 则当即调用 port.postMessage 表示当即执行 scheduledHostCallback 并传入是否超时(didTimeout)不然调用 requestAnimationFrameWithTimeout(animationTick);
流程图:React@16.8.6源码解读——调度(四)
模拟 requestAnimationFrame 在下一帧执行传入的 callback,由于 requestAnimationFrame 在页签是后台运行时不执行,因此又经过 setTimeout 设置了一个了一个定时器来解决这个问题,若是 requestAnimationFrame 生效了就取消定时器,反之亦然。
流程图:React@16.8.6源码解读——调度(四)
若是 scheduledHostCallback 不为空就接着调用 requestAnimationFrameWithTimeout 安排下一帧的任务 不然就是没有等待的任务了就退出,计算出下一帧运行的时间(nextFrameTime),若是小于 8ms 就设置为 8ms ,接着计算出当前帧的过时时间(frameDeadline)若是有任务就接着调用 port.postMessage。
流程图:React@16.8.6源码解读——调度(四)
port.postMessage 后就能够被 port.onmessage 接收到,收到以后判断当前帧是否还有剩余时间,若是没有检查下要执行的任务(scheduledHostCallback)是否超时,超时就设置 didTimeout = true 没超时就接着调用 requestAnimationFrameWithTimeout,而后退出;若是剩余时间还有就执行 scheduledHostCallback(didTimeout)。
流程图:React@16.8.6源码解读——调度(四)
它接受的参数就是 port.onmessage 传入的 didTimeout,若是 didTimeout 为真(说明当前帧没有时间了)判断第一个要执行的任务 (firstCallbackNode)的 expirationTime 是否小于当前时间,小于的话就不断执行 flushFirstCallback 直到 firstCallbackNode 为空或 firstCallbackNode.expirationTime 大于等于当前时间;若是 didTimeout 为假(说明当前帧还有时间)那就不断执行 flushFirstCallback 直到 firstCallbackNode 为空或当前帧已经没有剩余时间了,最后不管是上面何种状况都会再执行 scheduleHostCallbackIfNeeded 判断一下是否还有须要执行的任务。
流程图:React@16.8.6源码解读——调度(六)
执行链表里的第一个任务(firstCallbackNode)并传入是否超时(didUserCallbackTimeout),这里 ImmediatePriority 也会当作超时,firstCallbackNode 可能会再返回一个 callback,将新的回调函数插入到列表中,根据它的到期时间排序,若是新的回调是列表中优先级最高的就调用 scheduleHostCallbackIfNeeded 安排下一次执行。
流程图:React@16.8.6源码解析——调度(五)
对于 scheduleSyncCallback 来讲最终执行的** **scheduledHostCallback 就是 flushSyncCallbackQueueImpl
这个方法中就是循环执行 syncQueue 数组中的任务。
流程图:React@16.8.6源码解析——调度(五)
还记得最开始若是处于同步阶段而且 workPhase 为 NotWorking 时执行完 scheduleCallbackForRoot 就会调用这个方法,这个方法首先去调用 Scheduler_cancelCallback 取消 immediateQueueCallbackNode,接着会执行 flushSyncCallbackQueueImpl 也就是上面那个方法,immediateQueueCallbackNode 的 callback 对应的就是 flushSyncCallbackQueueImpl,因此我认为这个方法就是当即调用 flushSyncCallbackQueueImpl 去执行 syncQueue 中的回调任务而不是等待下一帧执行。
从 rootFiber 开始循环遍历 fiber 树的各个节点,对于每一个节点会根据节点类型调用不一样的更新方法,好比对于 class 组件会建立实例对象,调用 updateQueue 计算出新的 state,执行生命周期函数等,再好比对于 HostComponent 会给它的 children 建立 fiber 对象,当一侧子树遍历完成以后会开始执行完成操做,即建立对应 dom 节点并添加到父节点下以及设置父节点的 effect 链,而后遍历兄弟节点对兄弟节点也执行上述的更新操做,就这样将整棵树更新完成以后就能够进入下一阶段(commit phase)。
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)
这个方法是任务开始以前的一些准备工做,以前一直好奇 workInProgress 是那里初始化的,其实就是在这里,这里会调用 createWorkInProgress 根据 rootFiber 拷贝出一个 workInProgress 的 fiber 对象来,接着还会设置一些其它全局变量。
workLoopSync 比较简单内部循环调用 performUnitOfWork,判断条件是 performUnitOfWork 的返回值 workInProgress 是否为空。
workLoop 和 workLoopSync 比较相似,区别就是循环的终止条件新增了 shouldYield,shouleYield 方法判断当前是否应该被打断,若是当前任务没有超时而且任务的时间片已经不够用了就会被打断,这时候 workLoop 循环就会终止。
performUnitOfWork 是 workLoopSync 和 workLoop 两个方法都会调用的方法,在其内部会调用 beginWork 方法,beginWork 方法会返回下一个要执行的任务(next),若是 next 为空表示已经遍历到叶子节点了,则调用 completeUnitOfWork 能够执行完成逻辑了,关于这块的执行细节能够参考上一篇。
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)
当 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)
这个方法内部就是一个 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)
**
提交阶段主要作的事情就是对 render 阶段产生的 effect 进行处理,处理分为三个阶段
commitRoot 接受 fiberRoot 对象,而后调用 commitRootImpl 方法并把 fiberRoot 对象传递给它,若是执行的过程当中有 Passive effect 产生就会调用 flushPassiveEffects 去执行这些 effect,Passive effect 和 hooks 有关,这里暂且不表。
流程图:React@16.8.6源码浅析——提交阶段(commitRoot)
首先根据传入的 fiberRoot 获取到 finishedWork 也就是以前阶段完成的 rootFiber,而后重置一些变量,接着处理 effect 链,若是 rootFiber 也有 effect,那也须要加到 effect 链上,接着经过三个循环来分别处理这些 effect:
最后调用 flushSyncCallbackQueue 若是 syncQueue 还有其它任务则执行它们
流程图:React@16.8.6源码浅析——提交阶段(commitRoot)
该方法会判断 effect 上是否有 Snapshot,Snapshot 会在 render 阶段判断 class 组件是否有 getSnapshotBeforeUpdate 这个生命周期时加上,若是有就调用 commitBeforeMutationEffectOnFiber,在这个方法里会判断 fiber 的类型,若是是 function 组件会调用 commitHookEffectList (这里我不太明白为何 function 组件会有 Snapshot),若是是 class 组件就会执行 getSnapshotBeforeUpdate 这个方法并将返回的结果设置到实例的 __reactInternalSnapshotBeforeUpdate 属性上,这个再 componentDidUpdate 的时候会用到。
流程图:React@16.8.6源码浅析——提交阶段(commitBeforeMutationEffects)
在该方法中会对一些和 dom 操做相关的 effect 进行执行:
注:
- 关于 Placement 对于父节点不是 dom 节点的插入,能够参考这个流程图
- 删除操做的遍历方式相似树的深度优先遍历
流程图:React@16.8.6源码浅析——提交阶段(commitMutationEffects)
**
该方法是整个 commit 阶段最后一个循环执行的方法,内部主要调用两个方法 commitLayoutEffectOnFiber 和 commitAttachRef,第一个方法内部是一个 switch 对于不一样的节点进行不一样的操做:
至于 commitAttachRef 方法其实就是将节点的实例对象挂载到 ref.current 上
流程图:React@16.8.6源码浅析——提交阶段(commitLayoutEffects)
注入阶段是在 ReactDOM.js 文件一加载就去执行的,主要目的就是建立三个全局变量一边之后使用
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 而不是它本身,这样有利于减小内存开销提升性能,而对于交互类型和非交互类型的事件会绑定不一样的事件处理函数。
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,表示在捕获阶段触发仍是在冒泡阶段触发。
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。
流程图
React@16.8.6源码浅析——事件机制(事件对象)
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 中。
包含带注释的源码、demos和流程图
github.com/kwzm/learn-…