导语: Capture Value 是 React Hooks 中很重要的细节点,本篇文章将从一个实际需求的例子出发,对 Capture Value 进行介绍。
一、从一个例子说起
在 React 应用中异步需求很常见。现在有一个小需求:实现一个按钮默认显示 false,点击后立即更改为 true,两秒后变回 false。
代码如下,自己试试!
const Demo = (props) => { const [flag, setFlag] = useState(false); let timer; function handleClick() { setFlag(!flag);
timer = setTimeout(() => { setFlag(!flag); }, 2000); } useEffect(() => { return () => { clearTimeout(timer) } }) return ( <button onClick={handleClick}>{flag ? "true" : "false"}</button> ); } 复制代码
|
二、 Capture Value 介绍
Capture Value 从字面上可以理解为固化的值。
flag 作为 useState 的返回值,被上升为了状态。 React.useState 返回的实际是 [hook.memorizedState, dispatch],分别对应了我们接收的值和变更方法。当 setFlag 被调用时,hook.memorizedState 重新指向了 newState(注意:不是修改,而是重新指向)。但在 setTimeout 中的 flag 依然指向了旧的状态,因此得不到新的值。(即读的是旧值,但写的是新值,不是同一个
若对源码了解不多也没有关系,可以把每一次 render 理解为一次快照。
Each Render Has Its Own Props and State.
这句话很好理解,以下面计数器为例:
function Counter(props) { const [count, setCount] = useState(0);
return ( <div> <p>当前count值: {count}</p> <button onClick={() => setCount(count + 1)}>点击</button> </div> ); } 复制代码
|
点击两次后,发生了两次 rerender:
function Counter(props) { count = 0 <p>当前count值: 0</p> }
function Counter(props) { count = 1 <p>当前count值: 1</p> }
function Counter(props) { count = 2 <p>当前count值: 2</p> } 复制代码
|
初始状态下 count 值为 0。随着按钮被点击,在每次 Render 过程中,count 的值都会被固化为 1、2。每一次 Render 都是一个独立的过程,这个特性就是 **”Capture Value”**。
当然,除了 useState,事件处理函数以及useEffect都有自己的Capture Value特性。
// useEffect(还是以上面的计数器为例) function Counter(props) { const [count, setCount] = useState(0);
useEffect(() => { console.log('useEffect count: ', count) }) return ( //... ); } 复制代码
|
同上,随着点击,count 的状态在 useEffect 中也被固化为 1, 2, 3, …
总结一下:只要变量上升为了状态,把每一次 Render 理解为一次快照,每个快照独立,而每一次状态都被固化在了这个快照中(无论是在处理函数中还是在useEffect中)。
三、如何绕过 Capture Value
以文章开头的需求为例,按照上面的理解,我们现在可以用最简单的方式来解释这一 bug 的原因。
const [flag, setFlag] = useState(false) function handleClick() { setFlag(!flag);
timer = setTimeout(() => { setFlag(!flag); }, 2000); } 复制代码
|
首次点击按钮后,产生一个快照 :
falg = false;
function handleClick() { setFlag(true);
timer = setTimeout(() => { setFlag(true); }, 2000); }
复制代码
|
所以,2s 后 flag 依然 true。
要解决这个问题,很容易想到把上次的状态保存起来。
useRef 在这个时候就能派上用场啦~
自己试试!
const Demo = (props) => { const flagRef = useRef(flag); flagRef.current = flag; function handleClick() { setFlag(!flagRef.current);
setTimeout(() => { setFlag(!flagRef.current); }, 2000); }
} 复制代码
|
问题解决。(当然Demo只是用于展示 flag 的 Capture Value,还有些细节在此没有多做考虑)。
四、原理
掌握了 Capture Value,对 hooks 的工作原理也就熟知大半,帮助我们开发更加优质的代码。
有兴趣的话,还可以深究一下底层原理。
一个简易版的 React Hooks 实现:
let memorizedState = [] let cursor = 0 let lastRef
function useState(intialState) { memorizedState[cursor] = memeorizedState[cursor] || initialState const currentCursor = cursor; function setState(newState) { memorizedState[currentCursor] = newState render() } return [ memorizedState[cursor++], setState] }
function useEffect(callback, depArr) { const noDepArr = !depArr const deps = memorizedState[cursor] const hasDepsChanged = deps ? !depArr.every((el, i) => el === deps[i]) : true if (noDepArr || hasDepsChanged) { callback() memorizedState[cursor] = depArr } cursor++ }
function useRef(value){ lastRef = lastRef || { current: value } return lastRef } 复制代码
|
所以产生 Capture Value 的原因,正是每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
五、参考
Hooks探秘
《useEffect完全指南》