导语: 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 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

五、参考

Hooks探秘

《useEffect完全指南》