useEffect 为什么不能支持 async function?

2021-02-20 15:27:36 +08:00
 wxsm
  useEffect(async () => {
   await loadContent();
  }, []);

这种写法,应该是非常常见的需求。但是 React 本身并不支持这么做,理由是 effect function 应该返回一个销毁函数,如果用上了 async,返回值则变成了 Promise,会导致 react 在调用销毁函数的时候报错 (function.apply is undefined)。

因此,React 推荐的两种写法:

  useEffect(() => {
    // Create an scoped async function in the hook
    async function anyNameFunction() {
      await loadContent();
    }
    // Execute the created function directly
    anyNameFunction();
  }, []);

和:

  useEffect(() => {
    // Using an IIFE
    (async function anyNameFunction() {
      await loadContent();
    })();
  }, []);

我觉得都非常地不优雅,极度增加心智负担。

  1. 如无必要,勿增实体。第一种方式除了定义了一个无意义的函数(还得想办法给它取名)以外,IDE 还会报 warning ( Promise 未处理)。
  2. 至于方式二,更加无力吐槽。ES6 时代都 5 年了,还搞 IIFE,纯粹就是恶心人。

React 判断 useEffect 的传参到底是纯函数还是 Promise 非常简单,它为什么不做呢?这到底是设计缺陷,还是 React 偷懒,还是我傻逼?

6519 次点击
所在节点    程序员
74 条回复
wxsm
2021-02-20 16:52:32 +08:00
@noe132 可惜我司的项目已经被 redux 套牢了
wxsm
2021-02-20 16:57:11 +08:00
@iahu 第一,hooks 目前并不是全部要求同步的,至少 useCallback 可以直接定义 async 函数。第二,useEffect 现在的做法也只能是假同步,要求别人在 effet 里必须写纯函数,然而又建议有异步需求的人在纯函数里面写 IIFE,这种做法也同样无法保证销毁函数与 effect 函数的时序性。这就是单纯的简化了自己的工作,把困难的工作交给了用户。
Kasumi20
2021-02-20 17:19:23 +08:00
useEffect 需要立即同步地返回一个清除函数,你为了用一个 await 语法,返回一个 Promise 给 React 干嘛
joesonw
2021-02-20 17:24:55 +08:00
useEffect(fn: () => () => void).

useEffect 是要返回一个函数() => void 的, 作为 unmount 的时候回调用的. async 返回的话就变成 () => Promise<void>了. 签名不对
iahu
2021-02-20 18:07:09 +08:00
- useCallback 的签名是 `(a, b) => a` 第一个参数其实并不是 callback,在 useCallback 函数里不会被执行,任何时候传入相同参数都会返回相同的结果,它是符合纯函数的规范的。
- useEffect 属性 effect hook 。它的执行和销毁过程比较特殊,下执行时会清除上一次的状态,在同步的渲染逻辑里,怎么保证能清除异步的状态?
soulmt
2021-02-20 18:21:12 +08:00
用 aysnc 标记的话,这个函数就是分步执行了,这样的话在不报错的情况下,react 可能无法同步拿到 return 的函数,所以无法控制订阅的卸载导致代码不可控。在 react 的理念中,不可控是非常危险的操作,所以宁愿让代码恶心一点。
就比如你想在 return 里面卸载监听,但是 react 因为 await 的问题,都不知道你什么时候才能执行到 return,所以在执行到 return 这段时间内,你的所有操作都会造成不确定的结果。这是我想到的一个原因。
wxsm
2021-02-20 18:25:16 +08:00
@iahu 所以说心智负担太重,写个代码跟搞科研一样
cattchen
2021-02-20 18:26:33 +08:00
此外,用这种写法后,类型签名不一致了,原本应该返回一个 Function 用来处理 unmount,现在变成了 Promise <TValue>
otakustay
2021-02-20 18:27:27 +08:00
因为 React 的生命周期不是你控制的,严格来说是用户控制的(用户要离开你不能阻止他)
而对于 useEffect,在生命周期结束(或 deps 变化)时,副作用需要被清理
并且对于一个 async 的过程,如果它没有完成(未 resolve 也未 reject ),那么从理论上来说,在 effect 清理时它是**必须**被中断的,因为 async 过程完成后就会发生副作用,而此时 effect 已经结束了清理,这个副作用一但发生就会失控
所以,React 很简单地要求 useEffect 不能直接用 async 函数,来促使你处理 async 的中断逻辑

但很少有人理解到这个点,不但不处理 async 让异步过程随意泄露不可控,还怪 React 的 API 设计
wxsm
2021-02-20 18:27:51 +08:00
@soulmt 其实这个我都懂。只不过我在想的是,为什么这样。比如卸载函数为什么是 effect 的返回值,而不是独立的一个参数
soulmt
2021-02-20 18:31:14 +08:00
这也比较好理解,因为闭包,你注册的作用域和销毁的作用域要保持一致,用参数控制的话,一方面是作用域问题,还有就是参数来销毁不能保证结果,就比如你注册的函数可能调用失败了,但是销毁的函数并不知道,你们之间也没有办法在执行层面进行状态共享。
otakustay
2021-02-20 18:32:27 +08:00
我给一个官方说法的指引吧: https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions

副作用的清理和 rece condition 的应对是一个道理,都是 async 要在 effect 退出时中止(或至少告诉后续的副作用不要发生)
weimo383
2021-02-20 20:29:28 +08:00
@wxsm 网络请求的话请用 SWR,hook 超好用
cwliang
2021-02-20 21:32:21 +08:00
能问出这个问题,说明你还需加强学习
yuang
2021-02-20 21:32:21 +08:00
那个异步函数可以条件执行
ericls
2021-02-20 21:34:18 +08:00
Promise 没学好不要写 async .
mdn
2021-02-20 23:13:11 +08:00
useEffect 是最基础的 hook,
useAsyncEffect 可以自己封装

//github.com/rauldeheer/use-async-effect
wxsm
2021-02-20 23:14:59 +08:00
@cwliang
@ericls
可不就是在向各位学习吗。楼上很多回复还是很认真地回答了我的问题的。
KuroNekoFan
2021-02-20 23:23:25 +08:00
点进来好几次
最后觉得从实际操作上来说只要判断一下 useeffect 的返回值是不是 promise 就好,至于为什么不这样做,可能有些风格和个人偏好的原因吧……
claneo
2021-02-20 23:32:18 +08:00
这个帖子真的是有一半的人只看了标题就开始回答了。。。

我感觉 React 的理念就是这样,尽可能只提供基础的 API,剩下的部分鼓励用户去封装。

而且万一将来 React 支持异步渲染呢?说不定到时候 Promise 就有用了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/754630

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX