useEffect 為什麼不能支援 async function?

程式設計師 wxsm • at 2021-02-21 14:12:44 • 5 Views
  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 偷懶,還是我傻逼?


再強調一下:

我知道 useEffect 為什麼不能用 async,大家回覆前請看帖。

我想知道的是,React 為什麼要這麼設計。

大家經常說 React 是大道至簡,但是在我看來,這種 useEffect 就像 express,能用,但是醜陋。而我期待的是 koa2 這種級別的,極致的程式碼藝術。我覺得 React 這種級別的庫,有理由將對 async 的支援再上一層樓。
Total: 73
  • weixiangzhe 2021-02-21 14:12:44
    這樣寫也挺危險的, 建議
  • weixiangzhe 2021-02-21 14:12:45
    主要是 用 async,也相當於這樣吧

    ```
    useEffect(()=> new Promise(()=>{
    /**xxxx**/
    }))
    ```
    也就是 unmount 時的清理函式了
  • ayase252 2021-02-21 14:12:45
    我猜 useEffect 的返回值是要在解除安裝元件時呼叫的,React 需要在 mount 的時候馬上拿到這個值,不然就亂套了
  • islxyqwe 2021-02-21 14:12:45
    useEffect(() => {
    loadContent().then(()=>{/** do something */});
    }, []);
  • love 2021-02-21 14:12:45
    ??你直接寫個封裝 hook 不就看上去優雅了,比如
    useAsyncEffect(async ()=>{})
  • wxsm 2021-02-21 14:12:45
    @islxyqwe 醜陋的寫法
  • wxsm 2021-02-21 14:12:45
    @weixiangzhe 我覺得不夠優雅
  • wxsm 2021-02-21 14:12:45
    @love 我可以封裝,我只是想知道 react 為什麼不直接給使用者提供,而要留下坑人的空間。你可能不知道 react native 遇到錯誤的寫法有多爆炸,app 會直接崩潰。不是所有人都能精通 react,總會有一兩個愣頭青會這麼幹。
  • azcvcza 2021-02-21 14:12:45
    只能說,js 的 async 函式雖然叫做非同步函式,但實際上只是 Promise 的語法糖,和一般人認為的,都叫函式有啥不一樣,有很大區別,比較容易入坑
  • tedd 2021-02-21 14:12:45
    直接跟 .then 吧

    useEffect(async () => {
    loadContent().then(res => {});
    }, []);
  • tedd 2021-02-21 14:12:45
    忘去了 async 👆
  • anjianshi 2021-02-21 14:12:45
    我覺得 useEffect() 可能有個潛在邏輯:第二次觸發 useEffect 裡的回撥前,前一次觸發的行為都執行完成,返回的清理函式也執行完成。這樣邏輯才清楚。

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

    同步情況下,var 變化觸發 useEffect() 時,前一次的 doSomething() 和 clear() 肯定都執行完成了。

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

    而如果是非同步的,情況會變得很複雜,可能會很容易寫出有 bug 的程式碼。
    所以它不直接支援,如果使用者確定這個非同步操作與 useEffect() 的多次觸發沒有衝突,就自行封裝。
  • wxsm 2021-02-21 14:12:45
    @tedd 你不覺得很醜陋嗎,別的地方都用 async 這裡來個 then
  • wxsm 2021-02-21 14:12:45
    @azcvcza 說是這麼說,但是 async 的初衷就是讓非同步有同步的使用體驗,useEffect 明顯違背了這個初衷。
  • zhuweiyou 2021-02-21 14:12:45
    你返回了 promise, 它當銷燬函式處理了.

    不得不說, 確實 sb.
  • wxsm 2021-02-21 14:12:45
    @anjianshi 道理是這麼個道理,但複雜就不做,最終不是噁心了使用者嗎
  • iahu 2021-02-21 14:12:45
    我的理解是 hook 在每次 rerender 時都會生成一個快照,而且這些快照之間的資料結構是鏈式的。如果 useEffect 的 callback 是 Promise 型別的,那所有連結上的 hook 必將都得是非同步的,而 FC (functional component) 是同步的,在同步的 FC 裡想拿非同步的狀態就完全是另一套操作了。

    FC 和 hook 的搭配是函式式( FP )編輯思想,FP 構建是純函式之上的。之所以不能內部實現就是與設計思想不符,而且實現了就不能只在這一個地方實現,到處都得支援。
  • sm0king 2021-02-21 14:12:45
    看下 hooks 的實現原理或許可以解決你的疑問。
  • noe132 2021-02-21 14:12:45
    因為 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 2021-02-21 14:12:45
    @noe132 可惜我司的專案已經被 redux 套牢了
  • wxsm 2021-02-21 14:12:45
    @iahu 第一,hooks 目前並不是全部要求同步的,至少 useCallback 可以直接定義 async 函式。第二,useEffect 現在的做法也只能是假同步,要求別人在 effet 裡必須寫純函式,然而又建議有非同步需求的人在純函式裡面寫 IIFE,這種做法也同樣無法保證銷燬函式與 effect 函式的時序性。這就是單純的簡化了自己的工作,把困難的工作交給了使用者。
  • Kasumi20 2021-02-21 14:12:45
    useEffect 需要立即同步地返回一個清除函式,你為了用一個 await 語法,返回一個 Promise 給 React 幹嘛
  • joesonw 2021-02-21 14:12:45
    useEffect(fn: () => () => void).

    useEffect 是要返回一個函式() => void 的, 作為 unmount 的時候回撥用的. async 返回的話就變成 () => Promise<void>了. 簽名不對
  • iahu 2021-02-21 14:12:45
    - useCallback 的簽名是 `(a, b) => a` 第一個引數其實並不是 callback,在 useCallback 函式裡不會被執行,任何時候傳入相同引數都會返回相同的結果,它是符合純函式的規範的。
    - useEffect 屬性 effect hook 。它的執行和銷燬過程比較特殊,下執行時會清除上一次的狀態,在同步的渲染邏輯裡,怎麼保證能清除非同步的狀態?
  • soulmt 2021-02-21 14:12:45
    用 aysnc 標記的話,這個函式就是分步執行了,這樣的話在不報錯的情況下,react 可能無法同步拿到 return 的函式,所以無法控制訂閱的解除安裝導致程式碼不可控。在 react 的理念中,不可控是非常危險的操作,所以寧願讓程式碼噁心一點。
    就比如你想在 return 裡面解除安裝監聽,但是 react 因為 await 的問題,都不知道你什麼時候才能執行到 return,所以在執行到 return 這段時間內,你的所有操作都會造成不確定的結果。這是我想到的一個原因。
  • wxsm 2021-02-21 14:12:45
    @iahu 所以說心智負擔太重,寫個程式碼跟搞科研一樣
  • cattchen 2021-02-21 14:12:45
    此外,用這種寫法後,型別簽名不一致了,原本應該返回一個 Function 用來處理 unmount,現在變成了 Promise <TValue>
  • otakustay 2021-02-21 14:12:45
    因為 React 的生命週期不是你控制的,嚴格來說是使用者控制的(使用者要離開你不能阻止他)
    而對於 useEffect,在生命週期結束(或 deps 變化)時,副作用需要被清理
    並且對於一個 async 的過程,如果它沒有完成(未 resolve 也未 reject ),那麼從理論上來說,在 effect 清理時它是**必須**被中斷的,因為 async 過程完成後就會發生副作用,而此時 effect 已經結束了清理,這個副作用一但發生就會失控
    所以,React 很簡單地要求 useEffect 不能直接用 async 函式,來促使你處理 async 的中斷邏輯

    但很少有人理解到這個點,不但不處理 async 讓非同步過程隨意洩露不可控,還怪 React 的 API 設計
  • wxsm 2021-02-21 14:12:45
    @soulmt 其實這個我都懂。只不過我在想的是,為什麼這樣。比如解除安裝函式為什麼是 effect 的返回值,而不是獨立的一個引數
Add a reply
For Commenting you need to Login. If you dont have a Account you need to Register.