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();
})();
}, []);
我觉得都非常地不优雅,极度增加心智负担。
React 判断 useEffect 的传参到底是纯函数还是 Promise 非常简单,它为什么不做呢?这到底是设计缺陷,还是 React 偷懒,还是我傻逼?
1
weixiangzhe 2021-02-20 15:41:53 +08:00
这样写也挺危险的, 建议
|
2
weixiangzhe 2021-02-20 15:41:59 +08:00
|
3
weixiangzhe 2021-02-20 15:44:29 +08:00 1
主要是 用 async,也相当于这样吧
``` useEffect(()=> new Promise(()=>{ /**xxxx**/ })) ``` 也就是 unmount 时的清理函数了 |
4
ayase252 2021-02-20 15:46:50 +08:00 via iPhone 1
我猜 useEffect 的返回值是要在卸载组件时调用的,React 需要在 mount 的时候马上拿到这个值,不然就乱套了
|
5
islxyqwe 2021-02-20 15:47:30 +08:00
useEffect(() => {
loadContent().then(()=>{/** do something */}); }, []); |
6
love 2021-02-20 15:53:20 +08:00 via Android
??你直接写个封装 hook 不就看上去优雅了,比如
useAsyncEffect(async ()=>{}) |
8
wxsm OP @weixiangzhe 我觉得不够优雅
|
9
wxsm OP @love 我可以封装,我只是想知道 react 为什么不直接给用户提供,而要留下坑人的空间。你可能不知道 react native 遇到错误的写法有多爆炸,app 会直接崩溃。不是所有人都能精通 react,总会有一两个愣头青会这么干。
|
10
azcvcza 2021-02-20 16:05:17 +08:00
只能说,js 的 async 函数虽然叫做异步函数,但实际上只是 Promise 的语法糖,和一般人认为的,都叫函数有啥不一样,有很大区别,比较容易入坑
|
11
tedd 2021-02-20 16:05:27 +08:00
直接跟 .then 吧
useEffect(async () => { loadContent().then(res => {}); }, []); |
12
tedd 2021-02-20 16:05:59 +08:00
忘去了 async 👆
|
13
anjianshi 2021-02-20 16:07:36 +08:00 2
我觉得 useEffect() 可能有个潜在逻辑:第二次触发 useEffect 里的回调前,前一次触发的行为都执行完成,返回的清理函数也执行完成。这样逻辑才清楚。
useEffect(() =>{ doSomething() return () => clear() }, [var]) 同步情况下,var 变化触发 useEffect() 时,前一次的 doSomething() 和 clear() 肯定都执行完成了。 useEffect(async () =>{ await doSomething() return () => clear() }, [var]) 而如果是异步的,情况会变得很复杂,可能会很容易写出有 bug 的代码。 所以它不直接支持,如果用户确定这个异步操作与 useEffect() 的多次触发没有冲突,就自行封装。 |
16
zhuweiyou 2021-02-20 16:22:08 +08:00
你返回了 promise, 它当销毁函数处理了.
不得不说, 确实 sb. |
18
iahu 2021-02-20 16:30:09 +08:00 1
我的理解是 hook 在每次 rerender 时都会生成一个快照,而且这些快照之间的数据结构是链式的。如果 useEffect 的 callback 是 Promise 类型的,那所有链接上的 hook 必将都得是异步的,而 FC (functional component) 是同步的,在同步的 FC 里想拿异步的状态就完全是另一套操作了。
FC 和 hook 的搭配是函数式( FP )编辑思想,FP 构建是纯函数之上的。之所以不能内部实现就是与设计思想不符,而且实现了就不能只在这一个地方实现,到处都得支持。 |
19
sm0king 2021-02-20 16:46:36 +08:00
看下 hooks 的实现原理或许可以解决你的疑问。
|
20
noe132 2021-02-20 16:46:44 +08:00 via Android
因为 useEffect 每次渲染都会执行,每次执行都要执行上次执行返回的 teardown.
了解一下我针对 mobx 封装的 react-composition-api https://github.com/Firefox-Pro-Coding/react-composition-api 用 react+mobx 的同学可以试试 参考了 vue3 api,可以 onMounted onUnmounted 嵌套,支持 async,支持 reactivity watcher 自动 teardown,减少一大堆模板代码,极大提高开发舒适度。目前已经用在我们公司线上项目里了 |
22
wxsm OP @iahu 第一,hooks 目前并不是全部要求同步的,至少 useCallback 可以直接定义 async 函数。第二,useEffect 现在的做法也只能是假同步,要求别人在 effet 里必须写纯函数,然而又建议有异步需求的人在纯函数里面写 IIFE,这种做法也同样无法保证销毁函数与 effect 函数的时序性。这就是单纯的简化了自己的工作,把困难的工作交给了用户。
|
23
Kasumi20 2021-02-20 17:19:23 +08:00
useEffect 需要立即同步地返回一个清除函数,你为了用一个 await 语法,返回一个 Promise 给 React 干嘛
|
24
joesonw 2021-02-20 17:24:55 +08:00
useEffect(fn: () => () => void).
useEffect 是要返回一个函数() => void 的, 作为 unmount 的时候回调用的. async 返回的话就变成 () => Promise<void>了. 签名不对 |
25
iahu 2021-02-20 18:07:09 +08:00 2
- useCallback 的签名是 `(a, b) => a` 第一个参数其实并不是 callback,在 useCallback 函数里不会被执行,任何时候传入相同参数都会返回相同的结果,它是符合纯函数的规范的。
- useEffect 属性 effect hook 。它的执行和销毁过程比较特殊,下执行时会清除上一次的状态,在同步的渲染逻辑里,怎么保证能清除异步的状态? |
26
soulmt 2021-02-20 18:21:12 +08:00 2
用 aysnc 标记的话,这个函数就是分步执行了,这样的话在不报错的情况下,react 可能无法同步拿到 return 的函数,所以无法控制订阅的卸载导致代码不可控。在 react 的理念中,不可控是非常危险的操作,所以宁愿让代码恶心一点。
就比如你想在 return 里面卸载监听,但是 react 因为 await 的问题,都不知道你什么时候才能执行到 return,所以在执行到 return 这段时间内,你的所有操作都会造成不确定的结果。这是我想到的一个原因。 |
28
cattchen 2021-02-20 18:26:33 +08:00 via iPhone
此外,用这种写法后,类型签名不一致了,原本应该返回一个 Function 用来处理 unmount,现在变成了 Promise <TValue>
|
29
otakustay 2021-02-20 18:27:27 +08:00 7
因为 React 的生命周期不是你控制的,严格来说是用户控制的(用户要离开你不能阻止他)
而对于 useEffect,在生命周期结束(或 deps 变化)时,副作用需要被清理 并且对于一个 async 的过程,如果它没有完成(未 resolve 也未 reject ),那么从理论上来说,在 effect 清理时它是**必须**被中断的,因为 async 过程完成后就会发生副作用,而此时 effect 已经结束了清理,这个副作用一但发生就会失控 所以,React 很简单地要求 useEffect 不能直接用 async 函数,来促使你处理 async 的中断逻辑 但很少有人理解到这个点,不但不处理 async 让异步过程随意泄露不可控,还怪 React 的 API 设计 |
30
wxsm OP @soulmt 其实这个我都懂。只不过我在想的是,为什么这样。比如卸载函数为什么是 effect 的返回值,而不是独立的一个参数
|
31
soulmt 2021-02-20 18:31:14 +08:00 1
这也比较好理解,因为闭包,你注册的作用域和销毁的作用域要保持一致,用参数控制的话,一方面是作用域问题,还有就是参数来销毁不能保证结果,就比如你注册的函数可能调用失败了,但是销毁的函数并不知道,你们之间也没有办法在执行层面进行状态共享。
|
32
otakustay 2021-02-20 18:32:27 +08:00 3
我给一个官方说法的指引吧: https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions
副作用的清理和 rece condition 的应对是一个道理,都是 async 要在 effect 退出时中止(或至少告诉后续的副作用不要发生) |
34
cwliang 2021-02-20 21:32:21 +08:00
能问出这个问题,说明你还需加强学习
|
35
yuang 2021-02-20 21:32:21 +08:00 via Android
那个异步函数可以条件执行
|
36
ericls 2021-02-20 21:34:18 +08:00 via iPhone
Promise 没学好不要写 async .
|
37
mdn 2021-02-20 23:13:11 +08:00
|
39
KuroNekoFan 2021-02-20 23:23:25 +08:00 via iPhone
点进来好几次
最后觉得从实际操作上来说只要判断一下 useeffect 的返回值是不是 promise 就好,至于为什么不这样做,可能有些风格和个人偏好的原因吧…… |
40
claneo 2021-02-20 23:32:18 +08:00 1
这个帖子真的是有一半的人只看了标题就开始回答了。。。
我感觉 React 的理念就是这样,尽可能只提供基础的 API,剩下的部分鼓励用户去封装。 而且万一将来 React 支持异步渲染呢?说不定到时候 Promise 就有用了。 |
42
wxsm OP @KuroNekoFan 这个楼上很多大牛回答得很好,它不是单纯判断一下 promise 就能成的事。
|
43
leelz 2021-02-21 06:27:27 +08:00
@claneo Concurrent Mode + Suspense 已经支持异步渲染了。https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense
|
44
leelz 2021-02-21 06:28:22 +08:00 1
楼上很多回答都在解释为什么不能用,而楼主的疑惑是为什么这么设计。。
|
45
KuroNekoFan 2021-02-21 07:40:38 +08:00 via iPhone
@wxsm 我本来想说“fiber 做为最小执行单元,内部的东西必须是同步的”,但是事实上支持 async 函数直接作为参数和在函数内再用 iife+async 确实就是一个判断的事吧,因为按现在的约束来说有意义的返回是一个函数类型,而函数类型又是可以方便而明确的跟 promise 区分开来的
|
46
wanghaoipv6 2021-02-21 08:18:06 +08:00 via Android
应该是个设计上的取舍?如果开了「返回值是 Promise,就忽略」这个口子,那很多人就会想「为啥不把其他类型也顺便忽略了?」,然后大家就随便返回,慢慢开始忘记有「返回销毁函数」这件事。这个代码就变成了屎山。
我觉得看 React 可以把它想成是一个没有语言倾向,随时可能用 rust 重写的东西。react 的设计者兴趣点也不在 JavaScript 上(否则就会像 vue 那样狂用 proxy 了)。这种视角下你提到的「 koa2 极致的代码艺术」就变得非常的不优雅。(我猜的,我没怎么用过 koa2,不太清楚代码艺术具体指什么,不过我猜想是强依赖于 JavaScript 本身的特性发展出来的语法糖?)。 因此,「不管类型,在运行时检查来确定代码行为」这种 JavaScript 味很重的东西,在 typescript 风潮愈演愈烈的,react 野心越来越大的当下,是一件根本就不被考虑的妥协。 至于你上面提到的「卸载函数为什么是 effect 的返回值,而不是独立的一个参数」,这个实在是太丑了,更不可能考虑。 |
47
wxsm OP @leelz 看到你的回复还挺惊喜的,然而点进去看了一下说实话我挺失望的。说白了就是 React 也开始教你写代码了,发明的东西越来越多,并不能利用好 js 本身的特性。
|
48
wxsm OP @wanghaoipv6
> 我觉得看 React 可以把它想成是一个没有语言倾向,随时可能用 rust 重写的东西 俗话说干一行爱一行,有没有倾向暂且不谈,既然做了就要把它做好吧。至于 ts,它跟 js 也没有冲突,ts 本身就是一种对 js 的妥协,否则它干嘛不自立门户,要做 js 的超集呢。 |
49
wxsm OP @KuroNekoFan
> 但是事实上支持 async 函数直接作为参数和在函数内再用 iife+async 确实就是一个判断的事吧 还真不是一个判断的事。React 想要的是执行完函数立马得到销毁函数,如果加上了 async 这件事就无从谈起了。React 无法及时得到销毁函数,就无法及时销毁组件,整个架构立马复杂度倍增。 至于 IIFE,React 对这件事的态度就是:我知道很多情况有这种需求,你们可以用 IIFE 来实现,至于发生了什么事我不管,我只负责创建和销毁,你们开发者用了异步记得自己把屁股擦干净就行了。 |
50
KuroNekoFan 2021-02-21 11:33:09 +08:00
@wxsm 这很简单啊,对返回 promise 的当成跟 void 的一样处理就好了
那么问题就是 我允许 async 关键字,用户写起来方便一点,但多加一个针对性的约束 还是我不允许 async 关键字,用户写起来麻烦一点,但是少一些约定 /约束 |
51
wxsm OP @KuroNekoFan 两种情况都会成为约束,不允许 async 这件事本身就是一个约束
|
52
zed1018 2021-02-21 12:35:35 +08:00
不揣测 fb 这么设计的用意,帖子里的这种异步获取数据并更新界面的方案,我一直都是用 dispatch()做的。
|
53
jinliming2 2021-02-21 13:13:48 +08:00 1
如果你在 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()”? |
54
wxsm OP @jinliming2
> 按照我的习惯的话,这种异步任务不会写到 useEffect 里,而是写道外面,useEffect 中只是去调用这个函数 其实现在我们很多也都是这么写的,另外提醒一下你的代码不严谨,fetchData 必须用 useCallback 否则无限循环。 你的回答基本是基于”React 现在就是这样的”,然后提出了一系列反问。但是我觉得这种回答没什么建设性。我知道按照 React 现在的模式 async effect 走不通,我目前也提供不了更好的设计,只是我认为这不对,不够优雅,对于 React 这样一款追求大道至简的框架(库)来说,不契合。 |
55
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 |
56
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 里实现,除了个别需求都可以用近似的代码实现,可读性也挺好的。 |
57
aguesuka 2021-02-21 19:04:19 +08:00 via Android 1
返回销毁函数就不是个好设计。
|
58
EridanusSora 2021-02-22 01:33:08 +08:00 via Android
看了楼上的讨论,觉得还是返回销毁函数这个设计阻止了 async effect 的用法。
那么返回销毁函数是不是合理的?虽然使用上感觉很精巧,但是似乎有点 trick 的感觉? |
59
azcvcza 2021-02-22 10:11:24 +08:00
@no1xsyzy JS 默认的 Promise 没有 cancel,第三方实现的我看到过,但是各个 JS 引擎都没在正式实现加上
|
60
soulmt 2021-02-22 10:26:33 +08:00
@leelz 设计就是为了让开发者按照设计的规范使用,通过用法限制去反推设计,并不矛盾,如果说有些用法是设计者没想到的,我相信这种事情在 react 里面应该会当作 bug 修复,或者会被认为是合理的。
|
61
soulmt 2021-02-22 10:33:09 +08:00
@aguesuka 我觉得挺好的,原地销毁可以解决作用域的问题,把同一件事情的整个周期放在了一起处理,这个有助于代码可读性和维护性。
|
62
SmiteChow 2021-02-22 10:59:17 +08:00
可以用,我一直都在用,所以楼上各位使我非常疑惑。
|
63
wxsm OP @soulmt js 对于 async 的设计就是让异步函数和同步函数具有同等的书写体验,作为一款(至少目前来说)基于 js 的库,React 首先就没有尊重这个设定。
|
65
soulmt 2021-02-22 14:09:33 +08:00
@wxsm 那我可得杠一把了哈哈,react 是基于 js 没错,但是基于 js 怎么去定义开发方式,这个其实没有必要一定去遵守哈,就好比 react 当年在处理传递函数的时候是否要自动 bind(this)还是开发者手动 bind,也是因为那极小的一部分场景不兼容,而造成了开发者需要不厌其烦的手动 bind,又或者在定义 getDerivedStateFromProps 采用了纯函数的方式(内部访问不了 this,只能接受入参),这其实对于 class 组件的定义来说,也是"非常规"定义,但是这背后的含义,只有定义的人才知道,所以 hooks 也没有不尊重这个设定,起码允许在内部,开发者可以为所欲为的。
|
66
no1xsyzy 2021-02-22 15:03:27 +08:00
@wxsm 从书写体验这一点上来说,async 必然导致关键词污染……
话说 React 的函数式组件本身支持 async 定义吗? |
67
wxsm OP @soulmt 没有说一定要遵守,我的出发点是「优雅」。当开发体验与原生 js 的契合度越高,发明的东西越少,需要写的代码越少,我认为越优雅。不是说 React 定义的 hooks 方式不好,而是说,它目前存还存在这些不足,导致它还不够优雅。你说的手动 bind 确实丑陋,但是 React 现在不也通过 hook 把它干掉了吗,就算不写 hook 至少现在可以在类上面定义箭头成员函数,再也不用写 bind 了,这就是进步。
|
69
aguesuka 2021-02-22 17:58:12 +08:00 via Android 1
useEffect 的第一个参数的类型是
() => (() => void) | void // 在 ReactFiberHooks 文件 设计得不好的地方在于这个函数做了两件事情,它是一个有副作用的函数,而且是另一个有副作用的函数的生成器。但是语义上,它并不是销毁函数的生成器,而是在副作用的同时设置或者改变了销毁函数。 合理的思路应该是这个参数没有返回值,但是在执行过程中可以调用另外一个函数,相当于返回销毁函数。 @soulmt |
70
soulmt 2021-02-22 18:35:13 +08:00 1
@aguesuka 设计返回函数的作用就是自己清理自己的副作用,关注点比较集中,不像 class 组件,一个地方注册了。需要在别的生命周期里面销毁,这对复杂页面的阅读性上有点不友好。
你说的调用另外一个函数(假设 B)来销毁,这一点就遇到一个问题,比如监听,你在 Effect 里面注册了监听 /定时器,那么你卸载的时候别的函数是访问不了 Effect 里面的变量的,那你监听的回调函数得上抛到 B 的作用域里面,这样的话,Effect 所带来的独立作用域就被打乱了,也不符合 hooks 的心智模型,这在 class 组件里面也是一个用起来很麻烦的事情。 |
72
myCupOfTea 2021-02-23 17:02:39 +08:00
,useEffect 如果支持 async 同样会带来其他心智负担,干脆不支持,我觉得也没啥毛病
如果 useEffect 支持 async,cancel 怎么处理呢,根本就没有好的方法 useEffect(() => { // Create an scoped async function in the hook async function anyNameFunction() { await loadContent(); } // Execute the created function directly anyNameFunction(); }, []); 你这么写也会有问题 你能保证下次 effect 进来上次的执行的结果完成了吗,如果后台有负载均衡器,你能保证前一次的调用一定在后一次执行的返回值前面吗 很可能出现 前一次返回的数据比后一次慢导致修改 state 的值是前一次的结果,干脆简单一点,复杂的情况各自自己封装就好 |
73
myCupOfTea 2021-02-23 17:04:07 +08:00
async 的传染性太强了,除非整个 render 全部异步化重写一遍
|
74
yetrun 2022-11-01 14:13:31 +08:00
正好访问到这个帖子,我来简短地表达一下我的观点。
1. useEffect 返回销毁函数在我看来不是一个很好的设计,改成这样就好: ``` useEffect(callback).clearup(clearup) ``` 2. useEffect 不支持传递异步函数不是一个很好的设计,改成如上,支持传递异步函数。 |