博客
关于我
React的useEffect与useLayoutEffect执行机制剖析
阅读量:460 次
发布时间:2019-03-06

本文共 7735 字,大约阅读时间需要 25 分钟。

福禄ICH·架构组
福袋

useEffect与useLayoutEffect深度解析

引言

useEffect和useLayoutEffect是React Hooks中用于执行副作用的两个钩子函数,虽然它们功能相近,但执行时机存在显著差异。本文将从这两个钩子的执行时机入手,剖析React的运行原理和浏览器的渲染流程。

官方解释

useLayoutEffect的函数签名与useEffect相同,但它会在所有DOM变更之后同步调用effect。它主要用于读取DOM布局并触发重渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划会被同步刷新。相比之下,useEffect是异步的,执行时机在浏览器刷新屏幕Task之后。

简单来说,useEffect是异步的,而useLayoutEffect是同步的,这里的异、同步是相对于浏览器执行刷新屏幕Task而言。

眼见为实

通过一个简单的示例来说明具体执行过程。React 16.13.1版本:

import React, { useState, useEffect, useLayoutEffect } from 'react';const EffectDemo = () => {    const [count, setCount] = useState(0);    useEffect(() => {        console.log('useEffect:', count);    }, [count]);    useLayoutEffect(() => {        console.log('useLayoutEffect:', count);    }, [count]);    return (        
);};export default EffectDemo;

通过浏览器控制台Performance监控图可以看出,useLayoutEffect发生在页面渲染到屏幕(用户可见)之前,而useEffect发生在那之后,中间经历了DCL、FCP、FMP、LCP阶段。DCL是DOMContentLoaded,FCP是First Contentful Paint,FMP是First Meaningful Paint,LCP是Largest Contentful Paint。

模拟运行示例

在深入了解React运行之前,先在本地写一个简单的示例,模拟文章开始的例子:

// 例如,一个简单的useEffect和useLayoutEffect示例import React, { useState, useEffect, useLayoutEffect } from 'react';const Example = () => {    const [count, setCount] = useState(0);    useEffect(() => {        console.log('useEffect:', count);    }, [count]);    useLayoutEffect(() => {        console.log('useLayoutEffect:', count);    }, [count]);    return (        
);};export default Example;

启用Performance监控渲染情况:

React运行原理

React渲染页面分为两个阶段:

  • 调度阶段(reconciliation):找出需要更新的节点元素
  • 渲染阶段(commit):将需要更新的元素插入DOM
  • 接下来跟随React的运行流程,具体看不同阶段的执行情况。

    渲染流程图(初次渲染)

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

    use(Layout)Effect解析与执行

    1. 解析

    从上图可知,useEffect和useLayoutEffect最终都会调用mountEffectImpl函数,然后初始化/更新Fiber的updateQueue。看一下mountEffectImpl函数:

    function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {    var hook = mountWorkInProgressHook();    var nextDeps = deps === undefined ? null : deps;    currentlyRenderingFiber.effectTag |= fiberEffectTag;    hook.memoizedState = pushEffect(        HasEffect | hookEffectTag,        create,        undefined,        nextDeps    );}

    这个函数的功能如下:

  • 创建hook对象,放入到workInProgressHook链表中
  • Fiber的updateQueue和上一步创建的hook关联,这样每一个Fiber对象上就知道要执行Effect了
  • 2. updateQueue数据结构

    updateQueue是一个收尾相连的环形结构,用于存储需要执行的副作用。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 = 'It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately: useEffect(() => { async function fetchData() { ... } }, [someId]); // Or [] if effect doesn\'t need props or state. 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和useLayoutEffectDemo的tag分别是5和3,因此需要执行useEffect中的副作用函数时,commitHookEffectListMount的tag肯定是5,执行useLayoutEffect中的副作用函数时,commitHookEffectListMount的tag肯定是3。

    总的来说,所有的useEffect和useLayoutEffect的副作用函数都是在这里执行的,通过tag来控制他们的执行时机。

    3. 执行

    其实上面已经讲了commitHookEffectListMount的执行,这里再看下具体的执行过程:

    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();                }            }    }}

    可以看出,useLayoutEffect的入口是commitHookEffectListMount(Layout | HasEffect, finishedWork),而useEffect的入口是commitHookEffectListMount(HasEffect, finishedWork)。

    MessageChannel异步调度

    现在对useEffect和useLayoutEffect的执行有了大致的了解,那么还有一个关于scheduler异步调度的问题。本文最开始的例子中是通过setTimeout来完成的,而React中则是通过MessageChannel来实现的。如果不熟悉可以查查使用方式,这里来看下异步执行的过程。

    浏览器渲染流程

    关于浏览器的渲染这里我就以推荐学习资料为主,因为我自己也没有这些讲解得好,就没必要重复了。

    浏览器渲染基础知识

    浏览器的渲染是一个十分复杂的过程,可以参考Google提供的介绍文章: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在某些程度上相当于componentDidMount、componentDidUpdate、componentWillUnmount这三个钩子函数的集合,因为这些函数都会阻塞浏览器的渲染。其中componentDidMount、componentDidUpdate的执行是在哪里呢,可以看一下上面提到的commitLifeCycles函数:

    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
  • 你可能感兴趣的文章
    mysql ansi nulls_SET ANSI_NULLS ON SET QUOTED_IDENTIFIER ON 什么意思
    查看>>
    multi swiper bug solution
    查看>>
    MySQL Binlog 日志监听与 Spring 集成实战
    查看>>
    MySQL binlog三种模式
    查看>>
    multi-angle cosine and sines
    查看>>
    Mysql Can't connect to MySQL server
    查看>>
    mysql case when 乱码_Mysql CASE WHEN 用法
    查看>>
    Multicast1
    查看>>
    mysql client library_MySQL数据库之zabbix3.x安装出现“configure: error: Not found mysqlclient library”的解决办法...
    查看>>
    MySQL Cluster 7.0.36 发布
    查看>>
    Multimodal Unsupervised Image-to-Image Translation多通道无监督图像翻译
    查看>>
    MySQL Cluster与MGR集群实战
    查看>>
    multipart/form-data与application/octet-stream的区别、application/x-www-form-urlencoded
    查看>>
    mysql cmake 报错,MySQL云服务器应用及cmake报错解决办法
    查看>>
    Multiple websites on single instance of IIS
    查看>>
    mysql CONCAT()函数拼接有NULL
    查看>>
    multiprocessing.Manager 嵌套共享对象不适用于队列
    查看>>
    multiprocessing.pool.map 和带有两个参数的函数
    查看>>
    MYSQL CONCAT函数
    查看>>
    multiprocessing.Pool:map_async 和 imap 有什么区别?
    查看>>