引言

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在某些程度上相当于componentDidMountcomponentDidUpdatecomponentWillUnmount三个钩子函数的集合,因为这些函数都会阻塞浏览器的渲染,其中componentDidMountcomponentDidUpdate的执行是在哪里呢,看一下上面提到的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();
}
}

参考资料

福禄ICH·架构组
福袋

React的useEffect与useLayoutEffect执行机制剖析的更多相关文章

  1. useEffect 和 useLayoutEffect浅析

    执行时期的区别 useEffect 回调函数的执行时期 useEffect为异步执行,执行时期为 触发状态更新(如:setState,forceUpdate) React渲染函数执行(render) ...

  2. C#进阶系列——WebApi 路由机制剖析:你准备好了吗?

    前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: ...

  3. Java反射机制剖析(四)-深度剖析动态代理原理及总结

    动态代理类原理(示例代码参见java反射机制剖析(三)) a)  理解上面的动态代理示例流程 a)  理解上面的动态代理示例流程 b)  代理接口实现类源代码剖析 咱们一起来剖析一下代理实现类($Pr ...

  4. Jedis cluster命令执行流程剖析

    Jedis cluster命令执行流程剖析 在Redis Cluster集群模式下,由于key分布在各个节点上,会造成无法直接实现mget.sInter等功能.因此,无论我们使用什么客户端来操作Red ...

  5. 【C#】 WebApi 路由机制剖析

    C#进阶系列——WebApi 路由机制剖析:你准备好了吗? 转自:https://blog.csdn.net/wulex/article/details/71601478 2017年05月11日 10 ...

  6. 【THE LAST TIME】彻底吃透 JavaScript 执行机制

    前言 The last time, I have learned [THE LAST TIME]一直是我想写的一个系列,旨在厚积薄发,重温前端. 也是给自己的查缺补漏和技术分享. 欢迎大家多多评论指点 ...

  7. React中useEffect使用

    2019-08-24 07:00:00 文摘资讯 阅读数 1364  收藏 博文的原始地址     之前我们已经掌握了useState的使用,在 class 中,我们通过在构造函数中设置 this.s ...

  8. C#进阶系列——WebApi 路由机制剖析:你准备好了吗? 转载https://www.cnblogs.com/landeanfen/p/5501490.html

    阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...

  9. WebApi 路由机制剖析

    阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...

随机推荐

  1. Oracle版本发布规划 (文档 ID 742060.1)

    Oracle Database Release Schedule of Current Database Releases (文档 ID 742060.1) Oracle Database RoadM ...

  2. ASP.Net Core 3.1 With Autofac ConfigureServices returning an System.IServiceProvider isn't supported.

    ASP.Net Core 3.1 With Autofac ConfigureServices returning an System.IServiceProvider isn't supported ...

  3. 【JVM之内存与垃圾回收篇】方法区

    方法区 前言 这次所讲述的是运行时数据区的最后一个部分 从线程共享与否的角度来看 ThreadLocal:如何保证多个线程在并发环境下的安全性?典型应用就是数据库连接管理,以及会话管理 栈.堆.方法区 ...

  4. tomcat 认证爆破之custom iterator使用

    众所周知,BurpSuite是渗透测试最基本的工具,也可是神器,该神器有非常之多的模块:反正,每次翻看大佬们使用其的骚操作感到惊叹,这次我用其爆破模块的迭代器模式来练练手[不喜勿喷] 借助vulhub ...

  5. 【管理员已阻止你运行此应用】windows defender图标打叉,无法打开mmc.exe解决办法

    今天开机遇到一个奇怪的问题,发现windows defender图标上面打了个×: 打开按照系统提示需要restart服务,但是无法重启服务,会出现错误,然后尝试手动重启服务,准备打开管理控制台mmc ...

  6. Python语言及其应用PDF高清完整版免费下载|百度云盘|Python新手入门

    百度云盘:Python语言及其应用PDF高清完整版免费下载 提取码:6or6 内容简介 本书介绍Python 语言的基础知识及其在各个领域的具体应用,基于最新版本3.x.书中首先介绍了Python 语 ...

  7. 大数据篇:一文读懂@数据仓库(PPT文字版)

    大数据篇:一文读懂@数据仓库 1 网络词汇总结 1.1 数据中台 数据中台是聚合和治理跨域数据,将数据抽象封装成服务,提供给前台以业务价值的逻辑概念. 数据中台是一套可持续"让企业的数据用起 ...

  8. 《吊打面试官》系列-Redis基础知识

    前言Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联网公司面一次拿一次offer的面霸(请允许我使用一下 ...

  9. Go 中读取命令参数的几种方法总结

    前言 对于一名初学者来说,想要尽快熟悉 Go 语言特性,所以以操作式的学习方法为主,比如编写一个简单的数学计算器,读取命令行参数,进行数学运算. 本文讲述使用三种方式讲述 Go 语言如何接受命令行参数 ...

  10. Salt组件(一)

    一.管理对象属性(Grains) Grains里面记录着每台Minion的一些常用属性,比如CPU.内存.磁盘.网信息等,我们可以通过grains.items查看某台Minion的所有Grains信息 ...