React 源码解析系列文章欢迎阅读:
React16 源码解析 (一)- 图解 Fiber 架构
React16 源码解析(二)- 创建更新
React16 源码解析(三)-ExpirationTime
React16 源码解析(四)-Scheduler
React16 源码解析(五)- 更新流程渲染阶段 1
React16 源码解析(六)- 更新流程渲染阶段 2
React16 源码解析(七)- 更新流程渲染阶段 3
React16 源码解析(八)- 更新流程提交阶段
正在更新中 …
在 React 中创建更新主要有下面三种方式:
1、ReactDOM.render() || hydrate
2、setState
3、forceUpdate
注:
除了上面的还有 react 16.8 引进的 hooks 中的 useState,这个我们后续再讲。
hydrate 是服务端渲染相关的,这块我并不会重点讲解。
一、ReactDOM.render()
调用 legacyRenderSubtreeIntoContainer()
const ReactDOM: Object = {
// ......
render(
element: React$Element<any>,// 传入的 React 组件
container: DOMContainer,// 挂载的容器节点
callback: ?Function,// 挂载后的回调函数
) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
// ......
}
legacyRenderSubtreeIntoContainer
1、root = 创建 ReactRoot
2、调用 root.render()
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,//null
children: ReactNodeList,// 传入进来需要挂在的 class component
container: DOMContainer,// 根节点
forceHydrate: boolean,//false
callback: ?Function,// 挂载完成后的回调函数
) {
// ......
// 是否存在根节点 初次渲染是不存在根节点的
let root: Root = (container._reactRootContainer: any);
if (!root) {
// 1、创建 ReactRoot 赋值给 container._reactRootContainer 和 root(这里发生了很多事,一件很重要很重要的事 生成了 fiber 结构树。。)root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// ......
} else {
// ......
// 2、调用 root.render()
root.render(children, callback);
}
return DOMRenderer.getPublicRootInstance(root._internalRoot);
}
legacyCreateRootFromDOMContainer
1、清除所有子元素
2、创建 new ReactRoot 节点
function legacyCreateRootFromDOMContainer(
container: DOMContainer,// 根节点
forceHydrate: boolean,//false
): Root {
// 服务端渲染相关 是否合并原先存在的 dom 节点 一般是 false
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// 1、清除所有子元素,通过 container.lastchild 循环来清除 container 的所有内容,因为我们的属于首次渲染,container 里边不包含任何元素
if (!shouldHydrate) {
let warned = false;
let rootSibling;
while ((rootSibling = container.lastChild)) {
// ......
container.removeChild(rootSibling);
}
}
// Legacy roots are not async by default.
const isConcurrent = false;
// 2、创建 new ReactRoot 节点
return new ReactRoot(container, isConcurrent, shouldHydrate);
}
ReactRoot
从 ReactRoot 中,我们把 createContainer 返回值赋给了 实例的_internalRoot,往下看 createContainer
function ReactRoot(
container: Container,
isConcurrent: boolean,
hydrate: boolean,
) {
// 这里创建了一个 FiberRoot
const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
this._internalRoot = root;
}
createContainer
从 createContainer 看出,createContainer 实际上是直接返回了 createFiberRoot, 而 createFiberRoot 则是通过 createHostRootFiber 函数的返回值 uninitializedFiber,并将其赋值在 root 对象的 current 上,这里需要注意一个点就是,uninitializedFiber 的 stateNode 的值是 root,即他们互相引用。
创建一个 RootFiber -> createHostRootFiber() -> createFiber() -> new FiberNode()
这里创建的这个 RootFiber 里面的绝大部分属性都是初始值 null 或者是 NoWork。所以具体代码我就没有贴出来了。
这里我提下有意义的点:
RootFiber 上的 tag 会被赋值为 HostRoot。这个之后会用来判断节点类型。
还有这里创建的 FiberRoot 还有一个 containerInfo 置为 ReactDOM.render 第二个参数传入进来的容器节点。这个后续挂载的时候会用到。
export function createContainer(
containerInfo: Container,
isConcurrent: boolean,
hydrate: boolean,
): OpaqueRoot {return createFiberRoot(containerInfo, isConcurrent, hydrate);
}
export function createFiberRoot(
containerInfo: any,
isConcurrent: boolean,
hydrate: boolean,
): FiberRoot {
// 1、创建了一个 RootFiber
const uninitializedFiber = createHostRootFiber(isConcurrent);
// 2、互相引用
// RootFiber.stateNode --> FiberRoot
// FiberRoot.current --> RootFiber
let root;
root = {
current: uninitializedFiber,
containerInfo: containerInfo,
// ......
}
uninitializedFiber.stateNode = root;
// 3、return 了这个 FiberRoot
return ((root: any): FiberRoot);
}
FiberRoot
这里牵扯到两种 react 中的数据结构,第一个 FiberRoot,也就是上面 createFiberRoot 函数返回的对象。
type BaseFiberRootProperties = {|
// root 节点,render 方法接收的第二个参数
containerInfo: any,
// 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom 不会用到
pendingChildren: any,
// 当前应用对应的 Fiber 对象,是 Root Fiber
current: Fiber,
// 一下的优先级是用来区分
// 1) 没有提交 (committed) 的任务
// 2) 没有提交的挂起任务
// 3) 没有提交的可能被挂起的任务
// 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
// The earliest and latest priority levels that are suspended from committing.
// 最老和新的在提交的时候被挂起的任务
earliestSuspendedTime: ExpirationTime,
latestSuspendedTime: ExpirationTime,
// The earliest and latest priority levels that are not known to be suspended.
// 最老和最新的不确定是否会挂起的优先级(所有任务进来一开始都是这个状态)earliestPendingTime: ExpirationTime,
latestPendingTime: ExpirationTime,
// The latest priority level that was pinged by a resolved promise and can
// be retried.
// 最新的通过一个 promise 被 reslove 并且可以重新尝试的优先级
latestPingedTime: ExpirationTime,
// 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
// 在 `renderRoot` 出现无法处理的错误时会被设置为 `true`
didError: boolean,
// 正在等待提交的任务的 `expirationTime`
pendingCommitExpirationTime: ExpirationTime,
// 已经完成的任务的 FiberRoot 对象,如果你只有一个 Root,那他永远只可能是这个 Root 对应的 Fiber,或者是 null
// 在 commit 阶段只会处理这个值对应的任务
finishedWork: Fiber | null,
// 在任务被挂起的时候通过 setTimeout 设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的 timeout
timeoutHandle: TimeoutHandle | NoTimeout,
// 顶层 context 对象,只有主动调用 `renderSubtreeIntoContainer` 时才会有用
context: Object | null,
pendingContext: Object | null,
// 用来确定第一次渲染的时候是否需要融合
+hydrate: boolean,
// 当前 root 上剩余的过期时间
// TODO: 提到 renderer 里面区处理
nextExpirationTimeToWorkOn: ExpirationTime,
// 当前更新对应的过期时间
expirationTime: ExpirationTime,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
// 顶层批次(批处理任务?)这个变量指明一个 commit 是否应该被推迟
// 同时包括完成之后的回调
// 貌似用在测试的时候?firstBatch: Batch | null,
// root 之间关联的链表结构
nextScheduledRoot: FiberRoot | null,
|};
Fiber
这里就是 createHostRootFiber 函数返回的 fiber 对象。注意这里其实每一个节点都对应一个 fiber 对象,不是 Root 专有的哦。
// Fiber 对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个 Fiber
type Fiber = {|
// 标记不同的组件类型
// export const FunctionComponent = 0;
// export const ClassComponent = 1;
// export const IndeterminateComponent = 2; // Before we know whether it is function or class
// export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
// export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
// export const HostComponent = 5;
// export const HostText = 6;
// export const Fragment = 7;
// export const Mode = 8;
// export const ContextConsumer = 9;
// export const ContextProvider = 10;
// export const ForwardRef = 11;
// export const Profiler = 12;
// export const SuspenseComponent = 13;
// export const MemoComponent = 14;
// export const SimpleMemoComponent = 15;
// export const LazyComponent = 16;
// export const IncompleteClassComponent = 17;
tag: WorkTag,
// ReactElement 里面的 key
key: null | string,
// ReactElement.type,标签类型,也就是我们调用 `createElement` 的第一个参数
elementType: any,
// The resolved function/class/ associated with this fiber.
// 异步组件 resolved 之后返回的内容,一般是 `function` 或者 `class`
type: any,
// The local state associated with this fiber.
// 跟当前 Fiber 相关本地状态(比如浏览器环境就是 DOM 节点)stateNode: any,
// 指向他在 Fiber 节点树中的 `parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,
// 单链表树结构
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构
// 兄弟节点的 return 指向同一个父节点
sibling: Fiber | null,
index: number,
// ref 属性
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// 新的变动带来的新的 props
pendingProps: any,
// 上一次渲染完成之后的 props
memoizedProps: any,
// 该 Fiber 对应的组件产生的 Update 会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的时候的 state
memoizedState: any,
// 一个列表,存放这个 Fiber 依赖的 context
firstContextDependency: ContextDependency<mixed> | null,
// 用来描述当前 Fiber 和他子树的 `Bitfield`
// 共存的模式表示这个子树是否默认是异步渲染的
// Fiber 被创建的时候他会继承父 Fiber
// 其他的标识也可以在创建的时候被设置
// 但是在创建之后不应该再被修改,特别是他的子 Fiber 创建之前
mode: TypeOfMode,
// Effect
// 用来记录 Side Effect
// Don't change these two values. They're used by React Dev Tools.
// export const NoEffect = /* */ 0b00000000000;
// export const PerformedWork = /* */ 0b00000000001;
// You can change the rest (and add more).
// export const Placement = /* */ 0b00000000010;
// export const Update = /* */ 0b00000000100;
// export const PlacementAndUpdate = /* */ 0b00000000110;
// export const Deletion = /* */ 0b00000001000;
// export const ContentReset = /* */ 0b00000010000;
// export const Callback = /* */ 0b00000100000;
// export const DidCapture = /* */ 0b00001000000;
// export const Ref = /* */ 0b00010000000;
// export const Snapshot = /* */ 0b00100000000;
// Update & Callback & Ref & Snapshot
// export const LifecycleEffectMask = /* */ 0b00110100100;
// Union of all host effects
// export const HostEffectMask = /* */ 0b00111111111;
// export const Incomplete = /* */ 0b01000000000;
// export const ShouldCapture = /* */ 0b10000000000;
effectTag: SideEffectTag,
// 单链表用来快速查找下一个 side effect
nextEffect: Fiber | null,
// 子树中第一个 side effect
firstEffect: Fiber | null,
// 子树中最后一个 side effect
lastEffect: Fiber | null,
// 代表任务在未来的哪个时间点应该被完成
// 不包括他的子树产生的任务
expirationTime: ExpirationTime,
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// 在 Fiber 树更新的过程中,每个 Fiber 都会有一个跟其对应的 Fiber
// 我们称他为 `current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,
// 下面是调试相关的,收集每个 Fiber 和子树渲染时间的
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
|};
root.render()
经过上面的步骤,创建好了 ReactRoot。初始化完成了。下面开始 root.render。
我们回到 legacyRenderSubtreeIntoContainer 函数,前面一堆讲解的是调用 legacyCreateRootFromDOMContainer 方法我们得到了一个 ReactRoot 对象。reactRoot 的原型上面我们找到了 render 方法:
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,): Work {
// 这个就是我们上面创建的 FiberRoot 对象
const root = this._internalRoot;
// ......
DOMRenderer.updateContainer(children, root, null, work._onCommit);
return work;
};
updateContainer
这个函数里面使用了 currentTime 和 expirationTime, currentTime 是用来计算 expirationTime 的,expirationTime 代表着优先级,这个留在后续分析。后续紧接着调用了 updateContainerAtExpirationTime。
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
// 这个 current 就是 FiberRoot 对应的 RootFiber
const current = container.current;
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, current);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback,
);
}
注:这个函数在 ReactFiberReconciler.js 里面。
updateContainerAtExpirationTime
这里将 current(即 Fiber 实例)提取出来,并作为参数传入调用 scheduleRootUpdate
export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
expirationTime: ExpirationTime,
callback: ?Function,
) {
const current = container.current;
// ......
return scheduleRootUpdate(current, element, expirationTime, callback);
}
scheduleRootUpdate
这个函数主要执行了两个操作:
1、创建更新 createUpdate 并放到更新队列 enqueueUpdate,创建更新的具体细节稍后再讲哈。因为待会我们会发现其他地方也用到了。
2、个是执行 sheculeWork 函数,进入 React 异步渲染的核心:React Scheduler,这个我后续文章详细讲解。
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
callback: ?Function,
) {
// ......
// 1、创建一个 update 对象
const update = createUpdate(expirationTime);
update.payload = {element};
// ......
// 2、将刚创建的 update 对象入队到 fiber.updateQueue 队列中
enqueueUpdate(current, update);
// 3、开始进入 React 异步渲染的核心:React Scheduler
scheduleWork(current, expirationTime);
return expirationTime;
}
图解
以上的过程我画了张图:
二、setState
虽然我还没有讲解到 class component 的渲染过程,但是这个不影响我现在要讨论的内容~
如下我们调用 this.setState 方法的时候,调用了 this.updater.enqueueSetState
Component.prototype.setState = function(partialState, callback) {this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
先不管 this.updater 什么时候被赋值的,直接看到 ReactFiberClassComponent.js 中的 enqueueSetState,这就是我们调用 setState 执行的 enqueueSetState 方法。
const classComponentUpdater = {
// ......
enqueueSetState(inst, payload, callback) {
// inst 就是我们调用 this.setState 的 this,也就是 classComponent 实例
// 获取到当前实例上的 fiber
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
// 计算当前 fiber 的到期时间(优先级)const expirationTime = computeExpirationForFiber(currentTime, fiber);
// 创建更新一个更新 update
const update = createUpdate(expirationTime);
//payload 是 setState 传进来的要更新的对象
update.payload = payload;
//callback 就是 setState({},()=>{})的回调函数
if (callback !== undefined && callback !== null) {if (__DEV__) {warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
// 把更新放到队列 UpdateQueue
enqueueUpdate(fiber, update);
// 开始进入 React 异步渲染的核心:React Scheduler
scheduleWork(fiber, expirationTime);
},
// ......
}
看到上面的代码,是不是发现和上面 ReactDOM.render 中 scheduleRootUpdate 非常的相似。其实他们就是同一个更新原理呢~
三、forceUpdate
废话不多说,先上代码。也是在 ReactFiberClassComponent.js 中 classComponentUpdater 对象中。
const classComponentUpdater = {
// ......
enqueueForceUpdate(inst, callback) {const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
// 与 setState 不同的地方
// 默认是 0 更新,需要改成 2 强制更新
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {if (__DEV__) {warnOnInvalidCallback(callback, 'forceUpdate');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
// ......
}
看到代码的我们很开心,简直就是 enqueueSetState 的孪生兄弟。我就不详说啦。
到这里我们总结一下上面三种更新的流程:
(1)获取节点对应的 fiber 对象
(2)计算 currentTime
(3)根据(1)fiber 和(2)currentTime 计算 fiber 对象的 expirationTime
(4)根据(3)expirationTime 创建 update 对象
(5)将 setState 中要更新的对象赋值到(4)update.payload,ReactDOM.render 是{element}
(6)将 callback 赋值到(4)update.callback
(7)update 入队 updateQueue
(8)进行任务调度
四、update 对象
上面三种创建更新的方式中都创建了一个叫 update 的对象。那这个对象里面到底是什么呢?充满好奇的我们点开 createUpdate 函数瞧瞧:
export function createUpdate(expirationTime: ExpirationTime): Update<*> {
return {
// 过期时间
expirationTime: expirationTime,
// export const UpdateState = 0;
// export const ReplaceState = 1;
// export const ForceUpdate = 2;
// export const CaptureUpdate = 3;
// 指定更新的类型,值为以上几种
// 提下 CaptureUpdate,在 React16 后有一个 ErrorBoundaries 功能
// 即在渲染过程中报错了,可以选择新的渲染状态(提示有错误的状态),来更新页面
// 0 更新 1 替换 2 强制更新 3 捕获性的更新
tag: UpdateState,
// 更新内容,比如 `setState` 接收的第一个参数
// 第一次渲染 ReactDOM.render 接收的是 payload = {element};
payload: null,
// 更新完成后对应的回调,`setState`,`render` 都有
callback: null,
// 指向下一个更新
next: null,
// 指向下一个 `side effect`,这块内容后续讲解
nextEffect: null,
};
}
就是返回了个简单的对象。对象每个属性的解释我都写在上面了。
五、UpdateQueue
UpdateQueue 是一个单向链表,用来存放 update。每个 update 用 next 连接。它的结构如下:
// 创建更新队列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
// 应用更新后的 state
// 每次的更新都是在这个 baseState 基础上进行更新
baseState,
// 队列中的第一个 update
firstUpdate: null,
// 队列中的最后一个 update
lastUpdate: null,
// 队列中第一个捕获类型的 update
firstCapturedUpdate: null,
// 队列中最后一个捕获类型的 update
lastCapturedUpdate: null,
// 第一个 side effect
firstEffect: null,
// 最后一个 side effect
lastEffect: null,
// 第一个和最后一个捕获产生的 `side effect`
firstCapturedEffect: null,
lastCapturedEffect: null,
};
return queue;
}
六、enqueueUpdate
创建了 update 对象之后,紧接着调用了 enqueueUpdate,把 update 对象放到队列 enqueueUpdate。同时保证 current 和 workInProgress 的 updateQueue 是一致的,即 fiber.updateQueue 和 fiber.alternate.updateQueue 保持一致。
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// 保证 current 和 workInProgress 的 updateQueue 是一致的
// alternate 即 workInProgress
const alternate = fiber.alternate;
// current 的队列
let queue1;
// alternate 的队列
let queue2;
// 如果 alternate 为空
if (alternate === null) {
// There's only one fiber.
queue1 = fiber.updateQueue;
queue2 = null;
// 如果 queue1 仍为空,则初始化更新队列
if (queue1 === null) {queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// 如果 alternate 不为空,则取各自的更新队列
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {if (queue2 === null) {
// 初始化
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState,);
} else {
// 如果 queue2 存在但 queue1 不存在的话,则根据 queue2 复制 queue1
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {if (queue2 === null) {
// Only one fiber has an update queue. Clone to create a new one.
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {// Both owners have an update queue.}
}
}
if (queue2 === null || queue1 === queue2) {
// 将 update 放入 queue1 中
appendUpdateToQueue(queue1, update);
} else {
// 两个队列共享的是用一个 update
// 如果两个都是空队列,则添加 update
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// 如果两个都不是空队列,由于两个结构共享,所以只在 queue1 加入 update
// 在 queue2 中,将 lastUpdate 指向 update
appendUpdateToQueue(queue1, update);
queue2.lastUpdate = update;
}
}
总结上面过程:
(1)queue1 取的是 fiber.updateQueue;
queue2 取的是 alternate.updateQueue
(2)如果两者均为 null,则调用 createUpdateQueue()获取初始队列
(3)如果两者之一为 null,则调用 cloneUpdateQueue() 从对方中获取队列
(4)如果两者均不为 null,则将 update 作为 lastUpdate
注:两个队列共享的是同一个 update。
七、scheduleWork
上面三种更新最后都调用了 scheduleWork(fiber, expirationTime)进入 React 异步渲染的核心:React Scheduler。后续文章详细讲解。
任世界纷繁复杂, 仍旧保持可爱。
我是小柚子小仙女。文章如有不妥,欢迎指正~