React的useEffect与useLayoutEffect执行机制剖析
引言
useEffect和useLayoutEffect是React官方推出的两个hooks,都是用来执行副作用的钩子函数,名字类似,功能相近,唯一不同的就是执行的时机有差异,今天这篇文章主要是从这两个钩子函数的执行时机入手,来剖析一下React的运行原理和浏览器的渲染流程。
官方解释
useLayoutEffect其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前, useLayoutEffect 内部的更新计划将被同步刷新,尽可能使用标准的 useEffect 以避免阻塞视觉更新。
简单来讲,就是:useEffect是异步的,useLayoutEffect是同步的,异(同)步是相对于浏览器执行刷新屏幕Task来说的。
眼见为实
下面将通过一个简单的demo示例来说明具体的执行过程,其中React是16.13.1版本,首先是示例代码:
import React, { useState, useEffect, useLayoutEffect } from 'react';
const EffectDemo = () => {
const [count, setCount] = useState(0);
useEffect(function useEffectDemo() {
console.log('useEffect:', count);
}, [count]);
useLayoutEffect(function useLayoutEffectDemo() {
console.log('useLayoutEffect:', count);
}, [count]);
return (
<div>
<button
onClick={() => {
setCount(count + 1);
}}
>click me</button>
</div>
);
};
export default EffectDemo;
功能很简单,就不做界面展示,这里主要是看一下浏览器控制台Performance的监控图:

通过两个hooks的执行图可以看出,useLayoutEffect发生在页面渲染到屏幕(用户可见)之前,useEffect发生在那之后,中间还经历了DCL,FCP,FMP,LCP阶段,除开DCL(DomContentLoaded)之外,这些指标是RAIL模型衡量页面性能的标准,总的来说,渲染到屏幕的阶段是一个分水岭,那么渲染包含什么呢,还是看图吧:

此阶段完成了样式的计算(Recalculate Style)和布局(Layout),紧接着是一个Task,完成Update Layer Tree,Paint,Composite Layers,经过这一系列的任务后,页面最终呈现给用户,可以用一张图来表示浏览器的渲染过程:

后面会有相关学习资料,这里就不展开细说了。
模拟运行示例
在深入了解React的运行之前,首先在本地写一个简单的示例,大致模拟文章开始的例子:
<body>
<div id="app"></div>
<script type="text/javascript">
(function iife(){
function render() {
var appNode = document.querySelector('#app');
var textNode = document.createElement('span');
textNode.id = 'tip';
textNode.textContent = 'hello';
appNode.appendChild(textNode);
}
function useLayoutEffectDemo() {
console.log('useLayoutEffectDemo', document.querySelector('#tip'));
}
function useEffectDemo() {
console.log('useEffectDemo');
}
render();
useLayoutEffectDemo();
setTimeout(useEffectDemo, 0);
})();
</script>
</body>
然后启用Performance监控渲染情况:

总结一下:
1.首先运行render,完成后立即执行useLayoutEffectDemo函数(虽然已经插入DOM,但是界面还没有渲染出来);
2.注册异步回调函数useEffectDemo,该函数将在0ms过后加入EventLoop中的宏任务队列;
3.页面开始渲染:Recalculate Style->Layout->Update Layer Tree->Paint->Composite Layers->GPU绘制;
4.取出宏任务useEffectDemo,执行回调;
React的执行比这个模拟示例复杂很多,但是抽象出的流程节点大同小异,了解之后,我们可以继续深入挖掘React的运行机制了。
React运行原理
React渲染页面分为两个阶段:
1.调度阶段(reconciliation):找出需要更新的节点元素
2.渲染阶段(commit):将需要更新的元素插入DOM
接下来就跟着React的运行流程来具体看下不同阶段的执行情况:
渲染流程图(初次渲染)

简单总结一下:
1.react-dom负责Fiber节点的创建,最终形成一个Fiber节点树,其中每个Fiber包含需要执行的副作用和渲染到屏幕的DOM对象;
2.调用scheduler暴露的方法注册需要调度的事件;
3.执行DOM插入;
4.执行useLyaoutEffect或者ClassComponent的生命周期函数;
5.浏览器接过控制权,执行渲染;
6.scheduler执行调度任务,执行useEffectDemo;
以上就是整体流程,接下来再深入一点,看看useEffect和useLayoutEffect是怎么解析和执行的:

use(Layout)Effect解析与执行
1.解析

从上图可知,uesEffect和useLayoutEffect最终都会调用mountEffectImpl函数,然后初始化/更新Fiber的updateQueue,可以看一下mountEffectImpl函数是怎样的:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber$1.effectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
}
都认识,但是不知道是干嘛的,好吧,还是用一张图来说明吧:

这个函数的功能如下:
1.创建hook对象,放入到workInProgressHook链表中;
2.Fiber的updateQueue和上一步创建的hook关联,这样每一个Fiber对象上就知道要执行Effect了;
那么workInProgressHook是干嘛的呢,看下源代码的解释吧:
var workInProgressHook = null; // Whether an update was scheduled at any point during the render phase. This
// does not get reset if we do another render pass; only when we're completely
// finished evaluating this component. This is an optimization so we know
// whether we need to clear render phase updates after a throw.
2.updateQueue数据结构
上面说到updateQueue,最终我们写的useEffectDemo和useLayoutEffectDemo都会放在这里,那么是怎么一个结构存储的呢,可以打印看一下:

其实就是一个收尾相连的环形结构,为什么要这么设计呢,大家看下commitHookEffectListMount执行函数的遍历方式就知道了:
function commitHookEffectListMount(tag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Mount
var create = effect.create;
effect.destroy = create();
{
var destroy = effect.destroy;
if (destroy !== undefined && typeof destroy !== 'function') {
var addendum = void 0;
if (destroy === null) {
addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
} else if (typeof destroy.then === 'function') {
addendum = '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching';
} else {
addendum = ' You returned: ' + destroy;
}
error('An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s%s', addendum, getStackByFiberInDevAndProd(finishedWork));
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
这里根据effect的tag不同决定执行哪一种effect,这里我们的useEffectDemo和useLayoutEfectDemo的tag分别是5和3,因此需要执行useEffect中的副作用函数时,commitHookEffectListMount的tag肯定就是5了,执行useLayoutEffect中的副作用函数时,commitHookEffectListMount的tag肯定就是3。
总的来说所有的useEffect和useLayoutEffect的副作用函数都是在这里执行的,通过tag来控制他们的执行时机。
3.执行
其实上面已经讲了commitHookEffectListMount的执行,这里再看下具体的执行过程:

执行useEffect的入口:
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
commitHookEffectListMount(Layout | HasEffect, finishedWork);
return;
}
......
}
执行useLayoutEffect的入口:
function commitPassiveHookEffects(finishedWork) {
if ((finishedWork.effectTag & Passive) !== NoEffect) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
......
commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);
break;
}
}
}
}
可以看出两个执行入口传入的第一个入参tag是不一样的,最终执行的副作用函数就区分开来了。
MessageChannel异步调度
现在大家应该对useEffect和useLayoutEffect的执行有了一个大致的了解,那么还有一个关于scheduler异步调度的小问题,本文最开始模拟的一个例子里是通过setTimeout来完成的,React中则是通过MessageChannel来实现的,如果不熟悉可以查查使用方式,这里来看下异步执行的过程:

浏览器渲染流程
- 关于浏览器的渲染这里我就以推荐学习资料为主,因为我自己也没有这些讲解得好,就没必要重复了;
基础知识
浏览器的渲染是一个十分复杂的过程,如果不是很了解,可以浏览谷歌提供的介绍文章,链接如下:https://developers.google.cn/web/fundamentals/performance/rendering
深入一点
了解了浏览器的基本渲染之后,可以更加深入窥探浏览器的运行,首先上一张图:

上面这幅图是来源于https://aerotwist.com/blog/the-anatomy-of-a-frame
这里还给大家推荐一篇讲解浏览器渲染的文章:https://juejin.im/entry/6844903476506394638
其他生命周期函数
在学习Hooks的时候,难免会和class组件中的生命周期做比较,这里我们只关注useEffect,useEffect在某些程度上相当于componentDidMount 、 componentDidUpdate 、 componentWillUnmount三个钩子函数的集合,因为这些函数都会阻塞浏览器的渲染,其中componentDidMount 、 componentDidUpdate的执行是在哪里呢,看一下上面提到的commitLifeCycles函数就清楚了(componentWillUnmount大家有兴趣自己找找吧);
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
commitHookEffectListMount(Layout | HasEffect, finishedWork);
return;
}
case ClassComponent:
{
var instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) { // 初次渲染
......
instance.componentDidMount();
stopPhaseTimer();
} else { // 更新渲染
......
instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
stopPhaseTimer();
}
}
参考资料
- https://mp.weixin.qq.com/s/of1ulUPtz7c8Evc9A8cYdw
- https://developers.google.cn/web/fundamentals/performance/rendering
- https://juejin.im/entry/6844903476506394638
- https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/
- https://blog.csdn.net/frontend_frank/article/details/107273939
福袋
React的useEffect与useLayoutEffect执行机制剖析的更多相关文章
- useEffect 和 useLayoutEffect浅析
执行时期的区别 useEffect 回调函数的执行时期 useEffect为异步执行,执行时期为 触发状态更新(如:setState,forceUpdate) React渲染函数执行(render) ...
- C#进阶系列——WebApi 路由机制剖析:你准备好了吗?
前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: ...
- Java反射机制剖析(四)-深度剖析动态代理原理及总结
动态代理类原理(示例代码参见java反射机制剖析(三)) a) 理解上面的动态代理示例流程 a) 理解上面的动态代理示例流程 b) 代理接口实现类源代码剖析 咱们一起来剖析一下代理实现类($Pr ...
- Jedis cluster命令执行流程剖析
Jedis cluster命令执行流程剖析 在Redis Cluster集群模式下,由于key分布在各个节点上,会造成无法直接实现mget.sInter等功能.因此,无论我们使用什么客户端来操作Red ...
- 【C#】 WebApi 路由机制剖析
C#进阶系列——WebApi 路由机制剖析:你准备好了吗? 转自:https://blog.csdn.net/wulex/article/details/71601478 2017年05月11日 10 ...
- 【THE LAST TIME】彻底吃透 JavaScript 执行机制
前言 The last time, I have learned [THE LAST TIME]一直是我想写的一个系列,旨在厚积薄发,重温前端. 也是给自己的查缺补漏和技术分享. 欢迎大家多多评论指点 ...
- React中useEffect使用
2019-08-24 07:00:00 文摘资讯 阅读数 1364 收藏 博文的原始地址 之前我们已经掌握了useState的使用,在 class 中,我们通过在构造函数中设置 this.s ...
- C#进阶系列——WebApi 路由机制剖析:你准备好了吗? 转载https://www.cnblogs.com/landeanfen/p/5501490.html
阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...
- WebApi 路由机制剖析
阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...
随机推荐
- jquery文件表单上传
1. 引入jquery文件 <script src="js/jquery-2.1.1.min.js"></script> 2. 创建form表单,如下: ...
- T133309 57级返校测试重测-T2-选票统计
大致题意: 找出个数超过n/4的数们. 基本思路: 一开始我是用map做的,然后就很玄学的TLE了. 啊,那就换个法吧. 先排个序,然后一样的数就在一起了, 再然后直接从前往后遍历一下,就能得出结果了 ...
- Spring当中循环依赖很少有人讲,今天一起来学习!
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
- 图灵学院笔记-java虚拟机底层原理
Table of Contents generated with DocToc 一.java虚拟机概述 二.栈内存解析 2.1 概述 2.2 栈帧内部结构 2.2.1 我们来解析一下compute() ...
- Python协程之Gevent模块
背景 进程是操作系统分配资源的最小单位,每个进程独享4G的内存地址空间,因此进程内数据是安全的,检查间的通信需要使用特定的方法.同理,正是因为进程是数据安全的,所以导致进程的切换是一个很麻烦效率不高的 ...
- POJ2774 --后缀树解法
POJ2774 Long Long Message --后缀树解法 原题链接 题意明确说明求两字符串的最长连续公共子串,可用字符串hash或者后缀数据结构来做 关于后缀树 后缀树的原理较为简单,但 \ ...
- H5移动端,ios从后台返回到app,页面会白一下
visibilitychange事件可以检查从后台返回事件,然后通过添加div,强制浏览器刷新页面 var divEle = document.createElement("DIV" ...
- 解决react使用antd table组件固定表头后,表头和表体列不对齐以及配置fixed固定左右侧后行高度不对齐
一.固定表头后表体列和表头不对齐 此问题可能在antd3.24.0版本之前都存在,反正3.16.2版本是存在这个问题的,如果是3.24.0之前的版本估计只能通过修改css样式解决. 按照官网说的: 1 ...
- 今天成功完成二维码扫描程序, 利用zxing
利用的网上参考文档是https://blog.csdn.net/gorky_19/article/details/78454030,里面介绍了如何修改build.gradle的dependency 和 ...
- linux dig 命令使用
linux dig 命令使用方法 2018.04.20 15:47 43101浏览 dig 命令主要用来从 DNS 域名服务器查询主机地址信息. 查询单个域名的 DNS 信息 dig 命令最典型的 ...