跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • useEffect 为什么不能支持 async function?
未分類
15 2 月 2021

useEffect 为什么不能支持 async function?

useEffect 为什么不能支持 async function?

資深大佬 : wxsm 2

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

大佬有話說 (73)

  • 資深大佬 : weixiangzhe

    这样写也挺危险的, 建议

  • 資深大佬 : weixiangzhe

    https://usehooks.com/useAsync/

  • 資深大佬 : weixiangzhe

    主要是 用 async,也相当于这样吧

    “`
    useEffect(()=> new Promise(()=>{
    /**xxxx**/
    }))
    “`
    也就是 unmount 时的清理函数了

  • 資深大佬 : ayase252

    我猜 useEffect 的返回值是要在卸载组件时调用的,React 需要在 mount 的时候马上拿到这个值,不然就乱套了

  • 資深大佬 : islxyqwe

    useEffect(() => {
    loadContent().then(()=>{/** do something */});
    }, []);

  • 資深大佬 : love

    ??你直接写个封装 hook 不就看上去优雅了,比如
    useAsyncEffect(async ()=>{})

  • 主 資深大佬 : wxsm

    @islxyqwe 丑陋的写法

  • 主 資深大佬 : wxsm

    @weixiangzhe 我觉得不够优雅

  • 主 資深大佬 : wxsm

    @love 我可以封装,我只是想知道 react 为什么不直接给用户提供,而要留下坑人的空间。你可能不知道 react native 遇到错误的写法有多爆炸,app 会直接崩溃。不是所有人都能精通 react,总会有一两个愣头青会这么干。

  • 資深大佬 : azcvcza

    只能说,js 的 async 函数虽然叫做异步函数,但实际上只是 Promise 的语法糖,和一般人认为的,都叫函数有啥不一样,有很大区别,比较容易入坑

  • 資深大佬 : tedd

    直接跟 .then 吧

    useEffect(async () => {
    loadContent().then(res => {});
    }, []);

  • 資深大佬 : tedd

    忘去了 async

  • 資深大佬 : anjianshi

    我觉得 useEffect() 可能有个潜在逻辑:第二次触发 useEffect 里的回调前,前一次触发的行为都执行完成,返回的清理函数也执行完成。这样逻辑才清楚。

    useEffect(() =>{
    doSomething()
    return () => clear()
    }, [var])

    同步情况下,var 变化触发 useEffect() 时,前一次的 doSomething() 和 clear() 肯定都执行完成了。

    useEffect(async () =>{
    await doSomething()
    return () => clear()
    }, [var])

    而如果是异步的,情况会变得很复杂,可能会很容易写出有 bug 的代码。
    所以它不直接支持,如果用户确定这个异步操作与 useEffect() 的多次触发没有冲突,就自行封装。

  • 主 資深大佬 : wxsm

    @tedd 你不觉得很丑陋吗,别的地方都用 async 这里来个 then

  • 主 資深大佬 : wxsm

    @azcvcza 说是这么说,但是 async 的初衷就是让异步有同步的使用体验,useEffect 明显违背了这个初衷。

  • 資深大佬 : zhuweiyou

    你返回了 promise, 它当销毁函数处理了.

    不得不说, 确实 sb.

  • 主 資深大佬 : wxsm

    @anjianshi 道理是这么个道理,但复杂就不做,最终不是恶心了用户吗

  • 資深大佬 : iahu

    我的理解是 hook 在每次 rerender 时都会生成一个快照,而且这些快照之间的数据结构是链式的。如果 useEffect 的 callback 是 Promise 类型的,那所有链接上的 hook 必将都得是异步的,而 FC (functional component) 是同步的,在同步的 FC 里想拿异步的状态就完全是另一套操作了。

    FC 和 hook 的搭配是函数式( FP )编辑思想,FP 构建是纯函数之上的。之所以不能内部实现就是与设计思想不符,而且实现了就不能只在这一个地方实现,到处都得支持。

  • 資深大佬 : sm0king

    看下 hooks 的实现原理或许可以解决你的疑问。

  • 資深大佬 : noe132

    因为 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,减少一大堆模板代码,极大提高开发舒适度。目前已经用在我们公司线上项目里了

  • 主 資深大佬 : wxsm

    @noe132 可惜我司的项目已经被 redux 套牢了

  • 主 資深大佬 : wxsm

    @iahu 第一,hooks 目前并不是全部要求同步的,至少 useCallback 可以直接定义 async 函数。第二,useEffect 现在的做法也只能是假同步,要求别人在 effet 里必须写纯函数,然而又建议有异步需求的人在纯函数里面写 IIFE,这种做法也同样无法保证销毁函数与 effect 函数的时序性。这就是单纯的简化了自己的工作,把困难的工作交给了用户。

  • 資深大佬 : Kasumi20

    useEffect 需要立即同步地返回一个清除函数,你为了用一个 await 语法,返回一个 Promise 给 React 干嘛

  • 資深大佬 : joesonw

    useEffect(fn: () => () => void).

    useEffect 是要返回一个函数() => void 的, 作为 unmount 的时候回调用的. async 返回的话就变成 () => Promise<void>了. 签名不对

  • 資深大佬 : iahu

    – useCallback 的签名是 `(a, b) => a` 第一个参数其实并不是 callback,在 useCallback 函数里不会被执行,任何时候传入相同参数都会返回相同的结果,它是符合纯函数的规范的。
    – useEffect 属性 effect hook 。它的执行和销毁过程比较特殊,下执行时会清除上一次的状态,在同步的渲染逻辑里,怎么保证能清除异步的状态?

  • 資深大佬 : soulmt

    用 aysnc 标记的话,这个函数就是分步执行了,这样的话在不报错的情况下,react 可能无法同步拿到 return 的函数,所以无法控制订阅的卸载导致代码不可控。在 react 的理念中,不可控是非常危险的操作,所以宁愿让代码恶心一点。
    就比如你想在 return 里面卸载监听,但是 react 因为 await 的问题,都不知道你什么时候才能执行到 return,所以在执行到 return 这段时间内,你的所有操作都会造成不确定的结果。这是我想到的一个原因。

  • 主 資深大佬 : wxsm

    @iahu 所以说心智负担太重,写个代码跟搞科研一样

  • 資深大佬 : cattchen

    此外,用这种写法后,类型签名不一致了,原本应该返回一个 Function 用来处理 unmount,现在变成了 Promise <TValue>

  • 資深大佬 : otakustay

    因为 React 的生命周期不是你控制的,严格来说是用户控制的(用户要离开你不能阻止他)
    而对于 useEffect,在生命周期结束(或 deps 变化)时,副作用需要被清理
    并且对于一个 async 的过程,如果它没有完成(未 resolve 也未 reject ),那么从理论上来说,在 effect 清理时它是**必须**被中断的,因为 async 过程完成后就会发生副作用,而此时 effect 已经结束了清理,这个副作用一但发生就会失控
    所以,React 很简单地要求 useEffect 不能直接用 async 函数,来促使你处理 async 的中断逻辑

    但很少有人理解到这个点,不但不处理 async 让异步过程随意泄露不可控,还怪 React 的 API 设计

  • 主 資深大佬 : wxsm

    @soulmt 其实这个我都懂。只不过我在想的是,为什么这样。比如卸载函数为什么是 effect 的返回值,而不是独立的一个参数

  • 資深大佬 : soulmt

    这也比较好理解,因为闭包,你注册的作用域和销毁的作用域要保持一致,用参数控制的话,一方面是作用域问题,还有就是参数来销毁不能保证结果,就比如你注册的函数可能调用失败了,但是销毁的函数并不知道,你们之间也没有办法在执行层面进行状态共享。

  • 資深大佬 : otakustay

    我给一个官方说法的指引吧: https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions

    副作用的清理和 rece condition 的应对是一个道理,都是 async 要在 effect 退出时中止(或至少告诉后续的副作用不要发生)

  • 資深大佬 : weimo383

    @wxsm 网络请求的话请用 SWR,hook 超好用

  • 資深大佬 : cwliang

    能问出这个问题,说明你还需加强学习

  • 資深大佬 : yuang

    那个异步函数可以条件执行

  • 資深大佬 : ericls

    Promise 没学好不要写 async .

  • 資深大佬 : mdn

    useEffect 是最基础的 hook,
    useAsyncEffect 可以自己封装

    //github.com/rauldeheer/use-async-effect

  • 主 資深大佬 : wxsm

    @cwliang
    @ericls
    可不就是在向各位学习吗。上很多回复还是很认真地回答了我的问题的。

  • 資深大佬 : KuroNekoFan

    点进来好几次
    最后觉得从实际操作上来说只要判断一下 useeffect 的返回值是不是 promise 就好,至于为什么不这样做,可能有些风格和个人偏好的原因吧……

  • 資深大佬 : claneo

    这个帖子真的是有一半的人只看了标题就开始回答了。。。

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

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

  • 主 資深大佬 : wxsm

    @claneo 你算是说到点上了,说到底还是因为 render 只支持同步执行。

  • 主 資深大佬 : wxsm

    @KuroNekoFan 这个上很多大牛回答得很好,它不是单纯判断一下 promise 就能成的事。

  • 資深大佬 : leelz

    @claneo Concurrent Mode + Suspense 已经支持异步渲染了。https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense

  • 資深大佬 : leelz

    上很多回答都在解释为什么不能用,而主的疑惑是为什么这么设计。。

  • 資深大佬 : KuroNekoFan

    @wxsm 我本来想说“fiber 做为最小执行单元,内部的东西必须是同步的”,但是事实上支持 async 函数直接作为参数和在函数内再用 iife+async 确实就是一个判断的事吧,因为按现在的约束来说有意义的返回是一个函数类型,而函数类型又是可以方便而明确的跟 promise 区分开来的

  • 資深大佬 : wanghaoipv6

    应该是个设计上的取舍?如果开了「返回值是 Promise,就忽略」这个口子,那很多人就会想「为啥不把其他类型也顺便忽略了?」,然后大家就随便返回,慢慢开始忘记有「返回销毁函数」这件事。这个代码就变成了屎山。

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

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

    至于你上面提到的「卸载函数为什么是 effect 的返回值,而不是独立的一个参数」,这个实在是太丑了,更不可能考虑。

  • 主 資深大佬 : wxsm

    @leelz 看到你的回复还挺惊喜的,然而点进去看了一下说实话我挺失望的。说白了就是 React 也开始教你写代码了,发明的东西越来越多,并不能利用好 js 本身的特性。

  • 主 資深大佬 : wxsm

    @wanghaoipv6

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

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

  • 主 資深大佬 : wxsm

    @KuroNekoFan

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

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

    至于 IIFE,React 对这件事的态度就是:我知道很多情况有这种需求,你们可以用 IIFE 来实现,至于发生了什么事我不管,我只负责创建和销毁,你们开发者用了异步记得自己把屁股擦干净就行了。

  • 資深大佬 : KuroNekoFan

    @wxsm 这很简单啊,对返回 promise 的当成跟 void 的一样处理就好了
    那么问题就是
    我允许 async 关键字,用户写起来方便一点,但多加一个针对性的约束
    还是我不允许 async 关键字,用户写起来麻烦一点,但是少一些约定 /约束

  • 主 資深大佬 : wxsm

    @KuroNekoFan 两种情况都会成为约束,不允许 async 这件事本身就是一个约束

  • 資深大佬 : zed1018

    不揣测 fb 这么设计的用意,帖子里的这种异步获取数据并更新界面的方案,我一直都是用 dispatch()做的。

  • 資深大佬 : jinliming2

    如果你在 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

    @jinliming2

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

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

    你的回答基本是基于”React 现在就是这样的”,然后提出了一系列反问。但是我觉得这种回答没什么建设性。我知道按照 React 现在的模式 async effect 走不通,我目前也提供不了更好的设计,只是我认为这不对,不够优雅,对于 React 这样一款追求大道至简的框架(库)来说,不契合。

  • 資深大佬 : no1xsyzy

    @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

    我是写 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

    返回销毁函数就不是个好设计。

  • 資深大佬 : EridanusSora

    看了上的讨论,觉得还是返回销毁函数这个设计阻止了 async effect 的用法。
    那么返回销毁函数是不是合理的?虽然使用上感觉很精巧,但是似乎有点 trick 的感觉?

  • 資深大佬 : azcvcza

    @no1xsyzy JS 默认的 Promise 没有 cancel,第三方实现的我看到过,但是各个 JS 引擎都没在正式实现加上

  • 資深大佬 : soulmt

    @leelz 设计就是为了让开发者按照设计的规范使用,通过用法限制去反推设计,并不矛盾,如果说有些用法是设计者没想到的,我相信这种事情在 react 里面应该会当作 bug 修复,或者会被认为是合理的。

  • 資深大佬 : soulmt

    @aguesuka 我觉得挺好的,原地销毁可以解决作用域的问题,把同一件事情的整个周期放在了一起处理,这个有助于代码可读性和维护性。

  • 資深大佬 : SmiteChow

    可以用,我一直都在用,所以上各位使我非常疑惑。

  • 主 資深大佬 : wxsm

    @soulmt js 对于 async 的设计就是让异步函数和同步函数具有同等的书写体验,作为一款(至少目前来说)基于 js 的库,React 首先就没有尊重这个设定。

  • 主 資深大佬 : wxsm

    @SmiteChow 如果你一直都是这么用的,那我建议你赶紧去认真学一学,改正过来。

  • 資深大佬 : soulmt

    @wxsm 那我可得杠一把了哈哈,react 是基于 js 没错,但是基于 js 怎么去定义开发方式,这个其实没有必要一定去遵守哈,就好比 react 当年在处理传递函数的时候是否要自动 bind(this)还是开发者手动 bind,也是因为那极小的一部分场景不兼容,而造成了开发者需要不厌其烦的手动 bind,又或者在定义 getDerivedStateFromProps 采用了纯函数的方式(内部访问不了 this,只能接受入参),这其实对于 class 组件的定义来说,也是”非常规”定义,但是这背后的含义,只有定义的人才知道,所以 hooks 也没有不尊重这个设定,起码允许在内部,开发者可以为所欲为的。

  • 資深大佬 : no1xsyzy

    @wxsm 从书写体验这一点上来说,async 必然导致关键词污染……
    话说 React 的函数式组件本身支持 async 定义吗?

  • 主 資深大佬 : wxsm

    @soulmt 没有说一定要遵守,我的出发点是「优雅」。当开发体验与原生 js 的契合度越高,发明的东西越少,需要写的代码越少,我认为越优雅。不是说 React 定义的 hooks 方式不好,而是说,它目前存还存在这些不足,导致它还不够优雅。你说的手动 bind 确实丑陋,但是 React 现在不也通过 hook 把它干掉了吗,就算不写 hook 至少现在可以在类上面定义箭头成员函数,再也不用写 bind 了,这就是进步。

  • 主 資深大佬 : wxsm

    @no1xsyzy

    1. 从利弊关系来说,利大于弊。
    2. 不支持,这应该是症结所在。

  • 資深大佬 : aguesuka

    useEffect 的第一个参数的类型是

    () => (() => void) | void // 在 ReactFiberHooks 文件

    设计得不好的地方在于这个函数做了两件事情,它是一个有副作用的函数,而且是另一个有副作用的函数的生成器。但是语义上,它并不是销毁函数的生成器,而是在副作用的同时设置或者改变了销毁函数。

    合理的思路应该是这个参数没有返回值,但是在执行过程中可以调用另外一个函数,相当于返回销毁函数。

    @soulmt

  • 資深大佬 : soulmt

    @aguesuka 设计返回函数的作用就是自己清理自己的副作用,关注点比较集中,不像 class 组件,一个地方注册了。需要在别的生命周期里面销毁,这对复杂页面的阅读性上有点不友好。

    你说的调用另外一个函数(假设 B)来销毁,这一点就遇到一个问题,比如监听,你在 Effect 里面注册了监听 /定时器,那么你卸载的时候别的函数是访问不了 Effect 里面的变量的,那你监听的回调函数得上抛到 B 的作用域里面,这样的话,Effect 所带来的独立作用域就被打乱了,也不符合 hooks 的心智模型,这在 class 组件里面也是一个用起来很麻烦的事情。

  • 資深大佬 : SmiteChow

    @wxsm 改啥?我能 work 啊

  • 資深大佬 : myCupOfTea

    ,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 的值是前一次的结果,干脆简单一点,复杂的情况各自自己封装就好

  • 資深大佬 : myCupOfTea

    async 的传染性太强了,除非整个 render 全部异步化重写一遍

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具