转前端一年半了,平时接触最多的框架就是React
。在熟悉了其用法以后,避免不了想深刻了解其实现原理,网上相关源码分析的文章挺多的,可是总感受不如本身阅读理解来得深入。因而话了几个周末去了解了一下经常使用的流程。也是经过这篇文章将本身的我的理解分享出来。html
在具体的源码流程分析以前,根据我的理解,结合网上比较好的文章,先来分析一些概念性的东西。后续再分析具体的流程逻辑。前端
React 15
版本(Fiber之前)整个更新渲染流程分为两个部分:node
Reconciler
(协调器); 负责找出变化的组件Renderer
(渲染器); 负责将变化的组件渲染到页面上在React
中能够经过setState
、forceUpdate
、ReactDOM.render
来触发更新。每当有更新发生时,Reconciler
会作以下工做:react
render
方法,将返回的JSX
转化为虚拟DOM
DOM
和上次更新时的虚拟DOM
对比DOM
Renderer
将变化的虚拟DOM渲染到页面上在对某个更新节点执行玩Reconciler
以后,会通知Renderer
根据不一样的"宿主环境"进行相应的节点渲染/更新。git
React 15
的diff
过程是 递归执行更新 的。因为是递归,一旦开始就"没法中断" 。当层级太深或者diff
逻辑(钩子函数里的逻辑)太复杂,致使递归更新的时间过长,Js
线程一直卡主,那么用户交互和渲染就会产生卡顿。看个例子: count-demogithub
<button> click <button>
<li>1<li> -> <li>2<li>
<li>2<li> -> <li>4<li>
<li>3<li> -> <li>6<li>
复制代码
当点击button
后,列表从左边的一、二、3
变为右边的二、四、6
。每一个节点的更新过程对用户来讲基本是同步,但实际上他们是顺序遍历的。具体步骤以下:算法
button
,触发更新Reconciler
检测到<li1>
须要变动为<li2>
,则马上通知Renderer
更新DOM
。列表变成二、二、3
Reconciler
检测到<li2>
须要变动为<li4>
,通知Renderer
更新DOM
。列表变成二、四、3
Reconciler
检测到<li3>
须要变动为<li6>
,则马上通知Renderer
更新DOM
。列表变成二、四、6
今后可见 Reconciler
和Renderer
是交替工做 的,当第一个节点在页面上已经变化后,第二个节点再进入Reconciler
。因为整个过程都是同步的,因此在用户看来全部节点是同时更新的。若是中断更新,则会在页面上看见更新不彻底的新的节点树!api
假如当进行到第2步的时候,忽然由于其余任务而中断当前任务,致使第三、4步没法进行那么用户就会看到:数组
<button> click <button>
<li>1<li> -> <li>2<li>
<li>2<li> -> <li>2<li>
<li>3<li> -> <li>3<li>
复制代码
这种状况是React
绝对不但愿出现的。可是这种应用场景又是十分必须的。想象一下,用户在某个时间点进行了输入事件,此时应该更新input
内的内容,可是由于一个不在当前可视区域的列表的更新致使用户的输入更新被滞后,那么给用户的体验就是卡顿的。所以React
团队须要寻找一个办法,来解决这个缺陷。浏览器
React15架构不能支撑异步更新以致于须要重构,因而React16架构改为分为三层结构:
React 15
对React 16
提出的需求是Diff更新应为可中断的,那么此时又出现了两个新的两个问题:中断方式和判断标准;
React
团队采用的是 合做式调度,即主动中断和控制器出让。判断标准为超时检测。同时还须要一种机制来告知中断的任务在什么时候恢复/从新执行。 React
借鉴了浏览器的requestIdleCallback
接口,当浏览器有剩余时间时通知执行。
因为一些缘由React
放弃使用rIdc
,而是本身实现了功能更完备的polyfill
,即Scheduler
。除了在空闲时触发回调的功能外,Scheduler
还提供了多种调度优先级供任务设置。
在React 15
中Reconciler
是递归处理Virtual DOM
的。而React16
使用了一种新的数据结构:Fiber
。Virtual DOM
树由以前的从上往下的树形结构,变化为基于多向链表的"图"。
更新流程从递归变成了能够中断的循环过程。每次循环都会调用shouldYield()
判断当前是否有剩余时间。源码地址。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
复制代码
前面有分析到React 15
中断执行会致使页面更新不彻底,缘由是由于Reconciler
和Renderer
是交替工做的,所以在React 16
中,Reconciler
与Renderer
再也不是交替工做。当Scheduler
将任务交给Reconciler
后,Reconciler
只是会为变化的Virtual DOM
打上表明增/删/更新的标记,而不会发生通知Renderer
去渲染。相似这样:
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
复制代码
只有当全部组件都完成Reconciler
的工做,才会统一交给Renderer
进行渲染更新。
Renderer
根据Reconciler
为Virtual DOM
打的标记,同步执行对应的渲染操做。
对于咱们在上一节使用过的例子,在React 16
架构中整个更新流程为:
setState
产生一个更新,更新内容为:state.count
从1
变为2
Scheduler
,Scheduler
发现没有其余更高优先任务,就将该任务交给Reconciler
Reconciler
接到任务,开始遍历Virtual DOM
,判断哪些Virtual DOM
须要更新,为须要更新的Virtual DOM
打上标记Reconciler
遍历完全部Virtual DOM
,通知Renderer
Renderer
根据Virtual DOM
的标记执行对应节点操做其中步骤二、三、4随时可能因为以下缘由被中断:
因为Scheduler
和Reconciler
的工做都在内存中进行,不会更新页面上的节点,因此用户不会看见更新不彻底的页面。
React的Diff
是有必定的 前提假设 的,主要分为三点:
Virtual DOM
树进行分层比较,两棵树只会对同一层次的节点进行比较。ID
区分节点不管是
JSX
格式仍是React.createElement
建立的React组件最终都会转化为Virtual DOM
,最终会根据层级生成相应的Virtual DOM
树形结构。React 15
每次更新会成新的Virtual DOM
,而后通 递归 的方式对比新旧Virtual DOM
的差别,获得对比后的"更新补丁",最后映射到真实的DOM
上。React 16
的具体流程后续会分析到
React源码很是多,并且16之后的源码一直在调整,目前Github上最新源码都是保留
xxx.new.js
与xxx.old.js
两份代码。react源码 是采用Monorepo
结构来进行管理的,不一样的功能分在不一样的package
里,惟一的坏处可能就是方法地址索引发来不是很方便,若是不是对源码比较熟悉的话,某个功能点可能须要经过关键字全局查询而后去一个个排查。开始以前,能够先阅读下官方的这份阅读指南
由于源码实在是太多太复杂了,全部我这里尽量的最大到小,从面到点的一个个分析。大体的流程以下:
JSX
或者createElement
编码的代码到底会转成啥ReactDOM.render
setState
更新的流程Scheduler
、Reconciler
、Renderer
的大体流程触发渲染更新的操做除了
ReactDOM.render
、setState
外,还有forceUpdate
。可是实际上是差很少的,最大差别在于forceUpdate
不会走shouldComponentUpdate
钩子函数。
先来看一个最简单的JSX
格式编码的组件,这里借助babel
进行代码转换,代码看这
// JSX
class App extends React.Component {
render() {
return <div />
}
}
// babel
var App = /*#__PURE__*/function (_React$Component) {
_inherits(App, _React$Component);
var _super = _createSuper(App);
function App() {
_classCallCheck(this, App);
return _super.apply(this, arguments);
}
_createClass(App, [{
key: "render",
value: function render() {
return /*#__PURE__*/React.createElement("div", null);
}
}]);
return App;
}(React.Component);
复制代码
关键点在于render
方法其实是调用了React.createElement
方法。那么接下来咱们只须要分析createElement
作了啥便可。咱们先看看ReactElement
的结构:
let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol === 'function' && Symbol.for) {
REACT_ELEMENT_TYPE = Symbol.for('react.element');
}
const ReactElement = function (type, key, ref, props) {
const element = {
// 惟一地标识为React Element,防止XSS,JSON里不能存Symbol
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
}
return element;
}
复制代码
很简单的一个数据结构,每一个属性的做用都一目了然,就不一一解释了。而后分析React.createElement
源码。
这里多提一句,源码注释里为何说
$$typeof
可以有效防护JSX呢?通常来讲,咱们编码的DOM
都会转为ReactElement
的对象,可是React
提供了dangerouslySetInnerHTML
来做为innerHTML
的替代方案,当攻击者在服务端的资源中插入一段dangerouslySetInnerHTML
的JSON
的时候,会由于没有$$typeof
而被React
判断为无效。由于Symbol
没法JSON
化呀;具体检测的源码看这里
const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
if (config !== null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
}
// 过滤React保留的关键字
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
// 遍历children
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 设置默认props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(type, key, ref, props);
}
复制代码
注释应该已经够清楚了哈。总结下来就是根据参数来生成一个ReactElement
对象,并绑定对应的props
、key
、ref
等;
开始正式流程分析以前,但愿你对Fiber
有过必定的了解。若是没有,建议你先看看这则视频。而后,先来熟悉下ReactFiber
的大概结构。
export type Fiber = {
// 任务类型信息;
// 好比ClassComponent、FunctionComponent、ContextProvider
tag: WorkTag,
key: null | string,
// reactElement.type的值,用于reconciliation期间的保留标识。
elementType: any,
// fiber关联的function/class
type: any,
// any类型!! 通常是指Fiber所对应的真实DOM节点或对应组件的实例
stateNode: any,
// 父节点/父组件
return: Fiber | null,
// 第一个子节点
child: Fiber | null,
// 下一个兄弟节点
sibling: Fiber | null,
// 变动状态,好比删除,移动
effectTag: SideEffectTag,
// 用于连接新树和旧树;旧->新,新->旧
alternate: Fiber | null,
// 开发模式
mode: TypeOfMode,
// ...
};
复制代码
每一次经过ReactDom.render
渲染的一棵树或者一个应用都会初始化一个对应的FiberRoot
对象做为应用的起点。其数据结构以下ReactFiberRoot
。
type BaseFiberRootProperties = {
// The type of root (legacy, batched, concurrent, etc.)
tag: RootTag,
// root节点,ReactDOM.render()的第二个参数
containerInfo: any,
// 持久更新会用到。react-dom是整个应用更新,用不到这个
pendingChildren: any,
// 当前应用root节点对应的Fiber对象
current: Fiber,
// 当前更新对应的过时时间
finishedExpirationTime: ExpirationTime,
// 已经完成任务的FiberRoot对象,在commit(提交)阶段只会处理该值对应的任务
finishedWork: Fiber | null,
// 树中存在的最旧的未到期时间
firstPendingTime: ExpirationTime,
// 挂起任务中的下一个已知到期时间
nextKnownPendingLevel: ExpirationTime,
// 树中存在的最新的未到期时间
lastPingedTime: ExpirationTime,
// 最新的过时时间
lastExpiredTime: ExpirationTime,
// ...
};
复制代码
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 不肯定类型;多是class或function
export const HostRoot = 3; // 树的根
export const HostPortal = 4; // 一颗子树
export const HostComponent = 5; // 原生节点;根据环境而定,浏览器环境就是div等
export const HostText = 6; // 纯文本节点
export const Fragment = 7;
复制代码
到React 16.13.1
版本位置,内置的开发模式有以下几种:
export type TypeOfMode = number;
// 普通模式,同步渲染,React15-16的生产环境用
export const NoMode = 0b0000;
// 严格模式,用来检测是否存在废弃API(会屡次调用渲染阶段生命周期),React16-17开发环境使用
export const StrictMode = 0b0001;
// ConcurrentMode 模式的过渡版本
export const BlockingMode = 0b0010;
// 并发模式,异步渲染,React17的生产环境用
export const ConcurrentMode = 0b0100;
// 性能测试模式,用来检测哪里存在性能问题,React16-17开发环境使用
export const ProfileMode = 0b1000;
复制代码
本文只分析 ConcurrentMode 模式
ReactDOM.render
使用参考这里
通常来讲,使用React
编写应用,ReactDOM.render
是咱们触发的第一个函数。那么咱们先从ReactDOM.render
这个入口函数开始分析render
的整个流程。
源码中会频繁出现针对
hydrate
的逻辑判断和处理。这个是跟SSR
结合客户端渲染相关,不会作过多分析。源码部分我都会进行省略
ReactDOM.render
实际上对ReactDOMLegacy
里的render
方法的引用,精简后的逻辑以下:
export function render( // React.creatElement的产物 element: React$Element<any>, container: Container, callback: ?Function, ) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
复制代码
实际上调用的是legacyRenderSubtreeIntoContainer
方法,再来看看这个咯
function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, // 通常为null children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// [Q]: 初始化容器。清空容器内的节点,并建立FiberRoot
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// FiberRoot; 应用的起点
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// [Q]: 初始化不能批量处理,即同步更新
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 省略... 更上面相似,差异是无需初始化容器和可批处理
// [Q]:咦? unbatchedUpdates 有啥奥秘呢
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
复制代码
根据官网的使用文档可知,在这一步会先清空容器里现有的节点,若是有异步回调callback
会先保存起来,并绑定对应FiberRoot
引用关系,以用于后续传递正确的根节点。注释里我标注了两个[Q]
表明两个问题。咱们先来仔细分析这两个问题
从命名上看,legacyCreateRootFromDOMContainer
是用来初始化根节点的。 将legacyCreateRootFromDOMContainer
的返回结果赋值给container._reactRootContainer
,而_reactRootContainer
从代码上看是做为是否已经初始化的依据,也验证了这一点。不信的话,打开你的React
应用,查看下容器元素的_reactRootContainer
属性
function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean, ): RootType {
// 省略 hydrate ...
return createLegacyRoot(container, undefined);
}
export function createLegacyRoot( container: Container, options?: RootOptions, ): RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}
function ReactDOMBlockingRoot( container: Container, tag: RootTag, options: void | RootOptions, ) {
// !!! look here
this._internalRoot = createRootImpl(container, tag, options);
}
复制代码
一连串的函数调用,其实就是还回了一个ReactDOMBlockingRoot实例。其中重点在于属性_internalRoot
是经过createRootImpl
建立的产物。
function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) {
// 省略 hydrate ...
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
// 省略 hydrate ...
return root;
}
export function createContainer( containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot {
// 生成 FiberRoot
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// 为Root生成Fiber对象
const uninitializedFiber = createHostRootFiber(tag);
// 绑定 FiberRoot 与 Fiber
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 生成更新队列
initializeUpdateQueue(uninitializedFiber);
return root;
}
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
baseQueue: null,
shared: {
pending: null,
},
effects: null,
};
fiber.updateQueue = queue;
}
复制代码
大体逻辑就是生成了一个FiberRoot
对象root
。并生成了root
对应的Fiber
对象,同时生成了该fiber
的更新队列。从这里清楚的知道了FiberRoot
是在什么时候初始化的,咱们得先记住这个FiberRoot
,能够认为他是整个React
应用的起点。
源码中的英文注释说明这里是无需批处理,应该当即执行。其传入参数是一个执行updateContainer
的包装函数。 可是在else
判断中实际上也执行了updateContainer
。那么unbatchedUpdates
有啥奥秘呢?
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
// !!! look here
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}
export function flushSyncCallbackQueue() {
// 省略...
flushSyncCallbackQueueImpl();
}
// 清空同步任务队列
function flushSyncCallbackQueueImpl() {
if (!isFlushingSyncQueue && syncQueue !== null) {
isFlushingSyncQueue = true;
let i = 0;
try {
const isSync = true;
const queue = syncQueue;
// 以最高优先级来清空队列里的任务
runWithPriority(ImmediatePriority, () => {
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
});
syncQueue = null;
} catch (error) {
// 移除错误的任务
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
}
// 在下一个执行单元恢复执行
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueue,
);
throw error;
} finally {
isFlushingSyncQueue = false;
}
}
}
复制代码
在unbatchedUpdates
中,其实就是多了一段finally
中的逻辑。其中的逻辑主要是刷新同步任务队列。想想,为啥呢?那么说明在fn(a)
的执行过程当中确定产生了同步任务呗!那么接下来继续跟进到updateContainer
中瞧一瞧。
注意,这里updateContainer
已是属于Reconciler
流程了哦。继续跟进:
export function updateContainer( element: ReactNodeList, // 要渲染的组件 container: OpaqueRoot, // OpaqueRoot就是FiberRoot parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTimeOpaque {
// 根节点Fiber
const current = container.current;
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// TODO:计算这次任务的过时时间
const expirationTime = computeExpirationForFiber(
currentTime,
current,
suspenseConfig,
);
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// 建立一个更新任务
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
update.payload = { element };
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 将任务插入Fiber的更新队列
enqueueUpdate(current, update);
// 调度任务 scheduleWork为scheduleUpdateOnFiber
scheduleWork(current, expirationTime);
return expirationTime;
}
复制代码
这一步看上去代码贼多,其实就是先计算出当前更新的过时时间,而后经过createUpdate
建立了一个update
更新任务,接着经过enqueueUpdate
插入 循环任务队列,最后使用scheduleUpdateOnFiber
来调度任务。
从这里开始,源码中有同步和异步两种处理方式,同步任务是不会通过
Scheduer
进行调度的。为了分析的完整性,咱们只分析异步过程。后续频繁提到的expirationTime
,能够暂且认为其为任务的"过时时间节点",是具体的"时间点",而不是"时间长度"。可是在不一样的阶段其意义是不同的。能够肯定的是,组件的更新与否或者说更新的时间节点是由其来决定的。
export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTimeOpaque, ) {
// 获取FiberRoot,并更新子Fiber的过时时间(父组件更新触发子组件更新)
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
return null;
}
if (expirationTime === Sync) {
// 同步任务调度
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
// 省略...
}
复制代码
scheduleUpdateOnFiber
只是用于 更新以当前节点为Root的整个"树"的过时时间。 其中重点在ensureRootIsScheduled
这个方法
// 此函数用于调度任务。 一个root(fiber节点)只能有一个任务在执行
// 若是已经有任务在调度中,将检查已有任务的到期时间与下一级别任务的到期时间相同。
// 每次更新和任务退出前都会调用此函数
// 注意:root是FiberRoot
function ensureRootIsScheduled(root: FiberRoot) {
// lastExpiredTime表明过时时间
const lastExpiredTime = root.lastExpiredTime;
if (lastExpiredTime !== NoWork) {
// 特殊状况:过时的工做应同步刷新
root.callbackExpirationTime = Sync;
root.callbackPriority = ImmediatePriority;
root.callbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
return;
}
// TODO:从暂停或等待的任务中取出优先级最高的任务的过时时间
// 就是从任务队列中取出下次将执行的调度任务的过时时间?
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
// root有正在处理的调度任务
const existingCallbackNode = root.callbackNode;
if (expirationTime === NoWork) {
if (existingCallbackNode !== null) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
}
return;
}
// 计算当前任务的过时时间; 同一事件中发生的全部优先级相同的更新都收到相同的到期时间
const currentTime = requestCurrentTimeForUpdate();
// 根据下一次调度任务的过时时间与当前任务的过时时间计算出当前任务的优先级
// 即currentTime小于expirationTime,那么其优先级更高
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
);
// 若是当前正在处理的任务优先级基于这次任务,取消正在处理的任务!
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// 任务必须具备彻底相同的到期时间。
existingCallbackExpirationTime === expirationTime &&
// 比较两次任务的优先级
existingCallbackPriority >= priorityLevel
) {
return;
}
// 取消调度任务
cancelCallback(existingCallbackNode);
}
// 更新到期时间与优先级
root.callbackExpirationTime = expirationTime;
root.callbackPriority = priorityLevel;
let callbackNode;
if (expirationTime === Sync) {
// 省略...
// 这里会将任务推入同步任务队列,前面分析到 flushSyncCallbackQueueImpl 清空的任务就是从这里推入
} else {
// 将任务推入Scheduler调度队列
callbackNode = scheduleCallback(
priorityLevel,
// 绑定
performConcurrentWorkOnRoot.bind(null, root),
// 计算超时时间
{ timeout: expirationTimeToMs(expirationTime) - now() },
);
}
// 更新Fiber的当前回调节点
root.callbackNode = callbackNode;
}
复制代码
ensureRootIsScheduled
中的主要逻辑分三步:
Scheduler
中的调度队列,并设置其优先级与任务过时时间这段代码每一段都是能够去延伸开分析的。可是我这里主要是分析大体流程,因此主要分析scheduleCallback
相关的逻辑。其余部分,之后有时间在进一步分析。
scheduleCallback
是将任务的执行函数交由Scheduler
来处理。那么后续的流程须要等待Scheduler
来触发具体的执行函数performConcurrentWorkOnRoot
。关于render
的流程就先暂时分析到这里为止。
render
会调用legacyRenderSubtreeIntoContainer
方法legacyRenderSubtreeIntoContainer
中,若是是第一次渲染,会先初始化FiberRoot
,其为应用的起点。同时生成根节点的Fiber
实例。这里 FiberRoot.current
= Fiber
; Fiber.stateNode
= FiberRoot
。updateContainer
会计算出这次更新的过时时间。并生成任务对象update
,将其插入Fiber
中的更新队列,而后调用scheduleUpdateOnFiber
触发任务调度scheduleUpdateOnFiber
会更新以该Fiber节点为根节点的整棵Fiber树的过时时间。而后调用ensureRootIsScheduled
进行调度ensureRootIsScheduled
中会绑定任务与具体执行函数。而后交由Scheduler
处理在继续分析后续的Reconciler
和Renderer
细节以前,咋们趁热打铁来熟悉下setState
的流程。既然调用的时候是经过this.setState
来调动的,那么就从Component
里面去找咯。来look
一下ReactBaseClasses
const emptyObject = {};
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
// ReactNoopUpdateQueue 是一个没啥意义的空对象
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码
Component
的初始结构很简单。咱们看到其setState
方法就是调用了this.updater.enqueueSetState
方法,可是update
默认是空的无用对象,咱们通常也没有在构造方法里传入一个update
参数,那么说明这个方法确定是后续注入的咯。与是我找啊找,找到了一个差很少的东西classComponentUpdater
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// 生成这次setState的更新对象
const update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 更新任务入队
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueReplaceState(inst, payload, callback) {
// 同上相似
},
enqueueForceUpdate(inst, callback) {
// 同上相似
},
};
复制代码
嘿嘿,是否是发现了enqueueSetState
里的逻辑有点似曾相识。其实就是咱们以前分析render
流程中遇到的updateContainer
的流程是同样的啦。不记得的话回头再看看咯。那么接下来咱们只要分析下classComponentUpdater
是怎么注入为Component
的update
属性便可了。
前面分析render
流程的时候,咱们还只分析到了生成任务分片并推入调度队列,尚未对组件的初始化有过度析。从Component
的构造函数中猜想是否是在初始化Component
的时候React
帮咱们注入的呢? 顺着这个思路进行下一步的分析。首先咱们先来看beginWork
方法中的一段代码,beginWork
方法在后面会具体分析。这里先知道他是用于建立子组件的Fiber
对象便可。
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
// 尝试复用 current 节点
if (current !== null) {
// 省略...
}
// 不能复用则 update 或者 mount
switch (workInProgress.tag) {
// 省略...
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 省略...
}
}
复制代码
beginWork
中的代码分为两部分。分别用于处理mount
和update
的逻辑。咱们分析的流程是第一次初始化,那么走的是mount
流程。beginWork
会根据不一样的tag
调用不一样的方法,这里咱们先来看看updateClassComponent
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
// 省略 context 的处理...
// 组件的实例
const instance = workInProgress.stateNode;
let shouldUpdate;
// instance为null 说明组件第一次渲染
if (instance === null) {
if (current !== null) {
// 重置current与wip的依赖(备份)
current.alternate = null;
workInProgress.alternate = null;
// 标记为新增节点
workInProgress.effectTag |= Placement;
}
// 初始化组件实例
constructClassInstance(workInProgress, Component, nextProps);
// 挂载; 并调用相应的生命周期
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
shouldUpdate = true;
} else {
// 省略更新逻辑...
}
// TODO:执行 render 新建子Fiber。
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
return nextUnitOfWork;
}
复制代码
function constructClassInstance( workInProgress: Fiber, ctor: any, props: any, ): any {
let context = emptyContextObject;
// 省略 context 相关逻辑...
const instance = new ctor(props, context);
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null);
adoptClassInstance(workInProgress, instance);
// 省略 context 相关逻辑...
return instance;
}
复制代码
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// 绑定实例与Fiber,方便后续更新使用
setInstance(instance, workInProgress);
}
复制代码
能够看到当instance
为null
的时候,会执行如下几个流程
effectTag
为Placement
,表明为新增节点Fiber(workInProgress)
上,并绑定update
属性mountClassInstance
来挂载节点,并调用相关的生命周期。至此,后续的更新流程就跟render
流程一致的了,就不作重复分析啦~
Scheduler
是React
团队针对任务调度单独实现的一个rIdc
的polyfill
。React
团队其意图不只仅局限于React
这一个应用场景,更想服务与更多的业务,成为更普遍应用的一个工具。
既然任务具备不一样的过时时间和优先级,那么就须要一个数据结构来管理优先级任务。React
中expirationTime
越小的任务应该更优先处理,那么这个数据结构显然就是一个最小优先队列啦。而React
是基于小顶堆来实现的最小优先队列。仍是直接看代码吧。SchedulerMinHeap
type Heap = Array<Node>;
type Node = {|
id: number,
sortIndex: number,
|};
// 插入到堆末尾
export function push(heap: Heap, node: Node): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
// 获取堆顶任务,sortIndex/id 最小的任务
export function peek(heap: Heap): Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
// 删除堆顶任务
export function pop(heap: Heap): Node | null {
const first = heap[0];
if (first !== undefined) {
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null;
}
}
// 向上维持小顶堆
function siftUp(heap, node, i) {
let index = i;
while (true) {
// 位运算;对应根据节点求其父节点-> i / 2 - 1
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (parent !== undefined && compare(parent, node) > 0) {
// parent 更大,交换位置
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
return;
}
}
}
// 向下维持小顶堆
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// // 若是左子节点或右子节点小于目标节点(父节点),则交换
if (left !== undefined && compare(left, node) < 0) {
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
// 先比较sort index,再比较 task id
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
复制代码
具体实现就是用数组模拟了一个最小堆的结构。能够看到,每次任务的插入或者移除都会从新回复最小堆结构,排序规则以sortIndex
,taskId
为辅。在React中sortIndex
对应的其实就是过时时间,taskId
则为递增任务序列。这一点后续会分析到。
前面有分析到在ensureRootIsScheduled
中会生成一个任务节点,而后经过scheduleCallback
将任务推入Scheduler
中。那么咱们先从这个任务进队的方法来逐步分析
var taskIdCounter = 1;
// 目前Scheduler对外的api都是unstate_级别的,表示不是稳定版本
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 实际是调用performance.now() 或者 Date.now() 前者更精确
var currentTime = getCurrentTime();
var startTime;
var timeout;
// 根据是否有延迟来肯定开始时间
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
// [Q1]:有超时配置直接用。不然根据优先级计算
timeout =
typeof options.timeout === 'number'
? options.timeout
: timeoutForPriorityLevel(priorityLevel);
} else {
timeout = timeoutForPriorityLevel(priorityLevel);
startTime = currentTime;
}
// 过时时间等于开始时间+超时时间
var expirationTime = startTime + timeout;
// 一个task的数据结构就是这样啦。
var newTask = {
// 相同超时时间的任务会对比id,那就是先到先得咯
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// [Q2]:下面出现了一个延迟队列(timerQueue)和一个任务队列(taskQueue)
if (startTime > currentTime) {
// This is a delayed task.
// 说明这是一个延迟任务;即options.delay存在嘛
newTask.sortIndex = startTime;
// 若是开始时间大于当前时间,就将它 push 进这个定时器队列,说明这个是一个等待队列
push(timerQueue, newTask);
// 若是任务队列为空,说明全部任务都被延迟,且newTask是最先的延迟任务。
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// 若是正在进行超时处理,先取消,后续再从新开始
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 发起一个超时处理
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
// 非延迟任务丢入任务队列
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// 若是没在调度中则开启调度;
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// [Q]开启调度
requestHostCallback(flushWork);
}
}
// [A]:还回这个task的引用
return newTask;
}
复制代码
从这段代码能够看到一个调度任务的数据结构是怎样的,以及任务的排序依据sortIndex
其实就是任务的过时时间expirationTime
,而id
则是一个递增序列。注释中标注了几个问题,下面一一具体分析
// 当即执行
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 用户行为阻塞
var USER_BLOCKING_PRIORITY = 250;
// 默认五秒过时时间
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// 永不过时, maxSigned31BitInt为v8 32为系统最大有效数值
var IDLE_PRIORITY = maxSigned31BitInt;
function timeoutForPriorityLevel(priorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
return IMMEDIATE_PRIORITY_TIMEOUT;
case UserBlockingPriority:
return USER_BLOCKING_PRIORITY;
case IdlePriority:
return IDLE_PRIORITY;
case LowPriority:
return LOW_PRIORITY_TIMEOUT;
case NormalPriority:
default:
return NORMAL_PRIORITY_TIMEOUT;
}
}
复制代码
能够看到,这里将优先级转换成了常量级的具体时间,优先级越高的timeout
时间越低。
在startTime > currentTime
的条件分支中,分别将任务推入了taskQueue
和timerQueue
。而这两个队列其实就是咱们前面分析到的一个最小堆的结构。taskQueue
表明当前正在调度的任务,而timerQueue
表明延迟任务队列。在任务调度的过程当中,会不停的将timerQueue
中的任务转移到taskQueue
中,这一步后续会分析到。
咱们看到当任务插入调度队列时,若是此时不在调度中,会调用requestHostCallback
方法开启调度,并传入了一个flushwork
做为入参函数。
requestHostCallback = function(callback) {
// 这里将传入的callback缓存起来了
scheduledHostCallback = callback;
// 是否在消息循环中
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
复制代码
从代码看彷佛rHC
的做用只是缓存了callback
即flushwork
这个入参函数。并发送了一个空的message
。那么重点就在与这个port
是为什么物了。其实这里就是React
如何模拟requestIdleCallback
的地方了。
不熟悉MessageChannel
的能够先了解一下。先来看看Scheduler
中是如何用的。
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
复制代码
能够得知,当使用port.postMessage
发生消息的时候,实际处理消息的函数为performWorkUntilDeadline
。
let isMessageLoopRunning = false;
let scheduledHostCallback = null;
const performWorkUntilDeadline = () => {
// scheduledHostCallback 具体是由 scheduledHostCallback 赋值的
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// [Q]:截止时间 = 当前时间 + yieldInterval
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// 是否还有剩余任务。scheduledHostCallback 多是 flushwork
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
// 没有更多任务 中止循环,并清楚scheduledHostCallback引用
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 若是还有任务,则继续发消息。相似一个递归的操做
port.postMessage(null);
}
} catch (error) {
// 若是一个任务出错了。直接跳过执行下一个任务,并抛出错误
port.postMessage(null);
throw error;
}
} else {
// 重置循环状态
isMessageLoopRunning = false;
}
// [Q]: 目前不知道这是啥
needsPaint = false;
};
复制代码
老样子,这里有几个问题须要仔细分析下。
从名字和使用方法上来看,我觉着应该是表明任务的执行时间。
// 默认是5
let yieldInterval = 5;
forceFrameRate = function (fps) {
// ??? 看不起我144hz
if (fps < 0 || fps > 125) {
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing framerates higher than 125 fps is not unsupported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5;
}
};
复制代码
forceFrameRate
是一个对外提供的api
接口,用于动态配置调度任务的执行周期。
let deadline = 0;
let maxYieldInterval = 300;
let needsPaint = false;
if (
enableIsInputPending &&
navigator !== undefined &&
navigator.scheduling !== undefined &&
navigator.scheduling.isInputPending !== undefined
) {
const scheduling = navigator.scheduling;
shouldYieldToHost = function () {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
// 没有时间了。可能但愿让主线程让出控制权,以便浏览器能够执行高优先级任务,主要是绘制和用户输入
// 所以若是有绘制或者用户输入行为,则应该让出,放回true
// 若是二者都不存在,那么能够在保持响应能力的同时下降产量
// 可是存在非`requestPaint`发起的绘制状态更新或其余主线程任务(如网络事件)
// 所以最终在某个临界点仍是得让出控制权
if (needsPaint || scheduling.isInputPending()) {
// 有待处理的绘制或用户输入
return true;
}
// 没有待处理的绘制或输入。但在达到最大产量间隔时也须要释放控制权
return currentTime >= maxYieldInterval;
} else {
return false;
}
};
requestPaint = function () {
needsPaint = true;
};
} else {
shouldYieldToHost = function () {
return getCurrentTime() >= deadline;
};
requestPaint = function () { };
}
复制代码
首先须要明确的是shouldYieldToHost
与requestPaint
是Scheduler
对外提供的接口函数。具体的使用后续会分析到位。
从代码可知,deadline
的用途是用于在shouldYieldToHost
中 检测调度是否超时。默认清空下是直接对比当前时间currentTime
与deadline
的值。可是,在支持navigator.scheduling
的环境下,React
会有更多的考虑,也就是浏览器绘制与用户输入要有限响应,不然能够适当的延长调度时间。
到这里先总结下调度启动的过程,省得脑子糊了。
requestHostCallback
准备好要执行的任务scheduledHostCallback
requestHostCallback
开启任务调度循环MessageChannel
接收消息,并调用performWorkUntilDeadline
执行任务performWorkUntilDeadline
中先计算这次调度的deadline
。而后执行任务performWorkUntilDeadline
。不然结束消息循环前面还只是分析了任务调度循环执行的逻辑。具体执行的任务是scheduledHostCallback
的引用函数flushWork
。
function flushWork(hasTimeRemaining, initialTime) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod codepath.
// 官方注释说,生成环境不会去catch workLoop抛出的错误
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
复制代码
flushWork
的工做比较简单。只是重置了一些标志符,最终返回了workLoop
的执行结果。那么重点确定在这个函数了。
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// [Q]: 这是做甚?
advanceTimers(currentTime);
// 取出顶端任务。即最优先的任务
currentTask = peek(taskQueue);
while (
currentTask !== null &&
// debug 用的,无论
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
// 任务未过时,而且当前调度的deadline到了,将任务放到下次调度周期进行; shouldYieldToHost
currentTask.expirationTime > currentTime &&
// 这两个前面分析过了; hasTimeRemaining一直为true,那还判断有啥意义???
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
const callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
// 计算当前任务是否已经超时
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
// [Q]: 执行callback,好比前面render流程分析到的 performConcurrentWorkOnRoot
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// continuationCallback 成立,则取代当前任务的callback
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
// continuationCallback 不成立,从任务队列弹出
// 防止任务被其余地方取出,得判断一下
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
// em.... 又是它
advanceTimers(currentTime);
} else {
// 任务被取消了,弹出任务
// 回顾下ensureRootIsScheduled 中调用 cancelCallback 的状况
pop(taskQueue);
}
// 再次从顶端取任务
// 注意:若是 continuationCallback 成立的话,是没有pop当前任务的。这次取到的仍是当前任务
currentTask = peek(taskQueue);
}
// performWorkUntilDeadline 中判断 hasMoreWork 的逻辑就是这里啦!
if (currentTask !== null) {
return true;
} else {
// [Q]:检测延迟队列中的任务是否是过时
let firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
复制代码
大体流程注释已经很详细了。老规矩,分析标注的几个问题。
function advanceTimers(currentTime) {
// 遍历 timerQueue 中的任务;将超时的任务转移到 taskQueue 中去
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// 任务被取消
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// 超时任务转移
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
// 未过期的继续挂起
return;
}
timer = peek(timerQueue);
}
}
复制代码
wookLoop
函数入口第一次调用advanceTimers
是将任务从新梳理一下,刷新任务队列。而以后每次在while
调用是 由于任务的执行是须要消耗必定的时间的,全部在执行完后须要从新刷新任务队列。
首先continuationCallback
的产生是有callback
决定的。callback
的返回值多是一个函数,这表明着当前任务应该被从新处理一次。这里先留个问题,后续在分析callback
的具体实现的时候,咱们再进一步分析
在wookLoop
的结尾,当currentTask === null
的时候,会去检测延迟队列中的任务是否已通过期。
requestHostTimeout = function (callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
// 从新梳理任务队列
advanceTimers(currentTime);
// isHostCallbackScheduled 为true。说明有新任务进来了
if (!isHostCallbackScheduled) {
// 若是上面的 advanceTimers 梳理了过时的延迟任务到任务队列中,则执行
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
// 不然递归调用该方法
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
复制代码
能够看出,其实就是在任务队列中的任务执行完成后。经过递归的方法从延迟队列中查询是否有过时任务,有的话则转移到任务队列中,并执行。
到这里,Scheduler
从任务入列,到循环调度,到任务执行的完整过程就已经分析完成了。作个简单的流程总结:
unstable_scheduleCallback
建立任务,若是任务是延迟的则推入延迟队列timerQueue
,不然推入任务队列taskQueue
requestHostTimeout
方法使用setTimeout
来 递归检测任务是否过时。不然直接发起任务调度requestHostCallback
requestHostCallback
经过MessageChannel
的port2
发送消息给port1
,具体的处理函数为performWorkUntilDeadline
performWorkUntilDeadline
会计算这次调度的deadline,同时使用 消息循环 来递归执行任务wookLoop
执行。其将任务从任务队列taskQueue
堆顶依次取出执行。若是任务队列清空,则调用requestHostTimeout
开启递归检测。分析完Scheduler
的逻辑后,接下来接着分析Reconciler
的逻辑。咱们老生常谈的Diff
更新的逻辑大部分就是发生在Reconciler
阶段,其中包含了大量的组件更新计算与优化。
上面分析了Scheduler
的调度过程。而具体在Scheduler
中的执行的callback
是performConcurrentWorkOnRoot
。咱们来看一看
// 被Scheduler调用的入口函数
function performConcurrentWorkOnRoot(root, didTimeout) {
// 重置
currentEventTime = NoWork;
if (didTimeout) {
// 任务已经超时
const currentTime = requestCurrentTimeForUpdate();
// 将过时时间标记为当前,以在单个批处理中同步处理已过时的工做。
markRootExpiredAtTime(root, currentTime);
// 调度一个同步任务
ensureRootIsScheduled(root);
return null;
}
// 获取下一个到期(更新)时间. 将以此做为本次渲染的执行必要性判断
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
if (expirationTime !== NoWork) {
const originalCallbackNode = root.callbackNode;
// TODO:刷新被动的Hooks
flushPassiveEffects();
// 若是根或到期时间已更改,则丢弃现有堆栈并准备新的堆栈。 不然,咱们将从中断的地方继续。
if (
root !== workInProgressRoot ||
expirationTime !== renderExpirationTime
) {
// [Q]: 重置数据;
// 设置 renderExpirationTime 为expirationTime
// 复制 root.current 为 workInProgress等
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
}
if (workInProgress !== null) {
// 省略...
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// 省略...
}
if (workInProgress !== null) {
// 仍然有任务要作。说明是超时了,退出而不提交。
stopInterruptedWorkLoopTimer();
} else {
stopFinishedWorkLoopTimer();
const finishedWork: Fiber = ((root.finishedWork =
root.current.alternate): any);
root.finishedExpirationTime = expirationTime;
// commit;开始 Renderer 流程
finishConcurrentRender(
root,
finishedWork,
workInProgressRootExitStatus,
expirationTime,
);
}
}
return null;
}
复制代码
首先会判断任务是否超时,若是超时则以同步的方式执行该任务,防止任务被中断。若是没有超时,则先在prepareFreshStack
中作一些初始化的工做。而后进入了workLoopConcurrent
循环。
// 本次渲染的到期时间
let renderExpirationTime: ExpirationTime = NoWork;
function prepareFreshStack(root, expirationTime) {
// 省略...
if (workInProgress !== null) {
// workInProgress 不为空说明以前有中断的任务。放弃
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
// 从current 复制 wip; 并重置effectList
workInProgress = createWorkInProgress(root.current, null);
// 设置renderExpirationTime为下一个到期时间
renderExpirationTime = expirationTime;
// 省略...
}
复制代码
若是当前wip
不为空,说明上次有中断的任务,经过不停向上回溯直到root
节点来取消中断的任务。而后从 同时将前面从FiberRoot
中获取下一个任务的到期时间,赋值给renderExpirationTime
做为本次渲染的到期时间。
workLoopConcurrent
的代码在本文开头就贴出来过,这里从新看下
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
// 第一次入参workInProgress为FiberRoot的Fiber
// 后续将上一次返回值(子Fiber)做为入参
workInProgress = performUnitOfWork(workInProgress);
}
}
复制代码
workLoopConcurrent
的工做主要是循环对比current
和workInProgress
两颗Fiber
树。在wip
中为变化的Fiber
打上effectTag
。同时会从下往上更新/建立DOM
节点,构成一颗离屏DOM
树,最后交由Renderer
处理。
在熟悉流程以前,先贴出一个删减版的代码流程。这里不按套路出牌,先根据我的理解作个总结。这样带着大体的思路结构可能会更好的去理解后续的源码。
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 旧的 Fiber, 用于对比
const current = unitOfWork.alternate;
// 省略...
// [Q]: 处理当前Fiber节点,还回下一个子节点Fiber
let next = beginWork(current, unitOfWork, renderExpirationTime);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 没有子节点
if (next === null) {
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
// 尝试完成当前的Fiber,而后移至下一个同级。若是没有更多的同级,返回父fiber。
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
workInProgress = unitOfWork;
do {
// 旧的 Fiber, 用于对比
const current = workInProgress.alternate;
const returnFiber = workInProgress.return;
// Check if the work completed or if something threw.
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
// [Q]: 建立/更新当前Fiber对应的节点实例
let next = completeWork(current, workInProgress, renderExpirationTime);
stopWorkTimer(workInProgress);
resetChildExpirationTime(workInProgress);
if (next !== null) {
// 产生了新的子节点
return next;
}
// [Q]:后面是在构建 effectList 的单向链表
// 先省略...
} else {
// 有异常抛出。根据是不是boundary来决策是捕获仍是抛出异常
// 省略...
}
const siblingFiber = workInProgress.sibling;
// 是否存在兄弟节点
if (siblingFiber !== null) {
return siblingFiber;
}
workInProgress = returnFiber;
} while (workInProgress !== null);
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
复制代码
首先执行beginWork
进行节点操做,以及建立子节点,子节点会返回成为next
,若是有next
就返回。返回到workLoopConcurrent
以后,workLoopConcurrent
会判断是否过时之类的,若是没过时则再次调用该方法。
若是next
不存在,说明当前节点向下遍历子节点已经到底了,说明这个子树侧枝已经遍历完,能够完成这部分工做了。执行completeUnitOfWork
,主要分一下几个步骤
completeUnitOfWork
首先调用completeWork
建立/更新当前Fiber
对应的节点实例(如原生DOM节点)instance
,同时将已经更新的子Fiber
的实例插入到instance
构成一颗离屏渲染树。Fiber
节点存在effectTag
则将其追加到effectList
中sibling
兄弟节点,有则返回该兄弟节点,由于这个节点可能也会存在子节点,须要经过beginWork
进行操做。root
节点或者在某一个节点发现有sibling
兄弟节点。root
,那么其返回也是null
,表明整棵树的遍历已经结束了,能够commit
了。若是中间遇到兄弟节点则同于第3
步文字表达可能不是很清楚,直接看一个例子:
workLoopConcurrent.png
执行顺序为:
文本节点“你好” 不会执行
beginWork/completeWork
,由于React
针对只有单一文本子节点的Fiber
,会特殊处理
1. App beginWork
2. div Fiber beginWork
3. span Fiber beginWork
4. span Fiber completeWork
5. div Fiber completeWork
6. p Fiber beginWork
7. p Fiber completeWork
8. App Fiber completeWork
复制代码
beginWork
在前面分析setState
的时候已经分析过其中mount
阶段对应的逻辑了。那么这里就只分析update
的逻辑了。先来看下beginWork
的大体工做。
/** * @param {*} current 旧的Fiber * @param {*} workInProgress 新的Fiber * @param {*} renderExpirationTime 下一次到期时间,即本次渲染有效时间 * @returns 子组件 Fiber */
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
// 尝试复用 current 节点
if (current !== null) {
// 省略...
// 复用 current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
workInProgress.expirationTime = NoWork;
// 不能复用则 update 或者 mount
switch (workInProgress.tag) {
// 省略...
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
// 省略...
}
}
复制代码
咱们接着以前分析过的updateClassComponent
来分析update
的流程。
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
// 提早处理context逻辑。省略....
// 组件的实例
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
// mount. wip.effectTag = Placement
// 省略...
} else {
// update. wip.effectTag = Update | Snapshot
// 调用 render 以前的生命周期,getDerivedStateFromProps | UNSAFE_componentWillReceiveProps(可能两次)
// 接着调用shouldComponentUpdate判断是否须要更新
// 最后更新props 和 state
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
}
// 执行 render 新建子Fiber。
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
return nextUnitOfWork;
}
function finishClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, ) {
// 引用应该更新,即便shouldComponentUpdate返回false
markRef(current, workInProgress);
const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;
// 无需更新且没有发送错误则直接复用current
if (!shouldUpdate && !didCaptureError) {
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren = instance.render();
// PerformedWork 提供给 React DevTools 读取
workInProgress.effectTag |= PerformedWork;
if (current !== null && didCaptureError) {
// 出错了。
// 省略...
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
workInProgress.memoizedState = instance.state;
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) {
if (current === null) {
// mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
复制代码
最后还回的就是workInProgress.child
,跟beginWork
同样,根据current === null
来区分mount
和update
。
实际上mountChildFibers
和reconcileChildFibers
均指向同一个函数reconcileChildFibers
。差异在于第二个参数currentFirstChild
。若是为null
,则会去建立一个新的Fiber
对象,不然复用并更新props
。好比reconcileSingleElement
用于处理只有单个节点的状况。
function completeWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
//省略...
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// fiber节点对应的DOM节点是否存在
// update
if (current !== null && workInProgress.stateNode != null) {
// 为 wip 计算出新的 updateQueue
// updateQueue 是一个奇数索引的值为变化的prop key,偶数索引的值为变化的prop value 的数组
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// mount
if (!newProps) {
return null;
}
const currentHostContext = getHostContext();
// 是否是服务端渲染
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// 省略...
} else {
// 生成真实DOM
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中,从下往上,构成一颗离屏DOM树
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// 与updateHostComponent相似的处理 props
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
markRef(workInProgress);
}
}
return null;
}
//省略...
}
}
复制代码
首先和beginWork
同样,根据current === null
判断是mount
仍是update
。
update
时,主要作了以下几件事情,具体源码diffProperties
:
STYLE prop
DANGEROUSLY_SET_INNER_HTML prop
CHILDREN prop
每次计算出新的prop
,都将其propKey
与nextProp
成对的保存在数组updatePayload
中。最后将updatePayload
赋值给wip.updateQueue
。
mount
时,处理的事情比较多,大体以下:
createInstance
: 为Fiber
节点生成对应的真实DOM
节点appendAllChildren
: 将子孙DOM
节点插入刚生成的DOM
节点中。以此从下往上构成完整的DOM
树finalizeInitialChildren
: 在setInitialProperties
中处理事件注册。在setInitialDOMProperties
根据props
初始化DOM
属性值的注意的是appendAllChildren
方法。因为completeWork
属于向上回溯的过程,每次调用appendAllChildren
时都会将已生成的子孙DOM
节点插入当前生成的DOM
节点下。那么当回溯到根root
节点时,整个DOM
树就都已经更新好了。
在每次completeWork
后,表明某个节点已经处理完成。前面说过,Reconciler
会为发生改变的节点打上effectTag
,用于在Renderer
根据节点的effectTag
的执行具体更新。
所以在completeWork
的上层函数completeUnitOfWork
中(也就是以前省略的代码),每执行完completeWork
会去维护一个effectList
的单向链表。若是当前Fiber
存在effectTag
,则插入链表。
// 构建 effectList 的单向链表
if (
returnFiber !== null &&
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// firstEffect 为链表头结点
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
// lastEffect 为链表尾节点
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
const effectTag = workInProgress.effectTag;
// 跳过NoWork和PerformedWork tag。后者是提供给React Tools读取
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
复制代码
至此,Reconciler
流程结束。回头再看看开头的总结,是否是清楚一些了呢~
Commit
阶段的代码相对另外两个来讲是较为简单的。其入口在前面分析过的任务调度入口函数performConcurrentWorkOnRoot
中的结尾finishConcurrentRender
。最终调用的函数为commitRootImpl
。看看代码:
let nextEffect: Fiber | null = null;
function commitRootImpl(root, renderPriorityLevel) {
// 省略...
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
// commit不可中断。 老是同步完成。
// 所以,如今能够清除这些内容以容许安排新的回调。
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
root.nextKnownPendingLevel = NoWork;
// 省略...
// 获取effectList
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}
if (firstEffect !== null) {
// 省略...
nextEffect = firstEffect;
do {
// [Q]: 执行 snapshot = getSnapshotBeforeUpdate()
// 结果赋值为 Fiber.stateNode.instance.__reactInternalSnapshotBeforeUpdate = snapshot
commitBeforeMutationEffects();
} while (nextEffect !== null);
// 省略...
nextEffect = firstEffect;
do {
// [Q]: 根据Fiber.effectTag 执行具体的增删改DOM操做
// 若是是卸载组件,还会调用 componentWillUnmount()
commitMutationEffects(root, renderPriorityLevel);
} while (nextEffect !== null);
// 省略...
nextEffect = firstEffect;
do {
// [Q]: 调用 render 后的生命周期
// current === null ? componentDidMount : componentDidUpdate
commitLayoutEffects(root, expirationTime);
} while (nextEffect !== null);
stopCommitLifeCyclesTimer();
nextEffect = null;
// 告诉Scheduler在帧末尾中止调度,这样浏览器就有机会绘制。
requestPaint();
// 省略...
} else {
// 省略...
}
// 省略...
return null;
}
复制代码
省略了许多的代码,留下主要的内容。主要逻辑就是拿到Reconciler
维护的effectList
链表后,三次遍历该链表,分别作的是:
Snapsshot
;用于componentDidUpdate
的第三个参数Fiber.effectTag
对组件或DOM执行具体操做完整代码看commitBeforeMutationLifeCycles
,其中tai为ClassComponent
的组件主要逻辑以下:
const current = nextEffect.alternate;
finishedWork = nextEffect;
if (finishedWork.effectTag & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
复制代码
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
// 把节点的文字内容设置为空字符串
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
// 把ref置空,后续会设置ref,因此以前ref上的值须要先清空
commitDetachRef(current);
}
}
let primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// 从effectTag中清除Placement标记
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
// componentWillUnmount
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
// 省略...
}
nextEffect = nextEffect.nextEffect;
}
}
复制代码
好像也没啥好说的。值得注意的是,开始前会先调用commitDetachRef
将ref
的引用清除。而后针对不一样的effectTag
执行不一样的DOM
操做。
commitPlacement
; 新增节点。其中节点插入位置的计算算法能够看下;commitWork
; 根据Reconciler
在diffProperties
计算出来的updateQueue
数组进行DOM
更新commitDeletion
; 这一步会从上往下依次调用该子树下每一个组件的componentWillUnmount
函数function commitLayoutEffects( root: FiberRoot, committedExpirationTime: ExpirationTime, ) {
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime,
);
}
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void {
switch (finishedWork.tag) {
// ...
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 调用setState注册的回调函数
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
// ...
}
}
复制代码
仍是遍历每一个Fiber
节点。若是是ClassComponent
,须要调用生命周期方法。同时对于更新的ClassComponent
,须要判断调用的setState
是否有回调函数,若是有的话须要在这里一块儿调用。最后会调用commitAttachRef
更新ref
引用。
Commit
阶段的流程到这里也就结束了。
说实话,React
的源码是在是真的多。想完彻底全细节分析到每个点,须要大量的时间和精力。本文也只是分析了一个大体的流程,不少细节之处没有分析到位。后续会再花点时间针对一些细节问题作下探索。说到底,目前也只从面到面,而没有达到从面到点分析的效果。许多观点是我的的理解,写出来是以供学习交流,有不妥之处,还请提提意见。