前言
在说React闭包问题时,我们来看一个例子
在这个 demo 中我们可以看到 num 值发生变化后 useCallback 内部的函数是取不到最新的值的
当我们吧 useCallback 第二个参数给到 num 的时候 就可以每次取到最新的值了,这就是典型的一个闭包问题
当我们点击 div 三次后 useCallback 每次都能取到最新值
造成这个现象我们用原生 js 解释一下
解释
let isMount = true // 用来记录是否首次渲染
let container; //存储callback
let state; //存储state
// 对比判断依赖性有没有发生变化
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
function useCallback(callback, deps) {
const nextDeps = deps === undefined ? null : deps;
if (!isMount && areHookInputsEqual(container[1], nextDeps)) {
// 如果不是首次渲染并且第二个依赖性没发生变化 返回上次的函数
return container[0]
}
container = [callback, nextDeps]
return callback
}
// 修改state
function dispatchAction(action) {
state = action
}
function useState(initialValue) {
if (isMount) {
state = initialValue;
}
return [
state,
dispatchAction
]
}
function App() {
const [num, setNum] = useState(0)
const fn = useCallback(() => {
console.log(num,' callback')
}, [])
fn()
isMount = false
return {
onClick() {
setNum(num + 1)
}
}
}
我们可以用 控制台 App().onClick() 去模拟组件点击
可以看到 callback 内部函数也是拿不到最新的值的
闭包 官方给的解释是一个函数对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包,在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
在上面的例子中可以这么理解:
- 当我们
App().onClick()模拟点击之后会重新调用一次App组件 - 每次调用
App会生成一个新的词法环境,但是因为我callback的第二个参数给的是空数组,也就是说他会吧上次的函数缓存下来,之后调用的时候上次的函数会有自己的一个词法环境,取到的一直是第一次的值 - 当我们把
useCallback第二个参数给到num,可以看到每次修改num会生成一个新的函数,新的函数有自己最新的词法作用域,所以会取到最新的值
源码角度
只要是第二个参数有 deps 道理都是类似的,
这里我们从useCallback的角度去看,
源码中 callback 分为 mountCallback 和 updateCallback
mountCallback
- 图中
mountWorkInProgressHook调用的意思是会生成一个新的hook节点添加到fiber中 并且将workInProgressHook指针指向当前的hook - 可以看到如果
deps不传的的话会当null进行处理 - 第三行给hook节点了一个初始值
callback用来记录当前的函数。nextDeps是当前的依赖
updateCallback
更新阶段我们可以直接看 图中标红圈的部分
这里会用 Object.is() 去判断本次的deps 和上次的deps是否一样,如果一样的话 会返回上一次的缓存的函数,否则返回本次的函数
总结
闭包是一个函数调用,会生成一个新的词法环境,当取到的值不是最新的 ,就会出现上述的闭包问题
遇到上述情况的解决办法也很简单:正确的利用第二个 deps 给到依赖项