react hooks闭包陷阱切入浅谈

目录
  • 引言
  • 1、一个熟悉的闭包场景
  • 2 浅谈hooks原理,理解useEffect 的 “闭包陷阱” 出现原因
  • 2 难道真的要在依赖数组里写上的值,才能拿到新鲜的值?
  • 3 为什么使用useRef能够每次拿到新鲜的值?
  • 4 完毕

引言

首先,本文并不会讲解 hooks 的基本用法, 本文从 一个hooks中 “奇怪”(其实符合逻辑) 的 “闭包陷阱” 的场景切入,试图讲清楚其背后的因果。同时,在许多 react hooks 奇技淫巧的文章里,也能看到 useRef 的身影,那么为什么使用 useRef 又能摆脱 这个 “闭包陷阱” ? 我想搞清楚这些问题,将能较大的提升对 react hooks 的理解。

react hooks 一出现便受到了许多开发人员的追捧,或许在使用react hooks 的时候遇到 “闭包陷阱” 是每个开发人员在开发的时候都遇到过的事情,有的两眼懵逼、有的则稳如老狗瞬间就定义到了问题出现在何处。

(以下react示范demo,均为react 16.8.3 版本)

你一定遭遇过以下这个场景:

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    }, [])
}

在这个定时器里面去打印 count 的值,会发现,不管在这个组件中的其他地方使用 setCountcount 设置为任何值,还是设置多少次,打印的都是1。是不是有一种,尽管历经千帆,我记得的还是你当初的模样的感觉? hhh... 接下来,我将尽力的尝试将我理解的,为什么会发生这么个情况说清楚,并且浅谈一些hooks其他的特性。如果有错误,希望各位同学能救救孩子,不要让我带着错误的认知活下去了。。。

1、一个熟悉的闭包场景

首先从一个各位jser都很熟悉的场景入手。

for ( var i=0; i<5; i++ ) {
    setTimeout(()=>{
        console.log(i)
    }, 0)
}

想宝宝我刚刚毕业的那一年,这道题还是一道有些热门的面试题目。而如今...

我就不说为什么最终,打印的都是5的原因了。直接贴出使用闭包打印 0...4的代码:

for ( var i=0; i<5; i++ ) {
   (function(i){
         setTimeout(()=>{
            console.log(i)
        }, 0)
   })(i)
}

这个原理其实就是使用闭包,定时器的回调函数去引用立即执行函数里定义的变量,形成闭包保存了立即执行函数执行时 i 的值,异步定时器的回调函数才如我们想要的打印了顺序的值。

其实,useEffect 的哪个场景的原因,跟这个,简直是一样的,useEffect 闭包陷阱场景的出现,是 react 组件更新流程以及 useEffect 的实现的自然而然结果。

2 浅谈hooks原理,理解useEffect 的 “闭包陷阱” 出现原因

其实,很不想在写这篇文章的过程中,牵扯到react原理这方面的东西,因为真的是太整体了(其实主要原因是菜,自己也只是掌握的囫囵吞枣),你要明白这个大概的过程,你得明白支撑起这个大概的一些重要的点。

首先,可能都听过react的 Fiber 架构,其实一个 Fiber节点就对应的是一个组件。对于 classComponent 而言,有 state 是一件很正常的事情,Fiber对象上有一个 memoizedState 用于存放组件的 state

ok,现在看 hooks 所针对的 FunctionComponnet。 无论开发者怎么折腾,一个对象都只能有一个 state 属性或者 memoizedState 属性,可是,谁知道可爱的开发者们会在 FunctionComponent 里写上多少个 useStateuseEffect 等等 ? 所以,react用了链表这种数据结构来存储 FunctionComponent 里面的 hooks。比如:

function App(){
    const [count, setCount] = useState(1)
    const [name, setName] = useState('chechengyi')
    useEffect(()=>{
    }, [])
    const text = useMemo(()=>{
        return 'ddd'
    }, [])
}

在组件第一次渲染的时候,为每个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语句中了把?因为这样可能会导致顺序错乱,导致当前hooks拿到的不是自己对应的Hook对象。

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

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=&gt;{
        setInterval(()=&gt;{
            console.log(count)
        }, 1000)
    }, [])
    function click(){ setCount(2) }
}

好,开动脑袋开始想象起来,组件第一次渲染执行 App(),执行 useState 设置了初始状态为1,所以此时的 count 为1。然后执行了 useEffect,回调函数执行,设置了一个定时器每隔 1s 打印一次 count

接着想象如果 click 函数被触发了,调用 setCount(2) 肯定会触发react的更新,更新到当前组件的时候也是执行 App(),之前说的链表已经形成了哈,此时 useStateHook 对象 上保存的状态置为2, 那么此时 count 也为2了。然后在执行 useEffect 由于依赖数组是一个空的数组,所以此时回调并不会被执行。

ok,这次更新的过程中根本就没有涉及到这个定时器,这个定时器还在坚持的,默默的,每隔1s打印一次 count。 注意这里打印的 count ,是组件第一次渲染的时候 App() 时的 countcount的值为1,因为在定时器的回调函数里面被引用了,形成了闭包一直被保存。

2 难道真的要在依赖数组里写上的值,才能拿到新鲜的值?

仿佛都习惯性都去认为,只有在依赖数组里写上我们所需要的值,才能在更新的过程中拿到最新鲜的值。那么看一下这个场景:

function App() {
  return <Demo1 />
}
function Demo1(){
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(10)
  const text = useMemo(()=>{
    return `num1: ${num1} | num2:${num2}`
  }, [num2])
  function handClick(){
    setNum1(2)
    setNum2(20)
  }
  return (
    <div>
      {text}
      <div><button onClick={handClick}>click!</button></div>
    </div>
  )
}

text 是一个 useMemo ,它的依赖数组里面只有num2,没有num1,却同时使用了这两个state。当点击button 的时候,num1和num2的值都改变了。那么,只写明了依赖num2的 text 中能否拿到 num1 最新鲜的值呢?

如果你装了 react 的 eslint 插件,这里也许会提示你错误,因为在text中你使用了 num1 却没有在依赖数组中添加它。 但是执行这段代码会发现,是可以正常拿到num1最新鲜的值的。

如果理解了之前第一点说的“闭包陷阱”问题,肯定也能理解这个问题。

为什么呢,再说一遍,这个依赖数组存在的意义,是react为了判定,在本次更新中,是否需要执行其中的回调函数,这里依赖了的num2,而num2改变了。回调函数自然会执行, 这时形成的闭包引用的就是最新的num1和num2,所以,自然能够拿到新鲜的值。问题的关键,在于回调函数执行的时机,闭包就像是一个照相机,把回调函数执行的那个时机的那些值保存了下来。之前说的定时器的回调函数我想就像是一个从1000年前穿越到现代的人,虽然来到了现代,但是身上的血液、头发都是1000年前的。

3 为什么使用useRef能够每次拿到新鲜的值?

大白话说:因为初始化的 useRef 执行之后,返回的都是同一个对象。写到这里宝宝又不禁回忆起刚学js那会儿,捧着红宝书啃时候的场景了:

var A = {name: 'chechengyi'}
var B = A
B.name = 'baobao'
console.log(A.name) // baobao

对,这就是这个场景成立的最根本原因。

也就是说,在组件每一次渲染的过程中。 比如 ref = useRef() 所返回的都是同一个对象,每次组件更新所生成的ref指向的都是同一片内存空间, 那么当然能够每次都拿到最新鲜的值了。犬夜叉看过把?一口古井连接了现代世界与500年前的战国时代,这个同一个对象也将这些个被保存于不同闭包时机的变量了联系了起来。

使用一个例子或许好理解一点:

    /* 将这些相关的变量写在函数外 以模拟react hooks对应的对象 */
	let isC = false
	let isInit = true; // 模拟组件第一次加载
	let ref = {
		current: null
	}
	function useEffect(cb){
		// 这里用来模拟 useEffect 依赖为 [] 的时候只执行一次。
 		if (isC) return
		isC = true
		cb()
	}
	function useRef(value){
		// 组件是第一次加载的话设置值 否则直接返回对象
		if ( isInit ) {
			ref.current = value
			isInit = false
		}
		return ref
	}
	function App(){
		let ref_ = useRef(1)
		ref_.current++
		useEffect(()=>{
			setInterval(()=>{
				console.log(ref.current) // 3
			}, 2000)
		})
	}
		// 连续执行两次 第一次组件加载 第二次组件更新
	App()
	App()

所以,提出一个合理的设想。只要我们能保证每次组件更新的时候,useState 返回的是同一个对象的话?我们也能绕开闭包陷阱这个情景吗? 试一下吧。

function App() {
  // return <Demo1 />
  return <Demo2 />
}
function Demo2(){
  const [obj, setObj] = useState({name: 'chechengyi'})
  useEffect(()=>{
    setInterval(()=>{
      console.log(obj)
    }, 2000)
  }, [])
  function handClick(){
    setObj((prevState)=> {
      var nowObj = Object.assign(prevState, {
        name: 'baobao',
        age: 24
      })
      console.log(nowObj == prevState)
      return nowObj
    })
  }
  return (
    <div>
      <div>
        <span>name: {obj.name} | age: {obj.age}</span>
        <div><button onClick={handClick}>click!</button></div>
      </div>
    </div>
  )
}

简单说下这段代码,在执行 setObj 的时候,传入的是一个函数。这种用法就不用我多说了把?然后 Object.assign 返回的就是传入的第一个对象。总儿言之,就是在设置的时候返回了同一个对象。

执行这段代码发现,确实点击button后,定时器打印的值也变成了:

{
    name: 'baobao',
    age: 24
}

4 完毕

通过一次“闭包陷阱” 浅谈 react hooks 全文再此就结束了。 反正写完了这篇文章,我对 hooks 的认识是比以前深了,更多关于react hooks闭包的资料请关注我们其它相关文章!

(0)

相关推荐

  • ahooks整体架构及React工具库源码解读

    目录 引言 React hooks utils 库 ahooks 简介 特点 hooks 种类 ahooks 整体架构 项目启动 整体结构 hooks 总结 引言 本文是深入浅出 ahooks 源码系列文章的第一篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码解析是基于 v3.3.13.自己

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

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

  • React中10种Hook的使用介绍

    目录 React Hook是什么? React目前提供的Hook 1.useState 2.useEffect & useLayoutEffect 3.useMemo & useCallback 4.useRef 5.useContext 6.useReducer React Hook是什么? React官网是这么介绍的: Hook 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 完全可选的 你无需重写任何已有

  • ahooks正式发布React Hooks工具库

    目录 起因 解法 共建 项目目标 品牌升级 社区开源 API 规范 示例演示 开发迭代 下一步 起因 从 React Hooks 正式发布到现在,越来越多的项目正在使用 Function Component 替代 Class Component,Hooks 这一新特性也逐渐被广泛的使用. 然而在实践的过程中,我们发现在很多常见的场景下,大部分逻辑是重复且可被复用的,如对数据请求的逻辑处理,对防抖节流的逻辑处理等,同样的代码经常会在同一个或不同的项目中被重复的编写 . 另一方面,由于 Hooks

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

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

  • React Hooks与setInterval的踩坑问题小结

    目录 一.需求 二.解决方案 1.函数式更新 2.使用useRef 3.用useReducer 4.自定义的hooks 一.需求 我们希望有一个每一秒自动+1的定时器 function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval

  • react hooks闭包陷阱切入浅谈

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

  • 关于 React 中 useEffect 使用问题浅谈

    目录 前言 优化前 优化后 总结 前言 最近看了一下 ant-design 中的 tree 组件源码时发现 useEffect 中根据 props 来计算当前函数组件的 state 的,感到好奇,因为这样会导致应用重新绘制一次,这样才复杂场景下会对应用有一定的性能影响.为了验证自己猜想是否正确做了一下实践.这里的 React 是官方 16.12.0的源码. 优化前 import * as React from './react-source/packages/react' import * as

  • 浅谈react useEffect闭包的坑

    问题代码 看一段因为useEffect导致的闭包问题代码 const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('clic

  • 浅谈React中组件逻辑复用的那些事儿

    基本每个开发者都需要考虑逻辑复用的问题,否则你的项目中将充斥着大量的重复代码.那么 React 是怎么复用组件逻辑的呢?本文将一一介绍 React 复用组件逻辑的几种方法,希望你读完之后能够有所收获.如果你对这些内容已经非常清楚,那么略过本文即可. 我已尽量对文中的代码和内容进行了校验,但是因为自身知识水平限制,难免有错误,欢迎在评论区指正. 1. Mixins Mixins 事实上是 React.createClass 的产物了.当然,如果你曾经在低版本的 react 中使用过 Mixins,

  • 浅谈react 16.8版本新特性以及对react开发的影响

    目录 react16.8版本更新 useEffect react16.8版本更新解决了什么问题 组件复用更便捷 hooks和reactdiff算法 总结 Facebook团队对社区上的MVC框架都不太满意的情况下,开发了一套开源的前端框架react,于2013年发布第一个版本. react最开始倡导函数式编程,使用function以及内部方法React.creactClass创建组件,之后在ES6推出之后,使用类组件Class构建包含生命周期的组件. react 16.8版本更新 react16

  • 浅谈React 中的浅比较是如何工作的

    React 中浅比较的概念无处不在,它在不同的流程中起着关键的作用,也可以在React组件的多个生命周期中找到.比如,React Hooks中的依赖数组,通过React.memo进行记忆.在React的官方文档中也多次提到“浅比较”这个概念,下面我们就来看看React中的浅比较是如何工作的! 想要理解浅比较的概念,最直接的方法就是研究React的源代码,下面就来看看React中的shallowEqual.js 文件: import is from './objectIs'; import has

  • 浅谈React Refs 使用场景及核心要点

    目录 什么是 Refs? 使用方式 Refs 核心要点 避免重复创建 ref 内容 ref.current 存储的内容修改是突变 ref 作为数据存储时内容的变化不会引起 re-render ref 的读写只能在 useEffect 或者回调函数中进行 跨组件传递ref 获取dom时需要借助 forwardRef 包裹组件 ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为null 最佳实践 dom 操作相关 用于在两次 render 之间传递数据 在使用 React 进

  • React Hooks useReducer 逃避deps组件渲染次数增加陷阱

    目录 前言 自定义 Hooks 简单实现 在组件中使用自定义 Hooks 提前阻止 dispatch 触发 优化后再测试 结论 题外 前言 在快乐使用 React Hooks 开发自定义 Hooks 过程中,使用了 useEffect,useReducer,useRef,useCallback 等官方提供的 Hooks,将一些通用逻辑抽离出来,提高代码复用性. 但在组合使用 useEffect,useReducer,React.memo 时,发生了组件在状态未发生变化时触发渲染,因为此动作发生在

  • 浅谈React 属性和状态的一些总结

    一.属性 1.第一种使用方法:键值对 <ClaaNameA name = "Tom" /> <ClaaNameA name = {Tom} /> <ClaaNameA name = {"Tom"} /> <ClaaNameA name = {[1,2,3]} />//数组 <ClaaNameA name = {FunctionNAme} /> //定义一个函数 2.第二种方法:三个点的展开对象形式 var

随机推荐