用函数闭包的思想去解释 Hooks 的 Capture Value 特性

useEffect 完整指南 中谈到过:

Effect拿到的总是定义它的那次渲染中的props和state。这能够避免一些bugs,但在一些场景中又会有些讨人嫌。对于这些场景,你可以明确地使用可变的ref保存一些值。

我们先来看一个 Capture Value 的例子:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

如果我点击了很多次并且在effect里设置了延时,打印出来的结果会是什么呢?

结果并不会是我们想的和类组件一样输出多个最新的 count 值。而是顺序的打印输出 — 每一个都属于某次特定的渲染,因此有它该有的count值。

image

如果我们把 Hooks 函数组件当成普通的函数,从函数闭包的角度就能很好的理解这一特性了。

let _state = 0;

function useState(initialValue) {
  _state = _state | initialValue;
  function setState(newState) {
    _state = newState;
    // 会重新执行组件函数
    // render();
  }
  return [_state, setState];
}

function Component() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      console.log("You clicked on: " + count);
    }, 3000);
  };
  // 暴露页面上可以执行的函数
  return [handleClick, setCount];
}

// 初次 render
const [handleClick, setCount] = Component();

// 模拟点击按钮
handleClick();
setCount(1); // 此时会重新执行 Component(),并返回新的 handleClick
setCount(2);

先理解一下闭包的一些概念:
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。
也就是说,闭包可以让你从内部函数访问外部函数作用域。
在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

每次 render ,函数组件重新执行,生成并返回了和当前词法环境绑定的函数,函数组件闭包词法环境中的 state,props 是不会改变的。
当 handleClick() 执行时,输出的必然是初次 render 是的 count ,即 0 。3 秒之内无论多少次 setCount(),改变 count 的值,也只是每次 setCount() 后重新执行组件函数的新的词法环境的 count 值被改变了,并返回了新的 handleClick,并不会影响到第一次 render 闭包词法环境中的 count 。

所以打印结果是 You clicked on: 0

从而也解释了为什么 每一次渲染都有它自己的 Props、State and Effects,每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state

以上只是个人的理解,可能有不正确的地方,欢迎指正,谢谢~

posted @ 2020-06-01 03:09  xlupc  阅读(815)  评论(0编辑  收藏  举报