这就不叫react hook闭包陷阱

4,070 阅读3分钟

使用react hook常发生的state拿不到最新值问题,本质是react函数组件带来的闭包导致的。

问题来源

话不多说,直接上代码描述被称为react hook闭包陷阱的问题:

  const [a, setA] = React.useState(1);
  console.log("a_comp", a);

  React.useEffect(() => {
    setA(2);

    setTimeout(() => {
      console.log("a", a);
    }, 1000);
  }, []);

代码输出为:

a_comp 1
a_comp 2
a 1

按直觉来讲,打印a的语句远晚于a的更新,a的值却不是最新的,这会导致一些意料之外的bug。

而所谓的直觉,便是来一下一下代码:

let a = 1;

setTimeout(() => {
    console.log("a", a)
}, 100);

a = 2;

代码输出:

a 2

诚然 a = 1,这种赋值不是hook的赋值,setTimeout也没放在useEffect的回调里,是这两处差异导致最终打印的值不同吗?

网络上流行的解释是:

useEffect异步回调 or setTimeout的回调 带来的闭包将旧的a值保存到函数的作用域里,使得最终打印出来的是旧的值。

一般其他文章会分析hook的原理然后的出上述结论,但是只因为两个异步回调就导致输出值不同了吗?

我们把原生node代码也都放在异步回调里执行,代码如下:

let a = 1;

Promise.resolve().then(()=> {
    console.log("beforeSetTimeout:", a)
    setTimeout(() => {
        console.log("a", a)
    }, 100);
})

Promise.resolve().then(() => {
    a = 2;
})

代码输出:

beforeSetTimeout: 1
a 2

还是无法复现react闭包陷阱。在setTimeout放入回调之前a确实是1,不过最终a还是取到了最新值。

这印证了一句话,闭包拿到的是值的引用,被引用的a变了,打印的a也变了,那为什么react hook中拿不到最新值呢?下一部分进入现场复原。

现场复原

直接上代码,本文不细究hook具体细节,只当他们是异步执行的函数,且初次执行和二次执行行为不同,下面实现了丐中丐版的useEffect和useState。

// 实现useEffect
let firstEffect = true;
const useEffect = (callback, deps) => {
  if(deps.length === 0) {
    if(firstEffect) {
      setTimeout(() => {
        callback();
      }, 0)
      firstEffect = false;
    }
  }
}

// 实现useState
let firstState = true;
let state;
const useState = (initValue) => {
  if(firstState) {
    state = initValue;
    firstState= false;
  }
  const dispatch = (v) => {
    state = v;
    setTimeout(() => {
      Component()
    }, 50);;
  }
  return [state, dispatch];
}

// 实现函数组件
function Component() {
  const [a, setA] = useState(1)
  console.log("a_comp", a);

  useEffect(() => {
    setA(2);
    setTimeout(() => {
      console.log("a>>>", a);
    }, 1000)
  }, [])
}

Component();

代码输出:

a_comp 1
a_comp 2
a>>> 1

复现了!!!那么关键在哪里呢,我们保留了一部分函数组件的能力,这才能让你看的出这是react(手动狗头)。这部分能力就是函数中state更新时函数组件会重新执行,而我们发现,所谓保留的旧的a值,其实是函数组件重新执行带了的作用域隔绝效应,这时setTimeout中的回调只能拿旧的a值。

不信请看调试记录:

image

结论

  • 所谓react hook闭包陷阱应该叫react函数组件闭包陷阱,因为这个闭包值是函数组件再次执行带来的。

  • 看源码只是辅助,更重要的是实现细节。