导语: 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.
这句话很好理解,以下面计数器为例:
// useState中的Capture Value特性
function Counter(props) {
const [count, setCount] = useState(0);
return (
<div>
<p>当前count值: {count}</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
点击两次后,发生了两次 rerender:
// first render(初始)
function Counter(props) {
count = 0
// ...
<p>当前count值: 0</p>
}
// second render
function Counter(props) {
count = 1
// ...
<p>当前count值: 1</p>
}
// third render
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 = [] // 存放hooks
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 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。