React State Hooks的闭包陷阱,在使用Hooks之前必须掌握

2023-11-17

伴随着 React Hooks 的正式发布,因为其易用性以及对于逻辑代码的复用性更强,毫无疑问越来越多的同学会偏向于使用 Hooks 来写自己的组件。但是随着使用的深入,我们发现了一些 State Hooks 的陷阱,那么今天我们就来分析一下 State Hooks 存在的一些问题,帮助同学们踩坑。

前几天在 twitter 上看到了一个关于 Hooks 的讨论,其内容围绕着下面的 demo:

掘金上不让外挂代码,所以点击进去看吧

这里的代码想要实现的功能如下:

  • 点击 Start 开始执行 interval,并且一旦有可能就往 lapse 上加一
  • 点击 Stop 后取消 interval
  • 点击 Clear 会取消 interval,并且设置 lapse 为 0

但是这个例子在实际执行过程中会出现一个问题,那就是在 interval 开启的情况下,直接执行 clear,会停止 interval,但是显示的 lapse 却不是 0,那么这是为什么呢?

出现这样的情况主要原因是:useEffect 是异步的,也就是说我们执行 useEffect 中绑定的函数或者是解绑的函数,**都不是在一次 setState 产生的更新中被同步执行的。**啥意思呢?我们来模拟一下代码的执行顺序:

在我们点击来 clear 之后,我们调用了 setLapsesetRunning,这两个方法是用来更新 state 的,所以他们会标记组件更新,然后通知 React 我们需要重新渲染来。

然后 React 开始来重新渲染的流程,并很快执行到了 Stopwatch 组件。

注意以上都是同步执行的过程,所以不会存在在这个过程中 setInterval 又触发的情况,所以在更新 Stopwatch 的时候,如果我们能同步得执行 useEffect 的解绑函数,那么就可以在这次 JavaScript 的调用栈中清除这个 interval,而不会出现这种情况。

但是恰恰因为 useEffect 是异步执行的,他要在 React 走完本次更新之后才会执行解绑以及重新绑定的函数。那么这就给 interval 再次触发的机会,这也就导致来,我们设置 lapse 为 0 之后,他又在 interval 中被更新成了一个计算后的值,之后才被真正的解绑。

那么我们如何解决这个问题呢?

使用 useLayoutEffect

useLayoutEffect 可以看作是 useEffect 的同步版本。使用 useLayoutEffect 就可以达到我们上面说的,在同一次更新流程中解绑 interval 的目的。

那么同学们肯定要问了,既然 useLayoutEffect 可以避免这个问题,那么为什么还要用 useEffect 呢,直接所有地方都用 useLayoutEffect 不就好了。

这个呢主要是因为 useLayoutEffect 是同步的,如果我们要在 useLayoutEffect 调用状态更新,或者执行一些非常耗时的计算,可能会导致 React 运行时间过长,阻塞了浏览器的渲染,导致一些卡顿的问题。这块呢我们有机会再单独写一篇文章来分析,这里就不再赘述。

不使用 useLayoutEffect

当然我们不能因为 useLayoutEffect 非常方便得解决了问题所以就直接抛弃 useEffect,毕竟这是 React 更推荐的用法。那么我们该如何解决这个问题呢?

在解决问题之前,我们需要弄清楚问题的根本。在这个问题上,我们之前已经分析过,就是因为在我们设置了 lapse 之后,因为 interval 的再次触发,但是又设置了一次 lapse那么要解决这个问题,就可以通过避免最新的那次触发,或者在触发的时候判断如果没有 running,就不再设置。

使用 useLayoutEffect 显然属于第一种方法来解决问题,那么我们接下去来讲讲第二种方法。

按照这种思路,我们第一个反应应该就是在 setInterval 的回调中加入判断:

const intervalId = setInterval(() => {
  if (running) {
    setLapse(Date.now() - startTime)
  }
}, 0)
复制代码

但是很遗憾,这样做是不行的,因为这个回调方法保存了他的闭包,而在他的闭包里面,running 永远都是true。那么我们是否可以通过在 useEffect 外部声明方法来逃过闭包呢?比如下面这样:

function updateLapse(time) {
  if (runing) {
    setLapse(time)
  }
}

React.useEffect(() => {
  //...
  setInterval(() => {
    updateLapse(/* ... */)
  })
})
复制代码

看上去 updateLapse 使用的是直接外部的 running,所以不是 setInterval 回调保存的闭包来。但是可惜的是,这也是不行的。因为 updateLapse 也是 setInterval 闭包中的一部分,在这个闭包当中,running 永远都是一开始的值。

可能看到这里大家会有点迷糊,主要就是对于闭包的层次的不太理解,这里我就专门提出来讲解一下。

在这里我们的组件是一个函数组件,他是一个纯粹的函数,没有 this,同理也就没有 this.render 这样的在 ClassComponent 中特有的函数,所以每次我们渲染函数组件的时候,我们都是要执行这个方法的,在这里我们执行 Stopwatch

那么在开始执行的时候,我们就为 Stopwatch 创建来一个作用域,在这个作用域里面我们会声明方法,比如 updateLapse,他是在这次执行 Stopwatch 的时候才声明的,每一次执行 Stopwatch 的时候都会声明 updateLapse。同样的,lapserunning 也是每个作用域里单独声明的,**同一次声明的变量会出于同一个闭包,不同的声明在不同的闭包。**而 useEffect 只有在第一次渲染,或者后续 running 变化之后才会执行他的回调,所以对应的回调里面使用的闭包,也是每次执行的那次保存下来的。

这就导致了,在一个 useEffect 内部是无法获知 running 的变化的,这也是 useEffct 提供第二个参数的原因。

那么是不是这里就无解了呢?明显不是的,这时候我们需要考虑使用 useReducer 来管理 state

逃出闭包

我们先来看一下使用 useReducer 实现的代码:

掘金上不让外挂代码,所以点击进去看吧

在这里我们把 lapserunning 放在一起,变成了一个 state 对象,有点类似 Redux 的用法。在这里我们给 TICK action 上加了一个是否 running 的判断,以此来避开了在 running 被设置为 false 之后多余的 lapse 改变。

那么这个实现跟我们使用 updateLapse 的方式有什么区别呢?最大的区别是我们的 state 不来自于闭包,在之前的代码中,我们在任何方法中获取 lapserunning 都是通过闭包,而在这里,state 是作为参数传入到 Reducer 中的,也就是不论何时我们调用了 dispatch,在 Reducer 中得到的 State 都是最新的,这就帮助我们避开了闭包的问题。

其实我们也可以通过 useState 来实现,原理是一样的,我们可以通过把 lapserunning 放在一个对象中,然后使用

updateState(newState) {
  setState((state) => ({ ...state, newState }))
}
复制代码

这样的方式来更新状态。这里最重要的就是给 setState 传入的是回调,这个回调会接受最新的状态,所以不需要使用闭包中的状态来进行判断。具体的代码我这边就不为大家实现来,大家可以去试一下,最终的代码应该类似下面的(没有测试过):

const [state, dispatch] = React.useState(stateReducer, {
  lapse: 0,
  running: false,
})

function updateState(action) {
  setState(state => {
    switch (action.type) {
      case TOGGLE:
        return { ...state, running: !state.running }
      case TICK:
        if (state.running) {
          return { ...state, lapse: action.lapse }
        }
        return state
      case CLEAR:
        return { running: false, lapse: 0 }
      default:
        return state
    }
  })
}
复制代码

如果有问题非常欢迎跟我讨论哦。

总结

相信看到这里大家应该已经有一些自己的心得了,关于 Hooks 使用上存在的一些问题,最主要的其实就是因为函数组件的特性带来的作用域和闭包问题,一旦你能够理清楚那么你就可以理解很多了。

当然我们肯定不仅仅是给大家一些建议,从这个 demo 中我们也总结出一些最佳实践:

  • 讲相关的 state 最好放到一个对象中进行统一管理
  • 使用更新方法的时候最好使用回调的方式,使用传入的状态,而不要使用闭包中的 state
  • 管理复杂的状态可以考虑使用useReducer,或者类似的方式,对状态操作定义类型,执行不同的操作。

好了,以上就是这一次的分享,希望大家能收获一定的经验,避免以后在 Hooks 的使用中出现上面提到的这些问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React State Hooks的闭包陷阱,在使用Hooks之前必须掌握 的相关文章

随机推荐

  • 4.1 独立键盘检测

    题目 用数码管的前两位显示一个十进制的数 00 59变化 开始显示00 每按下S2键一次 数值加1 每按下S3键一次 数值减1 每按下S4键一次数值归零 按下S5键一次 利用定时器功能使数值开始自动每秒加1 再按下S5键 数值停止自动加1
  • Sqlite研究系列-1

    文章目录 简介 架构 简介 sqlite是一个开源的嵌入式关系型数据库 与常规数据库不同的地方是 零配置 没有账号概念 客户端和服务端运行在应用程序的进程空间 不需要网络配置 sqlite可以编译到应用程序中 依赖于文件系统 占用资源少 支
  • mpu6050数据实时发布到mqtt服务器

    vcc 5V SDA i2c数据 pin3 SDL i2c时钟 pin5 GND import smbus import SMBus module of I2C import time import math import json imp
  • 顺序表插入元素

    顺序表在插入元素时应注意 1 插入元素不可以插在最后一个位置 2 插入元素不可以插在超过顺序表的长度 代码实现 include
  • iOS下XMPP开发之XMPP开发环境配置(二)mac上搭建openfire服务器

    一 下载并安装openfire 1 到http www igniterealtime org downloads index jsp下载最新openfire for mac版 比如 Openfire 3 8 1 下载后的文件 openfir
  • XSS跨站脚本攻击(一)----XSS攻击的三种类型

    一 简介 什么是XSS 百度百科的解释 XSS又叫CSS Cross Site Script 跨站脚本攻击 它指的是恶意攻击者往Web页面里插入恶意html代码 当用户浏览该页之时 嵌入其中Web里面的html代码会被执行 从而达到恶意用户
  • gitlab项目代码仓库管理指南(自用)

    gitlab项目管理流程 注意事项 任何项目开始即创建对应项目仓库 issues应覆盖项目从原始需求 gt 项目结题过程中各环节 记录问题 解决思路等 及时整理 及时归档 流程图 git常用命令图 创建项目团队 注意事项 正式项目应所属团队
  • 相似性度量总结

    整理自 机器学习中的相似性度量 余弦距离 欧氏距离和杰卡德相似性度量的对比分析 在做分类时常常需要估算不同样本之间的相似性度量 Similarity Measurement 这时通常采用的方法就是计算样本间的 距离 Distance 采用什
  • VS2017突然不检查语法错误

    VS2017用着用着不检查语法错误 生成只说失败 错误列表显示0 只需要退出软件 到工程目录中删除 vs文件夹 重启软件即可 VS2019也是一样的
  • 关于https://goproxy.cn,direct与https://proxy.golang.org的问题,国内无法访问https://proxy.golang.org设置了GOPROXY仍不可行

    关于https goproxy cn direct与https proxy golang org的问题 国内无法访问https proxy golang org设置了GOPROXY仍不可行 一步一步说 首先 遇到报错信息 go github
  • NLP基础知识点:BLEU(及Python代码实现)

    Bleu 1 是IBM在2002提出的 用于机器翻译任务的评价 BLEU还有许多变种 根据n gram可以划分成多种评价指标 常见的指标有BLEU 1 BLEU 2 BLEU 3 BLEU 4四种 其中n gram指的是连续的单词个数为n
  • 校招真题练习008 浇花(百度)

    浇花 题目描述一个花坛中有很多花和两个喷泉 喷泉可以浇到以自己为中心 半径为r的圆内的所有范围的花 现在给出这些花的坐标和两个喷泉的坐标 要求你安排两个喷泉浇花的半径r1和r2 使得所有的花都能被浇到的同时 r1 2 r2 2 的值最小 输
  • GCC编译优化应用预编译头

    服务器编译优化记录 对项目编译优化过程中一些思路和脚本工具实现 对内存受限的编译环境有一些帮助 工具 https github com wangxiaobai dd GccPrecompiledHeader 环境 32G内存 16核 Mak
  • 入职华为外包一个月,我离职了

    我入职华为外包公司已经有一个月了 一开始我对这份工作充满了期待和热情 毕竟 华为是一家全球知名的科技公司 而我也有机会成为其中的一员 我相信这份工作会给我带来许多机遇和挑战 然而 随着时间的推移 我开始发现外包公司的工作条件并不如我所想象的
  • 指标实现层级_有了指标怎么用层次分析法建立模型?

    电脑 MATLAB软件 方法 步骤 建立层次结构模型 目标层 这一层次中只有一个元素 一般它是分析问题的预定目标或理想结果 因此也称为目标层 准则层 这一层次中包含了为实现目标所涉及的中间环节 它可以由若干个层次组成 包括所需考虑的准则 子
  • TQ210学习笔记:TQ210移植qt

    这几天搞了一块TQ210的板子 由于要求 需要移植qt进去 于是搞了近一个星期 现在终于看到了一点希望 开始找了一篇博客 我是按照他的步骤来 http emouse cnblogs com 首先是移植TSLIB 移植这个的原因是 因为电磁噪
  • C++进阶--对象指针

    对象指针定义形式 类名 对象指针名 例 Point a 5 10 Point ptr ptr a 通过指针访问对象成员 对象指针名 gt 成员名 例 ptr gt getx 就相当于 ptr getx this指针 隐含于类的每一个非静态成
  • 行为型模式 - 状态模式State

    状态模式的定义与特点 状态 State 模式的定义 对有状态的对象 把复杂的 判断逻辑 提取到不同的状态对象中 允许状态对象在其内部状态发生改变时改变其行为 状态模式是一种对象行为型模式 其主要优点如下 结构清晰 状态模式将与特定状态相关的
  • 算法设计与分析(期末复习重点)更新中

    第一章 算法设计基础 算法的五大特性 输入 输出 可行性 有穷性 确定性 1 输入 一个算法有零个或多个输入 2 输出 一个算法有一个或多个输出 3 可行性 算法描述的操作可以通过已经实现的基本操作执行有限次来实现 每步可执行 4 有穷性
  • React State Hooks的闭包陷阱,在使用Hooks之前必须掌握

    伴随着 React Hooks 的正式发布 因为其易用性以及对于逻辑代码的复用性更强 毫无疑问越来越多的同学会偏向于使用 Hooks 来写自己的组件 但是随着使用的深入 我们发现了一些 State Hooks 的陷阱 那么今天我们就来分析一