再学 React Hooks (一):闭包陷阱

这是我参与更文挑战的第 9 天,活动详情查看:更文挑战

什么是闭包陷阱

const FunctionComponent = () => {
  const [value, setValue] = useState(1)
  const log = () => {
    setTimeout(() => {
      alert(value)
    }, 1000);
  }
  return (
    <div>
      <p>FunctionComponent</p>
      <div>value: {value}</div>
      <button onClick={() => setValue(value + 1)}>add</button>
      <br/>
      <button onClick={log}>alert</button>
    </div>
  )
}
复制代码

在上面的函数式组件中,我们点击 alert 按钮后会在 1s 后弹出 value 的值,我们在这 1s 的时间内可以继续点击 add 按钮增加 value 的值。

image-20210612154401986

上图是我们操作的结果。我们发现弹出的值和当前页面显示的值不相同。换句话说:log 方法内的 value 和点击动作触发那一刻的 value 相同,value 的后续变化不会对 log 方法内的 value 造成影响。这种现象被称为“闭包陷阱”或者被叫做“Capture Value” :函数式组件每次render 都会生产一个新的 log 函数,这个新的 log 函数会产生一个在当前这个阶段 value 值的闭包。

上面例子 “闭包陷阱” 的分析:

  1. 初始次渲染,生成一个 log 函数(value = 1)
  2. value 为 1 时,点击 alert 按钮执行 log 函数(value = 1)
  3. 点击按钮增加 value,比如 value 增加到 6,组件 render ,生成一个新的 log 函数(value = 6)
  4. 计时器触发,log 函数(value = 1)弹出闭包内的 value 为 1

解决闭包陷阱的方案

使用 useRef 解决闭包陷阱的问题

const FunctionComponent = () => {
  const [value, setValue] = useState(1)
  const countRef = useRef(value)

  useEffect(() => {
    countRef.current = value
  }, [value])

  const log = useCallback(
    () => {
      setTimeout(() => {
        alert(countRef.current)
      }, 1000);
    },
    [value],
  )

  return (
    <div>
      <p>FunctionComponent</p>
      <div>value: {value}</div>
      <button onClick={() => setValue(value + 1)}>add</button>
      <br/>
      <button onClick={log}>alert</button>
    </div>
  )
}

复制代码

useRef 每次 render 时都会返回同一个引用类型的对象,我们设置值和读取值都在这个对象上处理,这样就能获取到最新的 value 值了。

更新 State 时的回调函数

Effect Hook 可以让你在函数组件中执行副作用操作

假设现在我们要开一个每秒自增的计数器,我们一般会写出下面这样的代码:

const Counter = () => {
  const [value, setValue] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('new value:', value+1)
      setValue(value + 1)
    }, 1000);
    return () => {
      clearInterval(timer)
    }
  }, [])

  return (
    <div>
      <p>Counter</p>
      <div>count: {value}</div>
    </div>
  )
}
复制代码

上面的代码中,我们在 useEffect 中不断更新 value 的值,但是结合我们之前的闭包陷阱问题来分析,我们可以发现定时器的value值永远都会是 0,这就导致每次设置的 value 值都是 1,下图是运行的结果。

image-20210612163548629

“闭包陷阱” 最大的问题就是在函数数内无法获取的最新的 state 的值,那 React 提供了哪些方法来解决呢?

  1. useRef 上面已有介绍
  2. useState 更新值时传入回调函数

除了上面介绍的 useRef 的方法外,我们也可以在更新 state 时我们传入回调函数(回调函数里取到的值是最新的)。

const [value, setValue] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      // 回调函数的最新值
      setValue(value => value + 1)
    }, 1000);
    return () => {
      clearInterval(timer)
    }
  }, [])
复制代码

image-20210612170112557

闭包陷阱和 Hooks 依赖

useEffectuseLayoutEffectuseCallbackuseMemo 的第二个参数为依赖数组,依·赖数组中任意一个依赖变化(浅比较)会有如下效果:

  1. useEffectuseLayoutEffect 内部的副作用函数会执行,并且副作用函数可以获取到当前所有依赖的最新值。
  2. useCallbackuseMemo 会返回新的函数或对象,并缺内部的函数也能获取到当前所有依赖的最新值。

利用这个机制理论可以解决“闭包陷阱”,但是在某种情况下不适用:

const Counter = () => {
  const [value, setValue] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('tick:', value+1)
      setValue(value + 1)
    }, 1000);
    return () => {
    	console.log('clear')
      clearInterval(timer)
    }
  - }, [])
  + }, [value])

  return (
    <div>
      <p>Counter</p>
      <div>count: {value}</div>
    </div>
  )
}
复制代码

上面的代码我们把 value 作为依赖项加入到依赖数组,却是能够实现功能,但是每次都会经历 clearInterval -> setValue ->clearInterval 的循环。这就造成了不必要的性能消耗。还有一种极端的情况,如果我们没有返回取消定时器的函数,就会不断增加新的定时器

使用 Hook 依赖的注意事项

事件订阅

现在我们有如下的场景

useEffect(() =>{
	someThing.subscribe(() => {
		// do something with value
	})
}, [value])
复制代码

上面的代码中,value 变化会不断订阅新的事件。所以在 EffectHook 中我们记得返回取消副作用的函数

useEffect(() =>{
	someThing.subscribe(() => {
		// do something with value
	})
	return () => {
		+ // 添加取消副作用的函数
	}
}, [value])
复制代码

防抖节流

function BadDemo() {
  const [count, setCount] = useState(1);
  const [, setRerender] = useState(false);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  useEffect(() => {
    // 每500ms,组件重新render
    setInterval(() => {
      setRerender(r => !r);
    }, 500);
  }, []);
  return <div onClick={handleClick}>{count}</div>;
}

作者:蚂蚁保险体验技术
链接:https://juejin.cn/post/6844904090032406536
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

比如上面的代码,我们有一个需要防抖的 handleClick 函数,但是我们函数会不时地渲染,每次 render 都会生成一个新的函数,那么这个防抖的函数就失去了作用

总结

最后总结一下:

  1. React Hooks 存在“闭包渲染”的问题,每次 render 都会闭包缓存当前render对应的 state
  2. 可以通过 useRefstate 更新时的回调函数来解决这个问题
  3. 使用 EffectHook 依赖时要注意取消副作用

参考资料

分类:
前端
标签: