React hooks 的闭包陷阱揭秘

221 阅读2分钟

前言

在说React闭包问题时,我们来看一个例子

image.png

在这个 demo 中我们可以看到 num 值发生变化后 useCallback 内部的函数是取不到最新的值的

当我们吧 useCallback 第二个参数给到 num 的时候 就可以每次取到最新的值了,这就是典型的一个闭包问题

image.png

当我们点击 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() 去模拟组件点击

image.png

可以看到 callback 内部函数也是拿不到最新的值的

闭包 官方给的解释是一个函数对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包,在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

在上面的例子中可以这么理解:

  1. 当我们 App().onClick() 模拟点击之后会重新调用一次 App 组件
  2. 每次调用 App 会生成一个新的词法环境,但是因为我 callback 的第二个参数给的是空数组,也就是说他会吧上次的函数缓存下来,之后调用的时候上次的函数会有自己的一个词法环境,取到的一直是第一次的值
  3. 当我们把 useCallback 第二个参数给到 num,可以看到每次修改 num 会生成一个新的函数,新的函数有自己最新的词法作用域,所以会取到最新的值

image.png

image.png

源码角度

只要是第二个参数有 deps 道理都是类似的, 这里我们从useCallback的角度去看,

源码中 callback 分为 mountCallbackupdateCallback

mountCallback

image.png

  1. 图中 mountWorkInProgressHook 调用的意思是会生成一个新的 hook 节点添加到 fiber 中 并且将workInProgressHook 指针指向当前的 hook
  2. 可以看到如果 deps 不传的的话会当 null 进行处理
  3. 第三行给hook节点了一个初始值 callback 用来记录当前的函数。nextDeps 是当前的依赖

updateCallback

image.png

更新阶段我们可以直接看 图中标红圈的部分

这里会用 Object.is() 去判断本次的deps 和上次的deps是否一样,如果一样的话 会返回上一次的缓存的函数,否则返回本次的函数

总结

闭包是一个函数调用,会生成一个新的词法环境,当取到的值不是最新的 ,就会出现上述的闭包问题

遇到上述情况的解决办法也很简单:正确的利用第二个 deps 给到依赖项