从react hooks“闭包陷阱”切入,浅谈react hooks

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

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其余的特性。若是有错误,但愿各位同窗能救救孩子,不要让我带着错误的认知活下去了。。。闭包

一、一个熟悉的闭包场景

首先从一个各位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 的值,异步定时器的回调函数才如咱们想要的打印了顺序的值。ui

其实,useEffect 的哪一个场景的缘由,跟这个,简直是同样的,useEffect 闭包陷阱场景的出现,是 react 组件更新流程以及 useEffect 的实现的天然而然结果spa

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(()=>{
        setInterval(()=>{
            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 的认识是比之前深了。 但愿也能对其余以前跟我有一样疑惑的人有所帮助。 若是对你有帮助,答应我,请不要吝啬你的赞好吗。

相关文章
相关标签/搜索