面试官:useEffect和useLayoutEffect有什么区别?

news2024/11/26 2:54:03

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

Effect数据结构

顾名思义,React底层在函数式组件的Fiber节点设计中带入了hooks链表的概念(memorizedState),在此变量上专门存储每一个函数式组件对应的链表。

而对于副作用(useEffect or useLayoutEffect)来说,对应其hook类型就是Effect

单个的effect对象包括以下几个属性:

  • create: 传入useEffect or useLayoutEffect函数的第一个参数,即回调函数;

  • destroy: 回调函数return的函数,在该effect销毁的时候执行,渲染阶段为undefined

  • deps: 依赖项,改变重新执行副作用;

  • next: 指向下一个effect

  • tag: effect的类型,区分是useEffect还是useLayoutEffect

单纯看这些字段,和平时使用层面来联想还是很通俗易懂的,这里还是补充一下hooks链表的概念,有如下的例子:

const Hello = () => {
    const [ text, setText ] = useState('hello')
    useEffect(() => {
        console.log('effect1')
        return () => {
            console.log('destory1');
        }
    })
    useLayoutEffect(() => {
        console.log('effect2')
        return () => {
            console.log('destory2');
        }
    })
    return <div>effect</div>
}

挂载到Hello组件fibermemoizedState如下:

image.png

可以看到,打印出来结果和组件中声明hook的顺序是一样的,不难看出这是一个链表,这也是为什么react hook要求hook的使用不能放在条件分支语句中的原因,如果第一次mount走的是A情况,第二次updateMount走的是B情况,就会出现hooks链表混乱的情况,保证官方范式是比较重要的原因。

Hook

从上图的例子中可以看到,memorizedState的值会根据不同hook来决定。

  • 使用useState时,memorizedState对应是string(hello);
  • 使用useEffectuseLayoutEffect,对应的是Effect

Hook类型如下:

export type Hook = { 
    memoizedState: any, // Hook 自身维护的状态 
    baseQueue: any,
    baseState: any,
    queue: UpdateQueue<any, any> | null, // Hook 自身维护的更新队列 
    next: Hook | null, // next 指向下一个 Hook 
};

创建副作用流程

基于上面的数据结构,对于use(Layout)Effect来说,React做的事情就是

  • render阶段:函数组件开始渲染的时候,创建出对应的hook链表挂载到workInProgressmemoizedState上,并创建effect链表,也就是挂载到对应的fiber节点上,但是基于上次和本次依赖项的比较结果, 创建的effect是有差异的。这一点暂且可以理解为:依赖项有变化,effect可以被处理,否则不会被处理。
  • commit阶段:异步调度useEffect或者同步处理useLayoutEffecteffect。等到commit阶段完成后,更新应用到页面上之后,开始处理useEffect产生的effect,或是直接处理commit阶段同步执行阻塞页面更新的useLayoutEffect产生的effect

第二点提到了一个重点,就是useEffect和useLayoutEffect的执行时机不一样,前者被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 后者是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。

创建effect链表

useEffect的工作是在currentlyRenderingFiber加载当前的hook,具体流程就是判断当前fiber是否已经存在hook(就是判断fiber.memoizedState),存在的话则创建一个effect hook到链表的最后,也就是.next,没有的话则创建一个memoizedState

先看一下创建一个Effect的入口函数:

function mountEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null
): void {
    return mountEffectImpl(
        UpdateEffect | PassiveEffect,
        HookPassive,
        create,
        deps,
    );
};

可以看到本质上是调用了mountEffectImpl函数,传了上一节所说的Effect type中的字段,这里有个问题,为什么destroy没传呢?获取上一次effectdestroy函数,也就是useEffect回调中return的函数,在创建阶段是第一次,所以为undefined

这里看一下创建阶段调用的mountEffectImpl函数:

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 创建hook对象
  const hook = mountWorkInProgressHook();
  // 获取依赖
  const nextDeps = deps === undefined ? null : deps;

  // 为fiber打上副作用的effectTag
  currentlyRenderingFiber.flags |= fiberFlags;

  // 创建effect链表,挂载到hook的memoizedState上和fiber的updateQueue
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

接下来我们都知道,ReactVue都是状态改变导致页面重渲染,而useEffect or useLayoutEffect都会会根据deps变化重新执行,所以猜都猜得到,在更新时调用的updateEffectImpl函数,对比mountEffectImpl
函数多出来的一部分内容其实就是对比上一次的Effect的依赖变化,以及执行上一次Effect中的destroy部分内容~代码如下:

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    // 从currentHook中获取上一次的effect
    const prevEffect = currentHook.memoizedState;
    // 获取上一次effect的destory函数,也就是useEffect回调中return的函数
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 比较前后依赖,push一个不带HookHasEffect的effect
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;
  // 如果前后依赖有变,在effect的tag中加入HookHasEffect
  // 并将新的effect更新到hook.memoizedState上
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

可以看到在mountEffectImplupdateEffectImpl中,最后的结果走向都是pushEffect函数,它的工作很纯粹,就是创建出effect对象,把对象挂到链表中。

pushEffect代码如下:

function pushEffect(tag, create, destroy, deps) {
  // 创建effect对象
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };

  // 从workInProgress节点上获取到updateQueue,为构建链表做准备
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // 如果updateQueue为空,把effect放到链表中,和它自己形成闭环
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    // 将updateQueue赋值给WIP节点的updateQueue,实现effect链表的挂载
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // updateQueue不为空,将effect接到链表的后边
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

这里的主要逻辑其实就是本节开头所说的,区分两种情况,链表为空或链表存在的情况,值得一提的是这里的updateQueue是一个环形链表。

以上,就是effect链表的构建过程。我们可以看到,effect对象创建出来最终会以两种形式放到两个地方:单个的effect,放到hook.memorizedState上;环状的effect链表,放到fiber节点的updateQueue中。两者各有用途,前者的effect会作为上次更新的effect,为本次创建effect对象提供参照(对比依赖项数组),后者的effect链表会作为最终被执行的主体,带到commit阶段处理。

提交阶段

commitRoot

当我们完成更新,进入提交重渲染视图时,主要在commitRoot函数中执行,而在这之前创建Effect以及插入到hooks链表中,useEffectuseLayoutEffect其实做的都是一样的,也是共用的,在提交阶段,我们会看出两者执行时机不同的实现点。

// src/react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
  // 已经完成构建的fiber,上面会包括hook信息
  const { finishedWork } = root;

  // 如果存在useEffect或者useLayoutEffect
  if ((finishedWork.flags & Passive) !== NoFlags) {
    if (!rootDoesHavePassiveEffect) {
      rootDoesHavePassiveEffect = true;
      // 开启下一个宏任务
      requestIdleCallback(flushPassiveEffect);
    }
  }

  console.log('start commit.');
  
  // 判断自己身上有没有副作用
  const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
  // 如果自己的副作用或者子节点有副作用就进行DOM操作
  if (rootHasEffect) {
    console.log('DOM执行完毕');  
    commitMutationEffectsOnFiber(finishedWork, root);  
  
    // 执行layout Effect  
    console.log('开始执行layoutEffect');
    commitLayoutEffects(finishedWork, root);
    if (rootDoesHavePassiveEffect) {
      rootDoesHavePassiveEffect = false;
      rootWithPendingPassiveEffects = root;
    }
  }
  // 等DOM变更之后,更改root中current的指向
  root.current = finishedWork;
}

这里的rootDoesHavePassiveEffect是核心判断点,还记得Effect类型中的tag参数吗?就是依靠这个参数来标识区分useEffectuseLayoutEffect的。

rootDoesHavePassiveEffect === false,则执行宏任务,将Effect副作用推入宏任务执行栈中。我们可以简单理解成useEffect的回调函数包装在了requestIdleCallback中去异步执行,根据fiber的知识接下来会去走浏览器当前帧是否有空余时间来判断副作用函数的执行时机。

继续往下走,如果rootHasEffect === true,代表有副作用,如果是useEffect,副作用已经在上面进入宏任务队列了,所以如果是useLayoutEffect,就会在这个条件中去执行,所以在这里我们可以理解到那一句"useEffect和useLayoutEffect的区别是,前者会异步执行副作用函数不会阻塞页面更新,后者会立即执行副作用函数,会阻塞页面更新,不适合写入复杂逻辑。"的原因了。

结尾

useEffectuseLayoutEffect十分相似,就连签名都一样,不同之处就在于前者会在浏览器绘制后延迟执行,而后者会在所有DOM变更之后同步调用effect,希望你看到这里,可以对于这个结论的来源有一定的了解和学习,希望可以帮到你~

如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/608497.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

博弈论——巴什博弈(C++)

博弈论&#xff08;C&#xff09; 前言例题&#xff1a;拍卖会题目描述输入输出格式输入格式&#xff1a;输出格式&#xff1a; 输入输出样例输入样例#1&#xff1a;输出样例#1&#xff1a; 例题的解&#xff1a;巴什博奕&#xff08;Bash Game&#xff09;&#xff1a;代码&am…

CEPH部署

//存储类型 块存储 一对一&#xff0c;只能被一个主机挂载使用&#xff0c;数据以块为单位进行存储&#xff0c;典型代表&#xff1a;硬盘 文件存储 一对多&#xff0c;能被多个主机同时挂载使用&#xff0c;数据以文件的形式存储的&#xff08;元数据和实际数据是分开存储…

【30】核心易中期刊推荐——人工智能图像处理

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

javascript基础二十三:说说 JavaScript 中内存泄漏的几种情况

一、是什么 内存泄漏&#xff08;Memory leak&#xff09;是在计算机科学中&#xff0c;由于疏忽或错误造成程序未能释放已经不再使用的内存 并非指内存在物理上的消失&#xff0c;而是应用程序分配某段内存后&#xff0c;由于设计错误&#xff0c;导致在释放该段内存之前就失…

【数学建模】期末样题(2021年真题)

一、&#xff08;10分&#xff09;某乳制品厂计划生产A、B、C三种酸奶。已知生产单位重量的A需要加工设备3小时&#xff0c;原料甲1千克&#xff0c;原料乙0.2千克&#xff1b;生产单位重量的B需要加工设备5小时&#xff0c;原料甲1千克&#xff0c;原料乙0.3千克&#xff1b;生…

若依之权限处理

若依之权限处理 若依前后端不分离版本使用的是shiro进行权限控制&#xff0c;本文主要是对shiro在若依中的使用进行分析。 RBAC权限模型 RBAC是指基于角色的访问控制。其基本思想是&#xff0c;对系统的各种权限不是直接授予具体的用户&#xff0c;而是在用户集合与权限集合…

【论文总结】Drifuzz: Harvesting Bugs in Device Drivers from Golden Seeds

Drifuzz:从金种子中收获设备驱动程序中的漏洞 摘要: 现代计算机中的外围硬件通常被认为是安全的且不带恶意&#xff0c;并且设备驱动程序以信任从硬件输入的方式实现。然而&#xff0c;最近的漏洞攻击&#xff08;如Broadpwn&#xff09;已经证明攻击者可以通过易受攻击的外围…

Map复习(JDK1.8)

首先考虑的问题任然是高并发的安全问题&#xff1a; HashMap是一个不安全的&#xff0c;多线程会导致数据不一致。 Hashtable是安全的&#xff0c;但是速度比较慢&#xff0c;只要是安全的就是一性能为代价换来的&#xff0c;加锁开锁需要时间&#xff0c;加锁是只能一个线程访…

服务注册与发现总结

文章目录 概要一、服务注册与发现技术要点1.1、服务注册/下线1.1.1、代理注册1.1.2、客户端注册 1.2、健康检查&#xff08;续约&#xff09;1.2.1、主动检测1.2.2、被动检测 1.3、服务发现1.3.1、代理发现1.3.2、客户端发现 1.4、服务变更通知1.4.1、主动通知1.4.2、被动通知 …

linux系统vim编辑器的使用

前言&#xff1a; 前面我们就说过在linux系统下一切都是文件&#xff0c;也可以说在日常使用linux系统过程中使用频率最高工具&#xff0c;基本没有之一&#xff0c;今天就来详情介绍一下我们在日常使用vim编辑器的过程中一些常见的指令&#xff0c;博主始终遵循好记心不如烂笔…

Excel排序

Excel排序 写在前面&#xff1a; Excel是我们最常用的办公工具之一&#xff0c;其中排序是最基础和最重要的一项&#xff0c;如下介绍了两种排序方式&#xff0c;有需要者可以参照这些步骤完成相关任务~ 一、排序方法1 使用Excel自带的排序功能&#xff0c;具体步骤如下&…

pandas菜鸟速学-series

一、pandas是什么 一个基于numpy的数据处理数据分析的工具。 特点&#xff1a; Pandas 的主要数据结构是 Series &#xff08;一维数据&#xff09;与 DataFrame&#xff08;二维数据&#xff09;&#xff0c;这两种数据结构足以处理金融、统计、社会科学、工程等领域里的大…

重学SSE

概述 SSE&#xff08;Server-Sent Events&#xff09;是一种用于实现服务器主动向客户端推送数据的技术&#xff0c;也被称为“事件流”&#xff08;Event Stream&#xff09;。它基于 HTTP 协议&#xff0c;利用了其长连接特性&#xff0c;在客户端与服务器之间建立一条持久化…

暴力递归到动态规划(一)

⭐️前言⭐️ 动态规划是一个很难的模块&#xff0c;如果一道动态规划的题目直接去推出动态转移方程来解题&#xff0c;是很难的&#xff0c;所以应该先想出暴力解决的方法&#xff0c;再去用空间换时间优化&#xff0c;得出动态规划的解法。 &#x1f349;欢迎点赞 &#x1f…

【Java设计模式】—— 享元模式概述和示例

目录 概述享元模式的定义与特点享元模式的结构与实现1. 模式的结构2. 模式的理论实现 享元模式的应用实例代码实现 享元模式的应用场景享元模式的在实际工作中的应用参考文献 概述 在面向对象程序设计过程中&#xff0c;有时会面临要创建大量相同或相似对象实例的问题。创建那…

Linux命令学习之mkdir、rmdir和rm

这篇文章想要学习一下目录的创建&#xff08;mkdir&#xff09;、空目录的删除&#xff08;rmdir&#xff09;、非空目录的删除&#xff08;rm&#xff09;。 mkdir mkdir是新建目录的命令。man mkdir看一下mkdir的使用说明。 按q键退出帮助说明。 mkdir /learnwell在根目录…

贷款业务-贷款模式

参考文章 多流量模式下的系统设计- 呱说产品自营 互联网金融之信贷三部曲&#xff1a;贷中 导读 互联网的玩法&#xff0c;基本都是流量为王。网上也有种说法&#xff0c;互联网平台&#xff0c;最终都会走上金融业务&#xff0c;一般都是指贷款业务。 但是&#xff0c;做…

PLC信号发生器(博途SCL)

信号发生器的应用请参看下面的博客文章,在演示分析滤波器的作用时,我们需要对信号进行叠加处理。 博途PLC滤波指令 Filter_PT1、Filter_PT2、Filter_DT1详细使用说明(含Simulink+博途PLC仿真)_RXXW_Dor的博客-CSDN博客博途S7-1200/1500PLC的PID控制和详细使用说明,请参看…

计算机组成原理---第四章 指令系统习题详解版

&#xff08;一&#xff09;课内例题 4.1 4.2 具体分析&#xff0c;4.2 中&#xff0c; 因为只有一行&#xff1a;单字长 二地址&#xff1a;源寄存器、目标寄存器 操作码看OP&#xff08;15-91&#xff09; 然后按操作数的物理位置来区别RR型&#xff0c;RS型&#xf…

代码随想录训练营Day58| 739. 每日温度 496.下一个更大元素 I

目录 学习目标 学习内容 739. 每日温度 496.下一个更大元素 I 学习目标 739. 每日温度 496.下一个更大元素 I 学习内容 739. 每日温度 739. 每日温度 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/daily-temperatures/ class Solution:def da…