ahooks解决React闭包问题方法示例

引言

本文是深入浅出 ahooks 源码系列文章的第三篇,这个系列的目标主要有以下几点:

  • 加深对 React hooks 的理解。
  • 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。
  • 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

注:本系列对 ahooks 的源码解析是基于 v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情

系列文章:

  • 大家都能看得懂的源码 ahooks 整体架构篇
  • 如何使用插件化机制优雅的封装你的请求hook

本文来探索一下 ahooks 是怎么解决 React 的闭包问题的?。

React 的闭包问题

先来看一个例子:

import React, { useState, useEffect } from "react";
export default () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      console.log("setInterval:", count);
    }, 1000);
  }, []);
  return (
    <div>
      count: {count}
      <br />
      <button onClick={() => setCount((val) => val + 1)}>增加 1</button>
    </div>
  );
};

代码示例

当我点击按钮的时候,发现 setInterval 中打印出来的值并没有发生变化,始终都是 0。这就是 React 的闭包问题。

产生的原因

为了维护 Function Component 的 state,React 用链表的方式来存储 Function Component 里面的 hooks,并为每一个 hooks 创建了一个对象。

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
};

这个对象的 memoizedState 属性就是用来存储组件上一次更新后的 statenext 指向下一个 hook 对象。在组件更新的过程中,hooks 函数执行的顺序是不变的,就可以根据这个链表拿到当前 hooks 对应的 Hook 对象,函数式组件就是这样拥有了state的能力

同时制定了一系列的规则,比如不能将 hooks 写入到 if...else... 中。从而保证能够正确拿到相应 hook 的 state。

useEffect 接收了两个参数,一个回调函数和一个数组。数组里面就是 useEffect 的依赖,当为 [] 的时候,回调函数只会在组件第一次渲染的时候执行一次。如果有依赖其他项,react 会判断其依赖是否改变,如果改变了就会执行回调函数。

回到刚刚那个例子:

const [count, setCount] = useState(0);
useEffect(() => {
  setInterval(() => {
    console.log("setInterval:", count);
  }, 1000);
}, []);

它第一次执行的时候,执行 useState,count 为 0。执行 useEffect,执行其回调中的逻辑,启动定时器,每隔 1s 输出 setInterval: 0

当我点击按钮使 count 增加 1 的时候,整个函数式组件重新渲染,这个时候前一个执行的链表已经存在了。useState 将 Hook 对象 上保存的状态置为 1, 那么此时 count 也为 1 了。执行 useEffect,其依赖项为空,不执行回调函数。但是之前的回调函数还是在的,它还是会每隔 1s 执行 console.log("setInterval:", count);,但这里的 count 是之前第一次执行时候的 count 值,因为在定时器的回调函数里面被引用了,形成了闭包一直被保存。

解决的方法

解决方法一:给 useEffect 设置依赖项,重新执行函数,设置新的定时器,拿到最新值。

// 解决方法一
useEffect(() => {
  if (timer.current) {
    clearInterval(timer.current);
  }
  timer.current = setInterval(() => {
    console.log("setInterval:", count);
  }, 1000);
}, [count]);

解决方法二:使用 useRef。 useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。

useRef 创建的是一个普通 Javascript 对象,而且会在每次渲染时返回同一个 ref 对象,当我们变化它的 current 属性的时候,对象的引用都是同一个,所以定时器中能够读到最新的值。

const lastCount = useRef(count);
// 解决方法二
useEffect(() => {
  setInterval(() => {
    console.log("setInterval:", lastCount.current);
  }, 1000);
}, []);
return (
  <div>
    count: {count}
    <br />
    <button
      onClick={() => {
        setCount((val) => val + 1);
        // +1
        lastCount.current += 1;
      }}
    >
      增加 1
    </button>
  </div>
);

useRef => useLatest

终于回到我们 ahooks 主题,基于上述的第二种解决方案,useLatest 这个 hook 随之诞生。它返回当前最新值的 Hook,可以避免闭包问题。实现原理很简单,只有短短的十行代码,就是使用 useRef 包一层:

import { useRef } from 'react';
// 通过 useRef,保持每次获取到的都是最新的值
function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;
  return ref;
}
export default useLatest;

useEvent => useMemoizedFn

React 中另一个场景,是基于 useCallback 的。

const [count, setCount] = useState(0);
const callbackFn = useCallback(() => {
  console.log(`Current count is ${count}`);
}, []);

以上不管,我们的 count 的值变化成多少,执行 callbackFn 打印出来的 count 的值始终都是 0。这个是因为回调函数被 useCallback 缓存,形成闭包,从而形成闭包陷阱。

那我们怎么解决这个问题呢?官方提出了 useEvent。它解决的问题:如何同时保持函数引用不变与访问到最新状态。使用它之后,上面的例子就变成了。

const callbackFn = useEvent(() => {
  console.log(`Current count is ${count}`);
});

在这里我们不细看这个特性,实际上,在 ahooks 中已经实现了类似的功能,那就是 useMemoizedFn。

useMemoizedFn 是持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。使用 useMemoizedFn,可以省略第二个参数 deps,同时保证函数地址永远不会变化。以上的问题,通过以下的方式就能轻松解决:

const memoizedFn = useMemoizedFn(() => {
  console.log(`Current count is ${count}`);
});

Demo 地址

我们来看下它的源码,可以看到其还是通过 useRef 保持 function 引用地址不变,并且每次执行都可以拿到最新的 state 值。

function useMemoizedFn<T extends noop>(fn: T) {
  // 通过 useRef 保持其引用地址不变,并且值能够保持值最新
  const fnRef = useRef<T>(fn);
  fnRef.current = useMemo(() => fn, [fn]);
  // 通过 useRef 保持其引用地址不变,并且值能够保持值最新
  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    // 返回的持久化函数,调用该函数的时候,调用原始的函数
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }
  return memoizedFn.current as T;
}

总结与思考

React 自从引入 hooks,虽然解决了 class 组件的一些弊端,比如逻辑复用需要通过高阶组件层层嵌套等。但是也引入了一些问题,比如闭包问题。

这个是 React 的 Function Component State 管理导致的,有时候会让开发者产生疑惑。开发者可以通过添加依赖或者使用 useRef 的方式进行避免。

ahooks 也意识到了这个问题,通过 useLatest 保证获取到最新的值和 useMemoizedFn 持久化 function 的方式,避免类似的闭包陷阱。

值得一提的是 useMemoizedFn 是 ahooks 输出函数的标准,所有的输出函数都使用 useMemoizedFn 包一层。另外输入函数都使用 useRef 做一次记录,以保证在任何地方都能访问到最新的函数。

参考

  • 从react hooks“闭包陷阱”切入,浅谈react hooks
  • React官方团队出手,补齐原生Hook短板

以上就是ahooks解决React闭包问题方法示例的详细内容,更多关于ahooks React闭包的资料请关注我们其它相关文章!

(0)

相关推荐

  • ahooks useRequest源码精读解析

    目录 前言 架构图 源码解析 Fetch onBefore onRequest onSuccess onFinally onError 其它 API 小结 plugins usePollingPlugin useRetryPlugin 小结 useRequest 对自定义 hook 的思考 总结 前言 自从 React v16.8 推出了 Hooks API,前端框架圈并开启了新的逻辑复用的时代,不再需要在意 HOC 的无限套娃导致性能差的问题,也解决了 mixin 的可阅读性差的问题.当然对于

  • react hooks闭包陷阱切入浅谈

    目录 引言 1.一个熟悉的闭包场景 2 浅谈hooks原理,理解useEffect 的 “闭包陷阱” 出现原因 2 难道真的要在依赖数组里写上的值,才能拿到新鲜的值? 3 为什么使用useRef能够每次拿到新鲜的值? 4 完毕 引言 首先,本文并不会讲解 hooks 的基本用法, 本文从 一个hooks中 “奇怪”(其实符合逻辑) 的 “闭包陷阱” 的场景切入,试图讲清楚其背后的因果.同时,在许多 react hooks 奇技淫巧的文章里,也能看到 useRef 的身影,那么为什么使用 useR

  • vue3中的hook简单封装

    目录 vue3的hook封装 vue3的hooks总结 下面总结一下如何去书写hooks 计数器的hook vue3的hook封装 vue3最新鲜的就是组合式API了,通过组合式API我们可以把一些复杂的逻辑或一些常用的逻辑封装成一个个hook来进行调用,这样的方式也更易于维护. 使用 import useTest from "../../hooks/useTest"; export default defineComponent({   name: "vue3Test&qu

  • 插件化机制优雅封装你的hook请求使用方式

    目录 引言 useRequest 简介 架构 useRequest 入口处理 Fetch 和 Plugins state 以及 setState 插件化机制的实现 核心方法 —— runAsync 请求前 —— onBefore 进行请求——onRequest 取消请求 —— onCancel 最后结果处理——onSuccess/onError/onFinally 思考与总结 引言 本文是深入浅出 ahooks 源码系列文章的第二篇,这个系列的目标主要有以下几点: 加深对 React hooks

  • React官方团队完善原生Hook闭包陷阱

    目录 正文 useEvent useEvent的实现 与开源Hooks的差异 总结 正文 我们知道,Hooks使用时存在所谓的闭包陷阱,考虑如下代码: function Chat() { const [text, setText] = useState(''); const onClick = useCallback(() => { sendMessage(text); }, []); return <SendButton onClick={onClick} />; } 我们期望点击后s

  • 列表页常见hook封装实例

    目录 引言 列表页常见元素 usePagination useAntdTable 引言 本文是深入浅出 ahooks 源码系列文章,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 列表页常见元素 对于一些后台管理系统,典型的列表页包括筛选表单项.Table表格.Pagination分页这三部分. 针对使用 Antd 的系统,在 a

  • ahooks控制时机的hook实现方法

    目录 引言 Function Component VS Class Component Class Component Function Component LifeCycle - 生命周期 useMount useUnmount useUnmountedRef Effect useUpdateEffect 和 useUpdateLayoutEffect useDeepCompareEffect和useDeepCompareLayoutEffect useUpdate 总结与思考 引言 本文是深

  • ahooks解决用户多次提交方法示例

    目录 引言 场景 useLockFn 缺点 axios 自动取消重复请求 axios 取消请求 如何自动取消重复的请求 思考与总结 引言 本文是深入浅出 ahooks 源码系列文章的第四篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码解析是基于 v3.3.13.自己 folk 了一份源码,主要

  • ahooks解决React闭包问题方法示例

    引言 本文是深入浅出 ahooks 源码系列文章的第三篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码解析是基于 v3.3.13.自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情. 系列文章: 大家都能看得懂的源码 ahooks 整体架构篇 如何使用插件化机制优雅的封装你的请求

  • php解决DOM乱码的方法示例代码

    前言 DOM是php比较新的xml和html处理类,可以像javascript那样方便的操作DOM树,网上更多的是介绍它处理XML的情况,今天这篇文章就介绍下php解决DOM乱码的方法,下面话不多说,直接看下面的解决方法. 解决方法如下 /** * 请求url页面信息 * @param str $url * @return str mixed|boolean */ function curl_get($url) { $curl = curl_init(); curl_setopt($curl,

  • java开放地址法和链地址法解决hash冲突的方法示例

    hashMap对各位小伙们来说,没有不知道的了,使用过的人想必或多或少的都了解一点hashMap的底层实现原理,总结来说就是,数组+链表,至于源码的实现,大家可参看源码,今天想说的是hashMap是怎么解决hash冲突的呢? 首先看一张图, 从这张图也大概可以看出来,hashMap维护的是一个数组,数组里面的每个单元又是一个个链表,那么为什么会产生hash冲突呢?这也就是接下来要探讨的问题. 既是数组,必然会有长度,当我们在往数组中插入数据的时候,不管是什么类型的数据,对于数组来说,就是占据了某

  • c#闭包使用方法示例

    代码简单,直接上代码了 复制代码 代码如下: private static void Before()        {            Action[] actions = new Action[10]; for (var i = 0; i < actions.Length; i++)            {                actions[i] = () =>                {                    Console.WriteLine(

  • 解决React Native端口号修改的方法

    本文介绍了解决React Native端口号修改的方法,分享给大家,具体如下: 看图说话 一般情况下,如果本地安装过一些服务的话,ReactNeact 就会毫不犹豫的给你报出错误信息, 如果你是这个错误,那么你的端口号被占用了,ReactNative默认端口为8081 解决方案1: 很简单,找到使用node生成的ReactNative项目 使用node命令: 在cmd命令中,切换到项目目录下,输入: react-native start --port 9999 接下来,继续走 然后,摇晃设备或者

  • react 兄弟组件如何调用对方的方法示例

    最近有一个场景是Child2组件点击让Child1组件里面的state的值发生改变,Child1是一个公用组件,把里面的state值改为props传递,修改内容太多,容易出错,就想找其他的方法来解决兄弟组件调用方法问题,下面看代码: Child1 是第一个子组件 class Child1 extends React.Component { constructor(props) { super(props); this.state = { text:'Child1' }; } onChange=(

  • 在react中使用vue的状态管理的方法示例

    我是要介绍一个新的 react 全局共享状态管理器,它和 vue 组件的状态管理一起同工之妙. 马上体验 在 react 状态管理领域,react-redux 可谓是只手遮天了,基于 flux 思想实现,小巧,immutable 的思想让数据变化可控.但 immutable 所带来的编程代价太大了,如果你要更新一个深层结构的对象的某个节点,写作将会是极其麻烦的一件事,而且还保不准会出错.为了保证 immutable,redux 的 reducer 机制让开发者掉光了头发.于是有了类似 dva.r

  • 解决Nginx端口冲突的排查方法示例

    问题描述 一个Spring + Angular前后端分离的项目,使用Nginx进行数据转发. Nginx监听端口8100,前台端口4200,后台端口8080. 像往常一样,提前配置好MySQL.配置好Redis,引入项目的Nginx配置文件,然后启动前台.后台,成功. 接下来出现了问题:前台发起的请求,只有极少数能被后台接收到,大部分都是404,随着在浏览器中的点击,控制台不断的出现404. 如果只是404,那问题就很简单,很大可能是Nginx端口转发设置错了.但它的神奇之处就在于,还有那么几次

  • react获取input输入框的值的方法示例

    react 获取input 输入框的值的多种方式 第一种方式 非受控组件获取 第二种方式 受控组件获取 非受控组件获取 ref import React , {Component} from 'react'; export default class App extends Component{ search(){ const inpVal = this.input.value; console.log(inpVal); } render(){ return( <div> <input

随机推荐