简介
在 2022/5/5 ,react 的新 RFC useEvent 推出,解决的是一个旧问题:zh-hans.reactjs.org/docs/hooks-… 关于具体的问题和使用场景,关键词搜索 useEvent 就有大量的文章,个人这篇 也掰扯过这个问题。
在 useEvent 具体实现中有个细节经常被重复的讨论,即 useLayoutEffect 实际上也不符合预期的问题,这里希望通过最简单的表述来彻底阐明它,解答你可能的疑惑。
Hook 执行顺序
render 同步,useLayoutEffect 为 Fiber dfs 遍历的同步(时机上高仿可用微任务),useEffect 宏任务。 通过一个简单测试我们明确一下 FC 中 Hooks 执行时机:codesandbox.io/s/test-usee…
父组件 A 内有:
子组件 B 内有:
执行结果为:
则执行顺序是: 父组件render>子组件render> 子组件useLayoutEffect>父组件useLayoutEffect>子组件useEffect>父组件useEffect
useLayoutEffect 执行时机
简单演示:codesandbox.io/s/test-usee…
假如父组件 A 有 logCount 函数,使用 useLayoutEffect 版本的 useEvent 包裹,传给子组件作为 props
考虑 Hook 执行顺序:父组件render>子组件render> 子组件useLayoutEffect>父组件useLayoutEffect>子组件useEffect>父组件useEffect
则 子组件render阶段 和 子组件useLayoutEffect阶段 因为执行在父组件 useLayoutEffect (给ref赋值的时机) 之前,这两个阶段子组件B从props里读取的 logCount 函数不符合预期,是旧的或直接是undefine。这就是 useLayoutEffect 为什么不符合预期的问题。
同理可知官方文档 里使用 useEffect 也不符合预期,和上面一样,子组件在 父组件useEffect 执行前的时机中读取的 logCount 都会是旧的。
但 ref.current 是引用,待父组件的 useLayoutEffect 执行完,子组件内的 logCount 也会变成新的,此时就符合预期了。
直接放 render 里就符合预期,为什么要放 useLayoutEffect
目前绝大部分第三方 Hooks 对这个函数的实现都是赋值直接放 render 里,即不包任何东西:
考虑 Hook 执行顺序:父组件render>子组件render> 子组件useLayoutEffect>父组件useLayoutEffect>子组件useEffect>父组件useEffect
ref 赋值直接发生在 父组件render 过程中,子组件任何时候读取都符合预期,为什么包在 useLayoutEffect 里?
这个问题主要是为了 concurrent 模式,见React Issue #16956 ,在 concurrent 模式里,在rendering 过程中更新 ref 可能存在问题,Mutating refs during render can cause bugs。详细讨论可以看这里:github.com/facebook/re…
可能出现的新 Hook 执行时机
最合理的解决方式是官方提供一个【新的hook】,处于一个【新的执行时机】,正面解决【concurrent模式里 rendering 中更新 ref 可能不安全】 的问题,这个新的hook 的执行很明显需要在【组件render 之后】执行,并保持先父组件后子组件的执行顺序。插入原hook 执行顺序则为:
父组件render > 【父组件新hook】 >子组件render > 【子组件新hook】 > 子组件useLayoutEffect>父组件useLayoutEffect>子组件useEffect>父组件useEffect
这就是文章头图问题的解释:
In a real implementation, this would run before layout effects.
实际上要出的是一个新的 hook 它在 layout effects 之前执行,并保持先父后子的执行顺序。
结语
如果本文对你有一点帮助,求点赞收藏