zoukankan      html  css  js  c++  java
  • 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 = '
    
    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() {
    ' + '    // You can await here
    ' + '    const response = await MyAPI.getData(someId);
    ' + '    // ...
    ' + '  }
    ' + '  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和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·架构组 福袋
  • 相关阅读:
    二叉树中和为某一值的路径
    二叉搜索树的后序遍历序列(important!)
    从上往下打印二叉树
    最小的k个数(important!)
    扑克牌顺子
    栈的压入、弹出序列(important!)
    和为s的连续正数序列(important!)
    数组中只出现一次的数字
    fgets()函数以及fputs()函数
    C语言中的指针
  • 原文地址:https://www.cnblogs.com/fulu/p/13470126.html
Copyright © 2011-2022 走看看