想必大家都能看得懂的源码 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属性就是用来存储组件上一次更新后的state,next指向下一个 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 做一次记录,以保证在任何地方都能访问到最新的函数。
欢迎大家关注更多精彩内容。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/128263.html
这是讲 ahooks 源码的第一篇文章,简要就是以下几点: 加深对 React hooks 的理解。 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。 注:本系列对 ahooks 的源码解析是基于v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见详情。 第一篇主要介绍 a...
起因 社会在不断的向前,技术也在不断的完善进步。从 React Hooks 正式发布到现在,越来越多的项目正在使用 Function Component 替代 Class Component,Hooks 这一新特性也逐渐被广泛的使用。 这样的解析是不是很熟悉,在日常中时常都有用到,但也有一个可以解决这样重复的就是对数据请求的逻辑处理,对防抖节流的逻辑处理等。 另一方面,由于 Hoo...
陷进到处都是啊!本篇文章就说说Hooks使用时存在所谓的闭包陷阱,看看下面代码: functionChat(){ const[text,setText]=useState(''); constonClick=useCallback(()=>{ sendMessage(text); },[]); return<SendButtononClick=...
摘要:原文链接有大量平均水平左右的工人可被选择参与进来这意味着好招人有成熟的大量的程序库可供选择这意味着大多数项目都是既有程序库的拼装,标准化程度高而定制化场景少开发工具测试工具问题排查工具完善,成熟基本上没有团队愿意在时间紧任务重的项目情况 原文链接:http://pfmiles.github.io/blog/java-groovy-mixed/ 有大量平均水平左右的工人可被选择、参与...
本篇主要和大家沟通关于ahooks ,我们可以理解为加深对 React hooks 的了解。 我们先说下关于抽象自定义 hooks。构建属于自己的 React hooks 工具库。 其实我们应该培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。 注:本系列对 ahooks 的源码解析是基于v3.3.13。 现在就进入主题用ahooks 来封装 React要注意的时机? Fun...
阅读 569·2023-03-27 18:33
阅读 758·2023-03-26 17:27
阅读 657·2023-03-26 17:14
阅读 609·2023-03-17 21:13
阅读 542·2023-03-17 08:28
阅读 1830·2023-02-27 22:32
阅读 1326·2023-02-27 22:27
阅读 2208·2023-01-20 08:28