转:

React源码 commit阶段详解

点击进入React源码调试仓库。

当render阶段完成后,意味着在内存中构建的workInProgress树所有更新工作已经完成,这包括树中fiber节点的更新、diff、effectTag的标记、effectList的收集。此时workInProgress树的完整形态如下:

和current树相比,它们的结构上固然存在区别,变化的fiber节点也存在于workInProgress树,但要将这些节点应用到DOM上却不会循环整棵树,而是通过循环effectList这个链表来实现,这样保证了只针对有变化的节点做工作。

所以循环effectList链表去将有更新的fiber节点应用到页面上是commit阶段的主要工作。

commit阶段的入口是commitRoot函数,它会告知scheduler以立即执行的优先级去调度commit阶段的工作。

function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}

scheduler去调度的是commitRootImpl,它是commit阶段的核心实现,整个commit阶段被划分成三个部分。

commit流程概览

commit阶段主要是针对root上收集的effectList进行处理。在真正的工作开始之前,有一个准备阶段,主要是变量的赋值,以及将root的effect加入到effectList中。随后开始针对effectList分三个阶段进行工作:

  • before mutation:读取组件变更前的状态,针对类组件,调用getSnapshotBeforeUpdate,让我们可以在DOM变更前获取组件实例的信息;针对函数组件,异步调度useEffect。
  • mutation:针对HostComponent,进行相应的DOM操作;针对类组件,调用componentWillUnmount;针对函数组件,执行useLayoutEffect的销毁函数。
  • layout:在DOM操作完成后,读取组件的状态,针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对函数组件填充useEffect 的 effect执行数组,并调度useEffect

before mutation和layout针对函数组件的useEffect调度是互斥的,只能发起一次调度

workInProgress 树切换到current树的时机是在mutation结束后,layout开始前。这样做的原因是在mutation阶段调用类组件的componentWillUnmount的时候,
还可以获取到卸载前的组件信息;在layout阶段调用componentDidMount/Update时,获取的组件信息更新后的。

function commitRootImpl(root, renderPriorityLevel) {

  // 进入commit阶段,先执行一次之前未执行的useEffect
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null); // 准备阶段----------------------------------------------- const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes; root.callbackNode = null;
root.callbackId = NoLanes; // effectList的整理,将root上的effect连到effectList的末尾
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
} // 准备阶段结束,开始处理effectList
if (firstEffect !== null) { ... // before mutation阶段--------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null); ... // mutation阶段---------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null); // 将wprkInProgress树切换为current树
root.current = finishedWork; // layout阶段-----------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null); nextEffect = null; // 通知浏览器去绘制
requestPaint(); } else {
// 没有effectList,直接将wprkInProgress树切换为current树
root.current = finishedWork; } const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; // 获取尚未处理的优先级,比如之前被跳过的任务的优先级
remainingLanes = root.pendingLanes;
// 将被跳过的优先级放到root上的pendingLanes(待处理的优先级)上
markRootFinished(root, remainingLanes); /*
* 每次commit阶段完成后,再执行一遍ensureRootIsScheduled,确保是否还有任务需要被调度。
* 例如,高优先级插队的更新完成后,commit完成后,还会再执行一遍,保证之前跳过的低优先级任务
* 重新调度
*
* */
ensureRootIsScheduled(root, now()); ... return null;
}

下面的部分,是对这三个阶段分别进行的详细讲解。

before Mutation

beforeMutation阶段的入口函数是commitBeforeMutationEffects

nextEffect = firstEffect;
do {
try {
commitBeforeMutationEffects();
} catch (error) {
...
}
} while (nextEffect !== null);

它的作用主要是调用类组件的getSnapshotBeforeUpdate,针对函数组件,异步调度useEffect。

function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate; ... const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
// 通过commitBeforeMutationEffectOnFiber调用getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber(current, nextEffect);
} if ((flags & Passive) !== NoFlags) {
// 异步调度useEffect
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}

commitBeforeMutationEffectOnFiber代码如下

function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
...
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// 调用getSnapshotBeforeUpdate
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
// 将返回值存储在内部属性上,方便componentDidUpdate获取
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
...
} }

mutation

mutation阶段会真正操作DOM节点,涉及到的操作有增、删、改。入口函数是commitMutationEffects

    nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
...
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);

由于过程较为复杂,所以我写了三篇文章来说明这三种DOM操作,如果想要了解细节,可以看一下。文章写于17还未正式发布的时候,所以里面的源码版本取自17.0.0-alpha0。

React和DOM的那些事-节点新增算法

React和DOM的那些事-节点删除算法

React和DOM的那些事-节点更新

layout阶段

layout阶段的入口函数是commitLayoutEffects

nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
...
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);

我们只关注classComponent和functionComponent。针对前者,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对后者,填充useEffect 的 effect执行数组,并调度useEffect(具体的原理在我的这篇文章:梳理useEffect和useLayoutEffect的原理与区别中有讲解)。

function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的创建
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // 填充useEffect的effect执行数组
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// 如果是初始挂载阶段,调用componentDidMount
instance.componentDidMount();
} else {
// 如果是更新阶段,调用componentDidUpdate
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState; instance.componentDidUpdate(
prevProps,
prevState,
// 将getSnapshotBeforeUpdate的结果传入
instance.__reactInternalSnapshotBeforeUpdate,
);
}
} // 调用setState的回调
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) { commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
} ... }
}

总结

commit阶段将effectList的处理分成三个阶段保证了不同生命周期函数的适时调用。相对于同步执行的useEffectLayout,useEffect的异步调度提供了一种不阻塞页面渲染的副作用操作入口。另外,标记root上还未处理的优先级和调用ensureRootIsScheduled使得被跳过的低优先级任务得以再次被调度。commit阶段的完成,也就意味着本次更新已经结束。

欢迎扫码关注公众号,发现更多技术文章

转:

React源码 commit阶段详解

React源码 commit阶段详解的更多相关文章

  1. Android源码下载方法详解

    转自:http://www.cnblogs.com/anakin/archive/2011/12/20/2295276.html Android源码下载方法详解 相信很多下载过内核的人都对这个很熟悉 ...

  2. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  3. 【转】ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    原文地址:http://blog.csdn.net/a396901990/article/details/36475213 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量—— ...

  4. Spring Boot源码中模块详解

    Spring Boot源码中模块详解 一.源码 spring boot2.1版本源码地址:https://github.com/spring-projects/spring-boot/tree/2.1 ...

  5. ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量--onMeasure():决定View的大小 2.布局--onLayout():决定View在ViewGroup中的位置 3. ...

  6. 【转】ANDROID自定义视图——onLayout源码 流程 思路详解

    转载(http://blog.csdn.net/a396901990) 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量——onMeasure():决定View的大小 2.布局 ...

  7. vue新手入门之使用vue框架搭建用户登录注册案例,手动搭建webpack+Vue项目(附源码,图文详解,亲测有效)

    前言 本篇随笔主要写了手动搭建一个webpack+Vue项目,掌握相关loader的安装与使用,包括css-loader.style-loader.vue-loader.url-loader.sass ...

  8. Android源码目录结构详解(转载)

    转自:http://blog.csdn.net/xiangjai/article/details/9012387 在学习Android的过程中,学习写应用还好,一开始不用管太多代码,直接调用函数就可以 ...

  9. 最全的Android源码目录结构详解

    Android 2.1|-- Makefile|-- bionic                        (bionic C库)|-- bootable                (启动引 ...

随机推荐

  1. HDU6331 Problem M. Walking Plan【Floyd + 矩阵 + 分块】

    HDU6331 Problem M. Walking Plan 题意: 给出一张有\(N\)个点的有向图,有\(q\)次询问,每次询问从\(s\)到\(t\)且最少走\(k\)条边的最短路径是多少 \ ...

  2. 2019牛客多校 Round5

    Solved:4 Rank:122 补题:8/10 A digits 2 签到 把这个数写n遍 #include <bits/stdc++.h> using namespace std; ...

  3. 2020杭电多校 C / HDU 6879 - Mine Sweeper

    题意: t组输入,每组输入一个s 你需要输出一个r行c列的阵列,这个阵列中'X'代表炸弹,'.'表示没有炸弹 对于'.'这些位置都会有一个数值,这个值取决于这个位置附近8个位置,这8个位置一共有几个炸 ...

  4. python的scrapy框架的使用 和xpath的使用 && scrapy中request和response的函数参数 && parse()函数运行机制

    这篇博客主要是讲一下scrapy框架的使用,对于糗事百科爬取数据并未去专门处理 最后爬取的数据保存为json格式 一.先说一下pyharm怎么去看一些函数在源码中的代码实现 按着ctrl然后点击函数就 ...

  5. Codeforces Round #647 (Div. 2) - Thanks, Algo Muse! A、Johnny and Ancient Computer B、Johnny and His Hobbies C、Johnny and Another Rating Drop

    题目链接:A.Johnny and Ancient Computer 题意: 给你两个数a,b.问你可不可以通过左移位运算或者右移位运算使得它们两个相等.可以的话输出操作次数,不可以输出-1 一次操作 ...

  6. WPF 之命令(七)

    一.前言 ​ 事件的作用是发布和传播一些消息,消息送达接收者,事件的使命也就完成了,至于消息响应者如何处理发送来的消息并不做规定,每个接收者可以使用自己的行为来响应事件.即事件不具有约束力. ​ 命令 ...

  7. mimikatz+procdump 提取 Windows 明文密码

    0x00 原理 获取到内存文件 lsass.exe 进程 (它用于本地安全和登陆策略) 中存储的明文登录密码. 0x01 操作 Windows10/2012 以下的版本:1.上传 procdump 执 ...

  8. 如何使用 js 扩展 prototype 方法

    如何使用 js 扩展 prototype 方法 expand prototype function enhancedLog(msg = ``) { // this.msg = msg; enhance ...

  9. queueMicrotask & microtask

    queueMicrotask & microtask microtask microtask queue Promise Mutation Observer API MutationObser ...

  10. CSS transition & shorthand property order

    CSS transition & shorthand property order shorthand property https://developer.mozilla.org/en-US ...