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 偷懒,还是我傻逼?

6815 次点击
所在节点    程序员
74 条回复
wxsm
2021-02-20 23:37:14 +08:00
@claneo 你算是说到点上了,说到底还是因为 render 只支持同步执行。
wxsm
2021-02-20 23:39:18 +08:00
@KuroNekoFan 这个楼上很多大牛回答得很好,它不是单纯判断一下 promise 就能成的事。
leelz
2021-02-21 06:27:27 +08:00
leelz
2021-02-21 06:28:22 +08:00
楼上很多回答都在解释为什么不能用,而楼主的疑惑是为什么这么设计。。
KuroNekoFan
2021-02-21 07:40:38 +08:00
@wxsm 我本来想说“fiber 做为最小执行单元,内部的东西必须是同步的”,但是事实上支持 async 函数直接作为参数和在函数内再用 iife+async 确实就是一个判断的事吧,因为按现在的约束来说有意义的返回是一个函数类型,而函数类型又是可以方便而明确的跟 promise 区分开来的
wanghaoipv6
2021-02-21 08:18:06 +08:00
应该是个设计上的取舍?如果开了「返回值是 Promise,就忽略」这个口子,那很多人就会想「为啥不把其他类型也顺便忽略了?」,然后大家就随便返回,慢慢开始忘记有「返回销毁函数」这件事。这个代码就变成了屎山。

我觉得看 React 可以把它想成是一个没有语言倾向,随时可能用 rust 重写的东西。react 的设计者兴趣点也不在 JavaScript 上(否则就会像 vue 那样狂用 proxy 了)。这种视角下你提到的「 koa2 极致的代码艺术」就变得非常的不优雅。(我猜的,我没怎么用过 koa2,不太清楚代码艺术具体指什么,不过我猜想是强依赖于 JavaScript 本身的特性发展出来的语法糖?)。

因此,「不管类型,在运行时检查来确定代码行为」这种 JavaScript 味很重的东西,在 typescript 风潮愈演愈烈的,react 野心越来越大的当下,是一件根本就不被考虑的妥协。

至于你上面提到的「卸载函数为什么是 effect 的返回值,而不是独立的一个参数」,这个实在是太丑了,更不可能考虑。
wxsm
2021-02-21 10:34:26 +08:00
@leelz 看到你的回复还挺惊喜的,然而点进去看了一下说实话我挺失望的。说白了就是 React 也开始教你写代码了,发明的东西越来越多,并不能利用好 js 本身的特性。
wxsm
2021-02-21 10:44:02 +08:00
@wanghaoipv6

> 我觉得看 React 可以把它想成是一个没有语言倾向,随时可能用 rust 重写的东西

俗话说干一行爱一行,有没有倾向暂且不谈,既然做了就要把它做好吧。至于 ts,它跟 js 也没有冲突,ts 本身就是一种对 js 的妥协,否则它干嘛不自立门户,要做 js 的超集呢。
wxsm
2021-02-21 10:57:00 +08:00
@KuroNekoFan

> 但是事实上支持 async 函数直接作为参数和在函数内再用 iife+async 确实就是一个判断的事吧

还真不是一个判断的事。React 想要的是执行完函数立马得到销毁函数,如果加上了 async 这件事就无从谈起了。React 无法及时得到销毁函数,就无法及时销毁组件,整个架构立马复杂度倍增。

至于 IIFE,React 对这件事的态度就是:我知道很多情况有这种需求,你们可以用 IIFE 来实现,至于发生了什么事我不管,我只负责创建和销毁,你们开发者用了异步记得自己把屁股擦干净就行了。
KuroNekoFan
2021-02-21 11:33:09 +08:00
@wxsm 这很简单啊,对返回 promise 的当成跟 void 的一样处理就好了
那么问题就是
我允许 async 关键字,用户写起来方便一点,但多加一个针对性的约束
还是我不允许 async 关键字,用户写起来麻烦一点,但是少一些约定 /约束
wxsm
2021-02-21 12:22:15 +08:00
@KuroNekoFan 两种情况都会成为约束,不允许 async 这件事本身就是一个约束
zed1018
2021-02-21 12:35:35 +08:00
不揣测 fb 这么设计的用意,帖子里的这种异步获取数据并更新界面的方案,我一直都是用 dispatch()做的。
jinliming2
2021-02-21 13:13:48 +08:00
如果你在 useEffect 里写了一个 async 函数:
useEffect(async () => {
await waitFor10Seconds();
return () => cleaningUp();
}, [dep]);
这样,在这个 async 函数中等待了 10 秒才会返回,而这之间你触发了 dep 的更新,请问现在的执行逻辑会怎样?
是整个组件卡着不动,等这个 Promise resolve 之后再去执行它的 cleaning up 函数吗?还是说这个 cleaning up 就不执行了?还是说把这个 cleaning up 函数加入队列,之后可能会乱序执行?或者排序后顺序执行?有时这个 Promise 也许永远不会 resolve 。
这样就会产生开发过程中的歧义。
默认约定返回 promise 的话就不支持 cleaning up ?但这就和 useEffect 本身的设计理念产生了冲突,本身的概念很简单,这又加了一种特例情况。

按照我的习惯的话,这种异步任务不会写到 useEffect 里,而是写道外面,useEffect 中只是去调用这个函数:
const fetchData = async () => {};
useEffect(() => {
fetchData();
return () => abortFetch();
}, [dep]);

另外 Promise 未处理的警告,我这里默认是没有这个警告的,我也不会去配置这个警告。
我觉得这是很正常的事情,Promise 作为一个返回值,它与其他的 return 1 、return "1" 有什么区别?在没有必要的情况下,其他的返回值你可以不接收、不处理,那为啥在没有必要处理的时候,要特别去关注 Promise 的处理呢?仅仅因为它是个“return new Promise()”?
wxsm
2021-02-21 15:50:56 +08:00
@jinliming2

> 按照我的习惯的话,这种异步任务不会写到 useEffect 里,而是写道外面,useEffect 中只是去调用这个函数

其实现在我们很多也都是这么写的,另外提醒一下你的代码不严谨,fetchData 必须用 useCallback 否则无限循环。

你的回答基本是基于”React 现在就是这样的”,然后提出了一系列反问。但是我觉得这种回答没什么建设性。我知道按照 React 现在的模式 async effect 走不通,我目前也提供不了更好的设计,只是我认为这不对,不够优雅,对于 React 这样一款追求大道至简的框架(库)来说,不契合。
no1xsyzy
2021-02-21 16:33:35 +08:00
@azcvcza async/await 模型就是 Future/Promise 的语法糖,跟 js 没太大关系,Python Rust 这两个也是这么玩的。
建议自己实现一个 Actor 模型(

我觉得 #13 的情况可能性比较大,不是复杂,而是根本执行模式上的冲突
话说 JS 的 Promise 可以 cancel 吗?
还是建议 Actor 模型(反正 stackfull 的没法在 JS 里实现只能模拟),Actor 模型才是真异步,连 Future 都没有,shot and gone

话说,计算机方面,但凡听说“大道至简”,这个人肯定是在说 worst is better
winglight2016
2021-02-21 18:07:38 +08:00
我是写 RN 的时候了解 react 的,我记得 useEffect 的目的是为了刷新 UI,然后调用的是 useContext 里的方法,这些方法是可以定义为 async 的,我也不理解为什么直接调用就不行。如下:
const {
state: {currentObj, electricity},
fetchElectricity,
} = useContext(EnergyContext);

useEffect(() => {
console.log('selectedDate: ' + selectedDate.format());
fetchElectricity(currentObj, timeType, selectedDate.valueOf());
}, [currentObj, selectedDate]);

const fetchElectricity = dispatch => async (currObj, timeType, timeValue) => {
...
这么定义我觉得虽然略为烦琐,但是封装得还不错了,而且所有业务逻辑都统一在 Context 里实现,除了个别需求都可以用近似的代码实现,可读性也挺好的。
aguesuka
2021-02-21 19:04:19 +08:00
返回销毁函数就不是个好设计。
EridanusSora
2021-02-22 01:33:08 +08:00
看了楼上的讨论,觉得还是返回销毁函数这个设计阻止了 async effect 的用法。
那么返回销毁函数是不是合理的?虽然使用上感觉很精巧,但是似乎有点 trick 的感觉?
azcvcza
2021-02-22 10:11:24 +08:00
@no1xsyzy JS 默认的 Promise 没有 cancel,第三方实现的我看到过,但是各个 JS 引擎都没在正式实现加上
soulmt
2021-02-22 10:26:33 +08:00
@leelz 设计就是为了让开发者按照设计的规范使用,通过用法限制去反推设计,并不矛盾,如果说有些用法是设计者没想到的,我相信这种事情在 react 里面应该会当作 bug 修复,或者会被认为是合理的。

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

https://ex.noerr.eu.org/t/754630

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

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

© 2021 V2EX