原文做者:Sebastian Markbåge
前端
譯者:UC 國際研發 Jothyreact
寫在最前:歡迎你來到「UC國際技術」公衆號,咱們將爲你們提供與客戶端、服務端、算法、測試、數據、前端等相關的高質量技術文章,不限於原創與翻譯。git
編者按:本文摘自 React Hooks issue,由 React 做者 Sebastian Markbåge 編寫,本文內容豐富,因此翻譯上也有難度,若是有翻譯不許確的地方歡迎指正反饋。github
看完全部相關的評論以後,我想總結一下個人感想。算法
不得不說,React Hooks 的反響很是強烈。它很受歡迎而且表現不錯。 你們廣泛承認它,並把它應用到生產中。它的名聲和使用規範貌似傳播得很好,還被其餘庫直接採用了。 固然這不是說不存在其餘可能的改變了,我想表達的是當前的設計並不徹底失敗。
設計模式
圍繞該機制的主要討論是 hooks 實際實現的注入和持久調用順序的依賴。 有些人但願其一,或者兩個都能改改。 但「最純的」模型就像 Monad(譯者注:一種程序設計模式)同樣。
數組
本質上,(傳入 hooks 的)參數是爲了替換掉 hooks 的實現代碼。這有點像通常的依賴注入和控制反轉問題。 React 沒有本身的依賴注入系統(與 Angular 不一樣),它也並不須要,由於大多數入口點是(主動) pull 而不是(被動) push 的(譯者注:可參照 Rxjs 中關於 push&pull 概念的解釋)。至於其它代碼,模塊系統已經提供了良好的依賴注入邊界。就測試而言,咱們比較推薦其它技術,好比在模塊系統級別進行 mock(例如使用 jest)。
安全
與之不一樣的是 setState,replaceState,isMounted,findDOMNode,batchedUpdates 等 API。事實上,React 已經用依賴注入將 updater 插入到 Component 基類中,做爲構造函數的第三個參數。Component 實際上啥都沒幹。這也正是 React 在 React ART 或 React Test Renderer 等相同環境中的不一樣版本中,具備多種不一樣類型的渲染器的原理。自定義渲染器也是這麼幹的。
服務器
理論上,相似 React-clones 這樣的第三方庫可使用 updater 來注入它們的實現。在實踐中,大部分人傾向於經過 shim 模塊來替換整個 react
模塊,由於可能存在某些權衡或者他們想要實現某些 API(例如,移除開發模式內容,或者合併基類及其實現細節)。
閉包
在 Hooks 的世界裏,這兩個選項仍然保留。 Hooks 其實並非在 react 包中實現的。它只是調用當前的調度程序(dispatcher)。正如我上面所說,你能夠隨時將其臨時重載爲任何你想要的實現,React 渲染器正是經過這一點,實現多個渲染器共享同一個 API。例如。你能夠專門爲單元測試 hooks 建立一個 hooks 測試調度程序。「調度程序」這個名字有點嚇人,但咱們能夠隨時改變它,這不是設計缺陷。如今,你能夠把調度程序移到用戶空間中,但這會給那些與該組件的做者幾乎毫無關聯的用戶帶來額外的干擾,正如這個 issue 中的大部分人並不瞭解 React 的 updater 同樣。
總的來講,咱們可能會引入更多的靜態函數調用,由於它們更適合搖樹優化技術,能夠更好地優化及內聯。
另外一個問題是,hooks 的主要入口是 react
而不是第三方包。極可能未來 react
會移除許多現有的東西,而 hooks 會被保留,因此 hooks 的膨脹並非問題。惟一的問題是,這些 hooks 隸屬於 react,而不是其它更通用的東西。例如 Vue 也曾考慮過實現 hooks API。然而,hooks 的關鍵是它的原語(primitives)很明確。在這一點上,Vue 有徹底不一樣的原語,而咱們已經對原語進行了迭代。其餘庫可能會提出略有不一樣的原語。在這一點上,過早地使這些過於籠統是沒有意義的。咱們選擇把第一次迭代在 react 上實現,只是爲了代表這就是咱們對原語的願景。若是其它庫有雷同,那麼咱們將建立第三方包以整合這些庫,並將 react
的庫重定向到該包。
要明確的是,咱們想談的並非執行順序的依賴。 先使用 useState 仍是先使用 useEffect 之類的問題並不重要。
React 中有不少依賴於執行順序的模式,正是因爲容許在渲染(render)中變異(mutation)(這仍然保持渲染自己的純淨)。
我不能更改代碼中 children 和 header 的順序。
Hooks 不關心你按什麼順序使用它們,它只關心順序是否持久,每次都按照同一個順序。這與調用之間隱含的依賴性截然不同。
最好不要依賴持久順序 - 全部事情都是平等的。可是,你得作權衡 - 好比說 - 語法干擾或其餘使人困惑的東西。
有些人認爲就算只是爲了純粹主義,那也該這麼作。可是,有些人也考慮得比較實際。
有人擔憂它會變得使人困惑,這可能發生在許多層面。我認爲這一點並不使人困惑,由於人們徹底無能爲力或只能放棄。但事實上,基礎知識很是容易掌握。
更有甚者,擔憂一旦出現問題,咱們很難弄清楚哪裏出了問題。即便你瞭解它的工做原理,你仍然可能犯錯誤,在這種狀況下,你必須得輕鬆找出問題並修復它。咱們發現了很多這類問題。通常來講它會被 lint 規則捕獲,報錯信息足以解釋緣由。可是咱們能夠作得更多。咱們能夠製造編譯硬錯誤,在開發模式中跟蹤 hooks 的相關信息,在順序切換時發出警告。在這些狀況下,咱們能夠優化錯誤消息,以顯示更改的堆棧,而不只僅是顯示 something changed。類型系統中逐漸有模擬效果的趨勢,例如 Koka。我敢打賭 JavaScript 確定會應用它,只是時間問題。
另外一個問題是,這些約束是否使編碼更加困難。對於普通的條件代碼,狀況彷佛並不是如此。它通常比較容易重構。缺少早期響應有點惱人,但也沒什麼大不了的。 Hooks 還有其餘與順序無關的難題。
可是,重構循環中的 Hooks 可能會很是煩人。解決方案一般是將循環體分解爲單獨的函數。這也挺不方便的,由於你須要把全部數據經過 props 傳遞,不然將會抱閉包問題。這是 React 中更難的問題了,不只限於 Hooks。這麼作是最佳實踐,有助於優化。例如,在更改列表中的單個項目時保持低渲染成本,確保每一個子項均可以獨立。使用 Suspense 意味着每一個子項均可以並行獲取並渲染而不是按順序獲取,報錯邊界具備相似的要求。所以就算是單獨解決 Hooks 問題,將循環體拆分爲單獨的組件仍然是最佳實踐 - 這也解決了 Hooks 的循環問題。
也就是說,Hooks 的最初實現能夠建立一個用做編譯器目標的鍵控嵌套做用域。它們確實建立了一種以嵌套方式支持 Hook 的機制。但它不是很是符合人體工程學或易於解釋,而且不管如何都會遇到上述問題。若是須要,咱們能夠在未來添加它。這如今它不該該是常見狀況。
咱們考慮的各類替代方案各自都存在許多缺點。
大多數方案不支持循環。這是關於 Hooks 無條件性的最大限制因素,彷佛許多提案都忽略了這一點。只是讓它在本身的條件下可用並無價值。
大多數方案並不解釋自定義 hooks 做爲常見狀況的緣由。咱們認爲這是實現性能和語法輕量級的重要目標。
一旦你容許 hooks 用於條件語句,有些地方會變得奇怪起來。例如,你可能會在條件中看到 useState,但這意味着什麼?這是否意味着它的做用範圍只在該塊中,仍是說它的生命週期隨之變化? if (c) useEffect(...)
是什麼意思?這是否意味着當條件爲真時該 effect 會觸發,仍是說每次 effect 爲真時它都會觸發?當條件爲否是卸載仍是繼續組件的生命週期?
對於像在 body 外聲明 hooks 的提議,屢次調用 hooks 意味着什麼?僅僅由於技術上能夠實現,不會讓它變得不那麼混亂。
大多數方案使用大量的間接和虛擬調度,很難進行靜態分析,這使得死代碼消除,內聯和其餘類型的優化變得更加困難。當前的 hooks 提議是很是有效的,由於它只有索引屬性,能夠輕鬆 minify,而且具備可預測的 O(1) 查找成本。請記住,文件大小很重要,這是當前設計真正的亮點。
此爲旁註:有人提到你們都關注併發的全局狀態。 若是未來 JS 支持線程,或者若是咱們編譯出某些支持線程的東西,咱們但願可以支持多個組件的並行執行。 可是,這在實踐中不是問題,由於咱們存儲的用於跟蹤當前正在執行的組件和當前 hook 索引的小狀態能夠輕易進入線程本地存儲 - 不管如何,這在某些形式的解決方案中始終是必需的,不管是可變域(mutable field)仍是代數效應(algebraic effects)。
你們廣泛關注調試會是什麼樣的。咱們從如下幾個角度來看。
首先是錯誤信息。爲了帶來更好的錯誤消息,咱們下了些工夫。當在 DEV(開發環境)中檢測到違反 hook 規則時,咱們至少能很好地處理錯誤。
斷點調試變得很是簡單,由於它只是使用正常的執行堆棧,這點不像 React 一般的作法。有些替代方案使用了更多的間接性,會讓斷點調試更難。
另外一個問題是樹的反射。 React DevTools 容許你檢查樹中任何內容的當前狀態。在生產包中,咱們常會將類名等進行 minify。極可能在咱們添加更多的生產優化,例如內聯和刪除沒必要要的 props 對象以後,若是沒有 source map,更多這樣的事情將不會自動進行。咱們沒有爲了生產調試而將元數據添加到 API 設計中的信仰。可是咱們能夠在開發模式時,進行輔助元數據(如 source map)等操做。
也就是說,咱們已經證實了咱們可使用 Debug Tools API 提取大量反射元數據。咱們還計劃添加更多,以便讓庫具備良好的擴展點,以便爲調試提供更豐富的反射數據,或者解析源代碼行以將名稱添加到單個 useState 調用。
你們都知道測試很重要,因此咱們得在更普遍的發佈以前清楚地記錄它。這一點毋庸置疑。
至於技術細節,我想上文提到的依賴注入點已經告訴了你能夠如何作到。
我認爲 API 設計中有一種感受,就是存在「可測試」的 API。當聽人這麼說時,我以爲他們會想到純函數之類的東西,只有一些輸入變量能夠單獨測試。 React API 很是簡單,你可能只會想直接調用 render 函數或直接調用單個 hook。
惋惜 API 的豐富性也帶來了些微妙差異。你不老是依賴它,因此你能夠常常在特殊狀況,或者在簡單的測試中一步使用它。可是,隨着代碼庫的增加,你會遇到愈來愈多這樣的狀況,而且你不但願在每一個代碼庫中都從新實現 React 運行時的全部細微差異。因此你想要一個測試框架。
好比說,咱們爲這個用例構建了淺層渲染器類。它容許你「使用」或者「遵循」(諸如此類的動詞)正確的語義來調用全部生命週期。測試 Hooks 原語的全部細微差異也挺有必要的。
然而在實踐中,咱們發現你們不怎麼使用淺渲染器(shallow renderer)。使用深層渲染更爲常見,由於你正在測試的工做單元一般依賴於更低的幾個級別,它們已經經過了測試。
也就是說,咱們還將包含一種與組件隔離的直接測試自定義 hooks 的方法。咱們要作的就是添加一些模擬調度程序的東西,並保持原語的語義一致。
這會取代 Redux 嗎?它會增長必須學習全部 Flux 知識的負擔嗎?與通常的 Flux 框架相比,Reducer 的使用範圍要窄得多。它很簡單。可是,若是你看一下框架/語言的方向,好比 Vue,Reason,Elm。這種調度並歸集邏輯,以在更高級別的狀態之間轉換的模式彷佛取得了巨大成功。它還解決了 React 中回調的許多奇怪問題,爲複雜的狀態轉換帶來了更多直觀的解決方案。特別是在並行的世界中。
在膨脹性方面(In terms of bloat),它並未給 React 添加其不須要的任何代碼。在概念方面,我認爲這是一個值得學習的概念,由於相同的模式不斷以各類形式出現,最好有一箇中央 API 來管理它。
因此我更多地把 useReducer 當成中心 API,而非 useState。 useState 仍是很棒的,由於它對於簡單用例來講很是簡潔而且易於解釋,但人們應該儘早研究 useReducer 或相似的模式。
也就是說,useReducer 並無作 Redux 和其餘 Flux 框架所作的許多事情。我通常認爲你不會須要它,因此它可能不像如今那樣廣泛存在,但它仍然存在。
有人說過,當你只想暴露一種消費 Context 的方法時,理想狀況下你不該該從模塊中暴露 Context Provider。看似 useContext 鼓勵你暴露 Context 對象。我認爲這樣作的方法是暴露一個自定義 hooks 以供消費。好比 useMyContext = () => useContext(Private),這一般會更好些,由於你能夠自由添加自定義邏輯、將其更改成全局邏輯或再行添加棄用警告。它彷佛不是須要框架進一步抽象來執行的東西。
咱們能夠考慮讓 createContext 直接返回一個 hooks ,咱們也鼓勵使用這個常見的模式。 [MyContextProvider, useMyContext] = createContext()
Context Provider 的另外一個怪異點是沒法用 hooks 提供新的上下文,你仍然須要一個包裝器組件。相似的問題還有你沒法經過 Hooks 或相似 findDOMNode 的方式將事件監聽器附加到當前組件。
這麼作的緣由在於,Hooks 在設計上要麼是獨立的,要麼只是觀察值。這意味着使用自定義 Hook 不會影響組件中未明確傳遞給該 Hook 的任何內容,它從不鑽入任何抽象層次。這也意味着順序依賴可有可無。惟一的例外是在處理相似遍歷 DOM 的全局可變狀態時。它是一個逃生口,但不是你能在 React 世界中濫用的東西。這也意味着使用 Hooks 不依賴於順序。像 useA(); useB();
或者 useB(); useA();
這樣調用都行。除非你經過共享數據的方式顯式建立依賴項。let a = useA(); useB(a);
到目前爲止,最怪異的 Hook 是 useEffect。須要明確的是,它預計是迄今爲止最難使用的 Hook,由於它使用較難管理的命令式代碼,這就是咱們試着保持聲明式的緣由。可是,從聲明式變爲命令式很難,由於聲明式能夠處理更多不一樣類型的狀態和每行代碼的轉換。當你實現某個效果時,理想狀況下也應處理全部隨之而來的 case。這麼作的部分目的是鼓勵處理更多 case,這樣的話有些怪異點也是能夠解決的。
毫無疑問,第二個參數挺古怪的。把它做爲第二個而不是第一個參數是由於對於全部這些方法,你能夠先編寫代碼,而後再進行添加。該屬性的好處是你能夠在 IDE 中使用 linter 或代碼重構工具,或者讓編譯器根據你的回調自動添加它。這是從 C# 中吸取的經驗,其中語法順序旨在支持自動完成等功能。
也許它該有個比較函數。我尚未看到不能將它重寫爲輸入的狀況,但不管咱們是否添加比較函數,咱們均可以放到以後作。那也須要一個輸入數組,來讓咱們知道要存儲及傳遞什麼給比較函數。
如今不容許使用異步函數做爲 effect,意思是你必須費盡心思來作異步清理。很難保證異步 effect 的正確,由於這些步驟之間一切皆有可能。在初始化新 effect 以前不能進行清理,不然可能會影響 effect 的屬性。以後咱們有可能放寬這種約束,但我懷疑它是一個糟糕的模式,也許咱們不該該鼓勵在第一個版本中使用它。
useEffect 最奇怪的狀況是在使用閉包時。這會在咱們想要跟蹤某個取值的時候混淆視聽。由於閉包的值實際上不是 reactive 的,它們會捕捉當前的狀態,實際上這是一個不錯的優勢。因爲批處理和併發模式,多數狀況下,事物以意想不到的方式交織。因爲違反直覺的閉包,捕獲的值確實會致使錯誤,但一旦修復,會大大減小它們的競爭條件問題。
另外一個是內存使用問題。我想說 React 的內存使用量通常都是不可預測的,由於咱們基本上都記得樹中的全部東西。可是,因爲閉包共享執行環境,它可能致使額外的反直覺延長。這些能夠經過使它成爲一個自定義 hooks 來解決,但它不老是那麼明顯,因此有時候你必須這麼作。瞭解此模式的優化編譯器也能夠輕鬆解決此問題。
針對這個問題,有個解決方案是咱們能夠引入 useEffect 做爲相同的函數,將全部輸入參數傳遞爲函數的參數,而且鼓勵掛起它們。但這是有問題的,由於閉包的好處是方便引用計算值。其餘模式咱們也鼓勵使用閉包。因此這彷佛破壞了「一切都進入方法體」的思想。這反過來又解決了其餘問題,例如默認 props,計算值等。我不肯定這對於剩下的少數狀況來講是否值得去作,但不作的話會遺留更多。
有幾我的指出咱們缺乏了一些 API。
setState
的第二個參數在此模型中不能很好地運行。一樣,咱們在 ReasonReact 中尚未相似 UpdateWithSideEffects 的東西。咱們已經考慮好了如何使用它,並會在以後作補充。例如,在 reducer 中調用 emitEffect。
因爲狀態轉換,咱們沒有辦法改動單個組件。若是有方法,那麼咱們可能反過來須要像 forceUpdate
那樣繞過它來進行改變。
咱們尚未 getDerivedStateFromError 和 componentDidCatch 的替代方法,但咱們有 HOC 來提供此功能。好比 catch((props, error) => { if (error) setState(...); useEffect(() => { if (error) ... }); })
。咱們會在以後添加。
一直以來總有人問,是否能夠有更底層的 API 來實現其餘語言(如 ClojureScript 或 Reason)的特定語義。這是咱們確定想要支持的,但我不肯定是否該使用相似公共 API 的機制來完成。好比說,React 的優化編譯器須要一個不一樣的入口點來定位該格式。該機制要能夠用於各類較底層的操做,沒必要進行易用性優化。所以,咱們可能會將其與公共 API 分開添加。
我相信大多數 JavaScript 類型的問題都已獲得解決,由於 Flow 和 TypeScript 如今都已定義。
有個有趣的問題尚未被說起:即便發生了調用順序錯誤的狀況,是否仍能夠在其餘語言(例如 Rust 或 Reason)中正確地編碼。 這尚未獲得證明 - 至少在非運行時沒有。
有些人擔憂這些非純函數會影響編譯器的優化。對此我有異議:咱們也想,而且已經作了許多運行時或靜態執行的優化。
事實上咱們努力地想將 Hooks 拿出來,由於它很是適合優化。它謹慎地鼓勵許多靜態的解決模式產生。
有兩個優化是關於合併組件的。對於由父組件無條件渲染的組件來講,這是普通 hooks,你能夠直接調用。你能夠在用戶空間中執行此優化。即便是對於循環和條件,咱們也知道如何爲它們添加相同做用的做用域。即便是動態渲染的組件,咱們也能夠跳過額外 Fibers 的建立,例如當一組父組件渲染一個也是函數組件的子組件時。咱們只須要在函數類型改變的狀況下,跟蹤切換髮生的順序就行了。
這對於基於記憶(memoization)的優化一樣有效。在具備代數 effect 的語言中,記憶功能只須要同時記住 effect 就像。這裏也同理。記憶只須要跟蹤調用期間發出的 hooks 。
許多使用對象或傳遞頭等函數的替代方案,須要以某種方式展開,這種方式因爲其間接性而加大了實際優化的難度,其中尤以 Generator 的難度最甚。
有人提到關於 setState 中的重載 API,它接受函數或函數的返回值做爲參數。 這是一個艱難的設計決定,咱們進行了諸多權衡。 確實,重載的 API 有時會致使難以預料的安全問題。 咱們就曾有過一例因子組件同時接受字符串或元素而致使的問題。 但也有許多重載的 API 是很是有用的抽象,不會致使安全問題。 我會更深刻地研究這一點,以確保對風險作合理的評估。
還有一個還沒有提出但我想說明的問題。 若是你認爲第三方 Hook 不可信,由於它能夠有條件地添加/刪除其狀態 hooks ,而後從其 hooks 的末尾開始讀取。 你容許它從外部組件讀取狀態。 若是你能夠執行代碼,一般你就輸了,但在相似 Caja/SES 的環境中,這多是相關的。 這是比較不幸的狀況。
爲何全部特殊的 hooks 都是核心?因爲全部機制都必須存在,所以它們中的大多數並無真正增長體積,而更多的是概念開銷。其中大多數是沒法在用戶空間中實現或經過描述意圖提供重要價值的原語。
例如,你如今能夠在用戶空間中使用 useMemo,但咱們但願,未來在低內存狀況或窗口組件中丟棄記憶值的狀況下,狀態仍能被保留。
useCallback 只是一個圍繞 useMemo 的簡單包裝器,可是咱們已經想好將來如何進一步優化它們,不管是靜態仍是使用運行時技術。
useImperativeMethods 提供了一個能夠在用戶空間中構建的 API,但因爲咱們有幾種不一樣的方式與 refs 交互,所以以單一規範方式維護它們更好一些。咱們已經兩度更改了 refs。
我一直聽到的一個爭論點是這次改變的動機不足,由於「類(class)挺好的呀」。聽說那幫試圖學習的用戶奔潰了。我認爲這個論點過於片面,有人還強調 class 對於新手來講難比登天呢——因此這不是重點。
主要動機是像閉包這樣的模式天然會建立值的副本,這使得編寫併發代碼更加簡單,由於你能夠在任何給定點存儲 n 個狀態,而不是在可變類的狀況下只存儲一個狀態。這避免了許多類的坑,由於類看起來很直觀但實際結果難以預料。
類(class)彷佛是保持狀態的理想方式,由於它本就爲此而生。可是,React 更像是一個聲明函數,它不斷被反覆執行以模擬反應狀態。兩者有一個阻抗不匹配,當咱們把這些看做是類時會不斷泄漏。
另外一個是 JS 中的類在同一命名空間中合併方法和值的問題。這是的優化難以進行,由於有時方法的行爲相似於靜態方法,有時又相似於包含函數的值。 Hooks 模式鼓勵對輔助函數使用更靜態可解析的調用。
在類中,每一個方法都有本身的做用域。它致使像咱們這樣的問題必須從新發明默認 props,以便咱們能夠建立一個單獨的共享解析對象。咱們還鼓勵你使用類中的可變字段在這些方法之間共享數據,由於惟一共享的是 this
。這對於併發性來講也是有問題的。
另外一個問題是,React 的概念心智模型只是遞歸調用其餘函數的函數。用這些術語表達它有不少價值,有助於創建正確的心智模型。
我很是同情的一個問題是,這隻會增長學習 React 的腦力成本 - 短時間內。那是由於你可能要在可預見的將來學習 Hooks 和 class。要麼是由於兩者你都使用,你的代碼庫中有以前的類或者其餘人編寫的類、你在 stackoverflow 上讀過的一個例子或教程使用的類,或者你正在調試的一個庫使用了它。
雖然須要不少年,但我打賭其中一種方法將取得勝利。要麼咱們必須回滾 Hooks,要麼逐漸減小 class 的使用,直到它們徹底消失爲止。
我認爲你們的批評很公正。咱們的確沒有明確的答覆,或給出「能夠合理地將 class 移出核心並外化爲 compat 層」的預計路線圖。我認爲短期內咱們都不會給出明確答覆,直到實際看到 Hooks 變得更好。到那時候,咱們會看是否給出下降類重要性的時間線。
英文原文:
https://github.com/reactjs/rfcs/pull/68#issuecomment-439314884
好文推薦:
React 16.x 路線圖公佈,包括服務器渲染的 Suspense 組件及Hooks等
React 官方發佈 V16.6.0,新增支持 lazy,memo 和 contextType
「UC國際技術」致力於與你共享高質量的技術文章
歡迎關注咱們的公衆號、將文章分享給你的好友