Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useEffect与闭包 #47

Open
lihongxun945 opened this issue Dec 2, 2020 · 2 comments
Open

useEffect与闭包 #47

lihongxun945 opened this issue Dec 2, 2020 · 2 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented Dec 2, 2020

复现问题

从一个最简单的demo开始,计算按钮的点击次数:

function App() {
  const [count, setCount] = useState(0)
  const update = () => {
    setCount(count+1)
  }
  return (
    <div className="App">
      <span>COUNT: {count}</span> <button onClick={update}>+</button>
    </div>
  )
}

这样肯定是没有问题的,点击按钮可以正常计数。如果这个时候我们将需求改一下,每秒钟要自动计数一次,那么代码就变成这样了:

function App() {
  const [count, setCount] = useState(0)
  const update = () => {
    setCount(count+1)
  }
  useEffect(function autoCount() {// 取个名字方便讲解
    const interval = setInterval(function repeat() { // 取个名字方便讲解
      setCount(count+1) // 也可以写成update(),效果是一样的
    }, 1000)
    return () => clearInterval(interval)
  }, [])
  return (
    <div className="App">
      <span>COUNT: {count}</span> <button onClick={update}>+</button>
    </div>
  )
}

这段看似正常的代码其实执行之后会发现有问题,自动计数只会统计到1,不会继续增加了。那么原因是什么呢? 是因为 autoUpdate 只执行了一次,这个函数中使用的 count 是第一次执行的时候App 闭包里面的值,也就是 0 ,所以每一次执行都会是 0+1=1 ,结果永远是1。
有些小伙伴这里可能不明白了, update 函数不也是闭包里面吗,为啥里面取到的 count 就能更新呢? 要回答这个问题就要理解 useEffect 的执行机制

useEffect执行过程

不需要去看 useEffect 的源码,只需要根据官网的描述我们就可以理解其执行逻辑:

Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.

useEffect 在每次 DOM 更新之后执行,准确的说,在上述例子中是 autoCount 在每次 DOM 更新后执行。而我们的第2个参数 [] 是执行条件,这样会导致只有第1次真的执行了 autoCount ,后面每次DOM更新都不会再执行。
我们的函数 APP 的执行过程是这样的:

  1. 执行 App 函数
    a. 获取 count 最新的值
    b. autoCount 函数进入 effects 队列
    c. 返回VDOM
  2. VDOM渲染为DOM
  3. effects中取出 autoCount 函数,根据条件决定是否执行

用一张图来表示1次render的过程如下:
useeffect 执行过程
由于每一次 App 函数执行,都会读取新的 count 并创建新的 autoCount ,所以在新创建的 autoCount 函数内部读取的 count 值是最新的。然而我们定时更新不是定时执行最新的 autoCount ,而是在第1次的 count 内部不停执行 repeat 函数,因此读取的 count 值就永远是 1 。

update 函数执行没问题的原因是,每次更新都执行了 update ,所以总能取到最新的值。

理解的关键是要跳出class组件的思维。 在传统的class写法中,是同一个方法多次执行,每次执行后的结果不同是因为 this 上的数据不同,比如 render 方法。而在函数式组件中,组件内部声明的方法,如上例中 update , autoCount 等方法,每次执行App的时候都会声明一个新的,使用后就丢弃了。

解决方法

那么如果确实要做定时更新逻辑呢? 如何解决? 只要能读取最新的 count 值就可以,不要用闭包内的变量,而是直接用 setState 的回调函数语法获取最新值就可以了。

  useEffect(function autoCount() {// 取个名字方便讲解
    const interval = setInterval(function repeat() { // 取个名字方便讲解
      setCount((count) =>count+1) // 也可以写成update(),效果是一样的
    }, 1000)
    return () => clearInterval(interval)
  }, [])
@lishion
Copy link

lishion commented Sep 24, 2021

hi 大佬,看完你写的文章我自己模拟了一下。发现以下代码:

const effects = []
let v = 0;
function setV(val) {
    v = val
}

function useEffect(func) {
    effects.push(func)
}

useEffect(() =>  {
    setInterval(() => {
        setV(v + 1);
        console.info(v);
    }, 1000)
})

effects.pop()()

在 nodejs 中可以正确的打印 1, 2, 3, ...。这里的 effect 依然只执行了一次,这与在 react 中行为不太一致。是因为实现有什么差别吗? 大佬有空的话可以指导一下。

@lihongxun945
Copy link
Owner Author

@lishion 我觉得你并没有看懂这篇文章,你这段代码问题有点多不知道如何评价:

  1. 没有构建一个外层闭包
  2. v应该是个常量而不是变量

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants