Open jtwang7 opened 3 years ago
参考文章: react hooks 与闭包
Hooks 中的许多特性与闭包密切相关,例如 useState 为何能在组件渲染后持有状态而不是重新定义,以及 useEffect 的”快照“等。 函数式组件在每次渲染时,会从上到下执行一遍,因为函数组件本身其实就是函数,以至于官方文档没有提及这点,但这点又十分重要,函数组件这一特性导致了每次重新渲染组件时,其内部的变量都会被重新声明。
由于函数组件每次渲染都会重新执行函数本身,若我们需要保持一个变量不被重新声明,则可以将其提取到函数组件外定义。这样函数组件内使用该变量时就会产生闭包,函数组件本身反复执行并不会影响到该外部变量。
import React, { useEffect, useRef } from 'react'; import * as echarts from 'echarts'; // chart 实例 let lgChart = null; export default function LineGradient(props) { // 获取容器实例 let ref = useRef(null); // 获取 chart 实例 useEffect(() => { lgChart = echarts.init(ref.current) }, []) // do something with lgChart return ( <div ref={ref}></div> ) }
以项目中创建 echarts 实例为例,由于实例需要挂载到已渲染的 DOM 节点上,因此实例必须放在 useEffect 内部创建。由于我们后续一直会用到 lgChart 这个变量,因此 lgChart 变量的声明必须放在 useEffect 作用域之外。若 lgChart 声明在 useEffect 外但在函数组件内,那么后续操作若触发了重渲染,chart 实例就会被重新声明,我们就需要重新创建 chart 实例。 为了避免反复创建 chart 实例,我们可以将 chart 实例声明置于函数组件外。此外,useRef,useMemo 等方法可以实现在函数组件内部持有变量。
注:函数组件外闭包,useRef,useMemo,useCallback 等方法一般用于持有”静态变量“,其主要目的是为了”性能优化“。关于”动态变量(状态)“的持有我们采用 useState 或 useReducer 的方法。 这里的”静态变量“指变量改变时不会被 React 探知,不会引发重渲染的变量,类似于类组件中的静态方法和静态变量。 useState 与 useReducer 都可以实现对”动态变量(状态)“的持久化,两者都提供了在状态发生改变时触发组件 render 的方法。
useState 与 useReducer 都可以实现对”动态变量(状态)“的持久化,两者都提供了在状态发生改变时触发组件 render 的方法。
尽管 Hooks 的持久化为函数式组件持有状态变量提供了方法。但也导致了一些问题,在 React 官网 FAQ 中提到的 为什么我会在我的函数中看到陈旧的 props 和 state ? 就是 React 闭包导致的。
参考官方例子:
function Example() { const [count, setCount] = useState(0); function handleAlertClick() { setInterval(() => { alert('You clicked on: ' + count); }, 3000); } return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div> ); }
当点击 Show alert 时,handleAlertClick 就会执行,设置计时器并开始循环打印 count,此时 setInterval 内部的 count 引用了作用域外的变量 count,形成闭包,之后每隔 3s 打印 "You clicked on: 0"。若点击 Show alert 随后点击了 Click me,仍会每隔 3s 打印 "You clicked on: 0",尽管 setCount 触发了重渲染,但原先设置的 setInterval 中 count 仍持有的是上一个 count 的引用。
函数组件闭包的产生相当于对函数组件进行了一次“快照”。在函数组件成功渲染的那一刻,对函数组件进行一次“拍摄”,将函数组件内的变量都“定格”下来,后续的一些方法执行(如计时器,useEffect 内副作用执行等)都参照于本次“快照”。 之前我们提到,可以在 useEffect 中获取最新的 state 原因也是基于函数组件在渲染后产生闭包这一特点。setState 会在组件 render 前统一更新 state,组件更新完毕后会进行一次“快照”,此时产生的闭包就持有对更新后 state 的引用。 个人体会:渲染函数组件的整个过程,可以以 render 为界分为两个部分:“准备阶段” & “应用阶段”,准备阶段主要用于变量或方法的声明(同步执行)或变量的更新(setState 异步执行),应用阶段主要用于操纵变量与执行方法(useEffect)。
个人体会:渲染函数组件的整个过程,可以以 render 为界分为两个部分:“准备阶段” & “应用阶段”,准备阶段主要用于变量或方法的声明(同步执行)或变量的更新(setState 异步执行),应用阶段主要用于操纵变量与执行方法(useEffect)。
我们用上述“快照”思想来判断下述例子的执行结果:
export default function App() { const [loading, setLoading] = useState(false); const foo = () => { console.log("is loading ? ", loading); }; const bar = callback => { setTimeout(() => { setLoading(true); callback(); }, 5000); }; const runner = () => { foo(); bar(runner); }; useEffect(() => { console.log("run"); runner(); }, []); return ( <div className="App"> <h1>Is it loading ? {loading ? 'true' : 'false'}</h1> </div> ); }
整体代码流程如下:
闭包持有的是变量的值,而非对该变量的引用,这意味着即使后续变量更改,闭包仍持有更改前的变量状态。
请谨慎使用闭包! 在 React 中要针对不同场景,决定使用 useRef 还是闭包来保存变量。 使用规则请参考:React 填坑记录 - 相同组件复用时状态无法独立存储的问题
React - Hooks 与 JS 闭包
参考文章: react hooks 与闭包
前言
Hooks 中的许多特性与闭包密切相关,例如 useState 为何能在组件渲染后持有状态而不是重新定义,以及 useEffect 的”快照“等。 函数式组件在每次渲染时,会从上到下执行一遍,因为函数组件本身其实就是函数,以至于官方文档没有提及这点,但这点又十分重要,函数组件这一特性导致了每次重新渲染组件时,其内部的变量都会被重新声明。
闭包
由于函数组件每次渲染都会重新执行函数本身,若我们需要保持一个变量不被重新声明,则可以将其提取到函数组件外定义。这样函数组件内使用该变量时就会产生闭包,函数组件本身反复执行并不会影响到该外部变量。
以项目中创建 echarts 实例为例,由于实例需要挂载到已渲染的 DOM 节点上,因此实例必须放在 useEffect 内部创建。由于我们后续一直会用到 lgChart 这个变量,因此 lgChart 变量的声明必须放在 useEffect 作用域之外。若 lgChart 声明在 useEffect 外但在函数组件内,那么后续操作若触发了重渲染,chart 实例就会被重新声明,我们就需要重新创建 chart 实例。 为了避免反复创建 chart 实例,我们可以将 chart 实例声明置于函数组件外。此外,useRef,useMemo 等方法可以实现在函数组件内部持有变量。
闭包引发的 "BUG"
尽管 Hooks 的持久化为函数式组件持有状态变量提供了方法。但也导致了一些问题,在 React 官网 FAQ 中提到的 为什么我会在我的函数中看到陈旧的 props 和 state ? 就是 React 闭包导致的。
参考官方例子:
当点击 Show alert 时,handleAlertClick 就会执行,设置计时器并开始循环打印 count,此时 setInterval 内部的 count 引用了作用域外的变量 count,形成闭包,之后每隔 3s 打印 "You clicked on: 0"。若点击 Show alert 随后点击了 Click me,仍会每隔 3s 打印 "You clicked on: 0",尽管 setCount 触发了重渲染,但原先设置的 setInterval 中 count 仍持有的是上一个 count 的引用。
代码解析
我们用上述“快照”思想来判断下述例子的执行结果:
整体代码流程如下: