關於 React Hooks 的一些使用經驗和換角度反思

本文基於 https://reactjs.org/docs/hook... 功能展開

算算時間都要一年半了, React 在 2018 年推出 Hooks, 引起了熱議.
印象裏就是在羣裏面, 我就很糾結裏邊的黑魔法太奇怪了.. 看得當心翼翼的.
而後看着別人研究代碼, 提出相似的實現之類的, 或者各類解釋.
慢慢地不少不一樣的聲音也發出來, 特別是迷之閉包, 不少人都中招了出來吐槽.
後來呢, React Hooks 蔓延開來, 連 Vue 社區也開始模仿.. 看來是真重要了.
因爲我沒有動力去深刻 React 完整的實現, 因此對細節也只是處在一個大體的瞭解的狀態.html

對於 Hooks 想要解決的問題, 我大體是認同的, React 的擴展功能太僵化了.
高階組件, 雖然在 FP 裏面牛逼哄哄的名字, 實際設計起來 decoration 滿天飛,
..太奇怪了, 一個 FP 裏引入的概念, 用大量的 OOP 和 mutable 方法來實現.
並且去掉了 mixin 機制以後, React 複用邏輯的問題感受就是個坑, 重複代碼挺多的.
雖然我是不信任 React 搞這種黑魔法推翻之前的一些宣傳口號的, 可是...
Hooks 確承認以幫我解決 class based 組件難以作好的邏輯複用的問題.前端

我在公司裏推 Hooks 的時間比較晚, 已是在活動聽到別人折騰 Hooks 以後了.
最開始是 Ajax 代碼複用, 網上當初那個例子很明顯, 如今咱們也抄了不少這種.
可是後來, 讓我感覺到最大變化的, 仍是發現 Hooks 對咱們的 Form 組件的改善,
能夠先看例子 http://fe.jimu.io/meson-form/...
大體上說, 就是用 Hooks 抽離狀態的話, 複用場景更加靈活, 超出組件層面...react

這篇文章是我幾個跟 Hooks 相關的想法梳理, 線索有點亂, 看章節吧.git

若是能用 Macro 實現 Hooks?

咱們知道 Hooks 最開始就明確說了, useState 等 API 調用, 跟依賴相關, 且必須是靜態的.
緣由也不難明確, React 組件運行過程當中要逐個追蹤, 動態性會破壞這個邏輯.
我用 ClojureScript 的時間挺長了的, FP 當中通常的玩法我是知道的,
通常來講, 爲了代碼的"引用透明", FP 當中會避免四處存在內部狀態.
一個用 Hooks API 維護狀態的函數組件, 自己竟然有這樣的狀態, 很反 FP 常規的玩法.
並且這個, 雖然如今是習慣了, 可是總歸是在有一些限制的, 使人忌憚.程序員

換個角度的話, 也不能說 FP 裏面就沒有這種狀態的性的東西.
而是說, FP 當中狀態是習慣於顯式跟普通的計算區分開的,
你要用狀態, 就明確聲明這邊有個狀態, 你們調用都注意. 否則, 那就是純的計算.
或者, 再換一個層次說, 你這不是代碼, 就是 DSL, 這個 DSL 提供新的一層抽象.
DSL 至關於構建一套原先的代碼之上的一層新的語義, 那無所謂 FP 不 FP 了.
能夠把深層的邏輯經過複雜的手段約束在 DSL 語義內部, 上層就是調用 DSL.
應該說我理解 React Hooks 就是這樣的一個狀態, 語言之上的 DSL.github

做爲 DSL 說的話, 我以爲 Svelte Vue 那樣卻是更天然了.
倒不是說它們提供的方案必定比 React 好, 可是有一個編譯階段, DSL 就更完善,
首先 DSL 某個語義未必就是一個函數就能實現的, 可能須要些比較繞的代碼,
其次, 提供語義也就意味着對用戶的術語有約束, 就須要編譯以前作驗證,
但 React Hooks 這邊, 這就是 runtime 插入功能而後讓人們本身去作限制了.數組

固然, 我持有的觀點, 有一點是 JavaScript 沒有 Macros, 限制了 Hooks 的設計.
我用 ClojureScript 做爲 Lisp, 比較容易修改語法樹, 展開簡單的代碼,
...應該說 Lisp 這種, 能力也遠不如 Babel 甚至 Svelte 那麼強大, 只能說廉價.
Hooks 的設計, 在加強功能的同時, 很大程度是想着保留 API 的簡潔.
若是有 Macros 能夠用的話, Hooks 能夠考慮的寫法還有不少,
好比說定義組件的時候寫成,閉包

(defcomponent c-demo [x y] (div {} (str x y)))

而後由 Macros 系統展開成函數, 而且由執行環境諸如幾個變量(影響到 ts 什麼我就無論了),app

(defn c-demo [x y]
  (fn [use-state use-effect internals]
    (div {} (str x y))))

咱們目前須要全局引用 useState useEffect 而後內心去想着那是運行時的東西如何如何,
可是從上面的展開例子, 從 Macros 的角度理解, 很容易知道這些是運行環境控制的操做,
這樣就沒有原先 React Hooks 那種讓人產生些許錯覺的寫法了.
(有可能語法展開跟函數執行的區別對非 Lisp 程序員比較難區分... 大體按 Babel 去想吧.less

另外一個是狀態追蹤的問題 Hooks API 依賴的是 useState 的順序.
我我的比較傾向於用名稱去控制這多個狀態, 在用戶端有更多控制能力.
而這部分在跟 Vue 和 Svelte 對比會發現 React 這邊能設計出更多的花樣.(沒去了解具體實現)
而 React 使用函數做爲惟一的手段, 就顯得很是, 或者仍是逃不脫被 DSL 要求存在的限制.
固然話說回來, React Hooks 單調的方式能被設計出來知足這麼多功能, 也是超乎我想象了的.
同時也因爲抽象手段單一, 就很是依賴 runtime 內部實現奇奇怪怪的手法去支持功能.

Respo 拙劣的山寨

Respo 是我我的項目使用的 cljs 之上的 Virtual DOM 方案. 並不基於 React.
這樣我就有機會試驗我本身思考的一些組件和狀態的抽象方案.
Respo 裏邊, 爲了保存熱替換過程當中組件的狀態, 設計了用戶態的狀態樹的概念,
用戶在定義組件的時候, 大體上對一個 Todolist 會在全局構建和存儲這樣的樹形結構:

{
  :data {}
  :todolist {
    :data {:input "xyz..."}
    "task-1-id" {:data {:draft "xxx..."}}}
    "task-2-id" {:data {:draft "yyy..."}}}
    "task-2-id" {:data {:draft "zzz.."}}}
}

組件代碼當中須要比較囉嗦的聲明,

; for app
(comp-todolist (>> states :todolist))

; for single task
(comp-task (>> states (:id task)) task)

另外具體使用還要比較囉嗦的聲明,

(let [state (or (:data states) {:input ""}]) "TODO whole list")

(let [state (or (:data states) {:draft ""}]) "TODO Task")

以及在組件層級之間傳遞 cursor 的路徑位置, 以便發起更新...
相比 React 任何一種多了不少的代碼, 只能說爲了熱替換穩定作了很是拙劣的模仿,
而後, 再次以後, 我也能在這個方案上, 也提供相似 Hooks API 的狀態抽象,

(defn use-menu-feature [states]
  ; TODO
  {:ui "TODO",
   effect-fn "TODO",
   :edit-fn "TODO"})

(menu-A (use-menu-feature (>> states :menu-a) {}))
(menu-B (use-menu-feature (>> states :menu-b) {}))

這是一個脫離了 Macros 和編譯方案, 也脫離了 React 內部狀態黑魔法的方案,
而這樣簡單囉嗦的方案, 切切實實也能模仿出 Hooks 核心的一些功能來.
(固然在總體功能上, 跟 React 不能比, 並且要實現的話也依賴 runtime 作.)

這個例子對於 React Hooks 自己沒有什麼幫助或者闡釋,
主要是從另外一個角度去看, 做爲一個前端 MV* 方案, 怎麼看待其中的核心需求和實現.
各類方案在各類需求點的路徑探索以及取捨, 脫離一下限定的視角, 會有其餘主意.

業務當中內秉的狀態

回到 Hooks 自己, 我目前在的業務當中探索使用的方案, 大體能夠參考 Form 這個例子,

let formItems: IMesonFieldItem[] = [
  { type: 'input', name: "name", label: "名字", required: true },
  { type: 'select', name: "city", options: selectItems, label: "城市" },
];

let [formElements, onCheckSubmit, formInternals] = useMesonItems({
  initialValue: {},
  items: formItems,
  onSubmit: (form) => {
    console.log('After validation:', form);
  },
});

或者也參考彈出提示的這個例子,

let [ui, waitConfirmation] = useConfirmModal();

let onClick = async () => {
  let result = await waitConfirmation({
    text: "節點可能包含子節點, 包含子元素, 刪除節點會一併刪除全部內容.",
  });
  console.log("result", result);
}

return <div>
  <button onClick={onClick}>Confirm</button>
  {ui}
</div>

相較於 React 組件以往的抽象方案來講, Hooks API 暴露出來了一個從前咱們熟悉的功能.
就是: 能夠修改抽象模塊的內部狀態了.
以往 React 宣傳的組件, 做爲一個抽象, 內部的狀態是封閉的, 外部不該該去操做.
好比就一個彈窗的 Modal, 你就須要外邊有個 visible 的狀態去控制, 傳進 props, 顯示, 仍是不顯示,
可是有了 Hooks, 這時候你能夠把狀態封進 Hooks API 內部, 而後暴露回調函數去操做.
但是你又明顯能知道, 這邊封裝了一個狀態.
至少從前 React 並不鼓勵這種狀態, 或者應該說, 此前並不存在簡單的這樣的功能.

反觀咱們實際業務當中, 局部的狀態倒是很常見的東西,
好比 visible, 以往咱們經過插件去實現的時候, 不就是把狀態藏在插件內部的嗎,
而後獲得一個 .toggle() 的方法, 而後可能還會多個地方去 if (modal.visible) {} 一次次匹配.
當 React 提出須要一個父組件存一個 visible 還有加一個 onChange 回調的時候, 讓人以爲很怪異,
最終咱們但願暴露給業務當中使用的時候, 明顯 .toggle() 纔是極爲簡短清晰的方案.

順着這個思路, 詭異的事情來了, jQuery 時代, 咱們歡快地 .toggle(),
React 來講, 說 rethink, 而後咱們加了 visibleonChange, 而後以爲 React 說得對,
如今基於 Hooks 方案, 封裝局部狀態的方案又開啓了... 歷史又陷入螺旋了.

其餘

...今天來不及寫了, 個人感受就是 React 選擇 FP 這條路的時候, 其實沒那麼清晰,
探索了那麼多方案, 強行把函數式的一些說法套用到前端這邊來, 可能不少人都不能理解準確吧,
你說是 stateless 仍是 state isolation, 細小的差別, 沒有梳理清楚.
真的函數式語言, PureScript Elm 怎麼寫的前端, 真的是 React 這樣子的嗎?
在狀態這個事情上, React 最終仍是向新的方案妥協, 並且將來誰說的得準要不要再妥協一次呢.

至於 Virtual DOM 帶來的 Model->View 那種 date-driven 的模式,或者換個裝逼的說法 "DOM 更新方案的自動化進程", 從 Angular/React 實打實普及到了整個前端領域.這一點上沒有多少分歧吧... 睡了睡了.

相關文章
相關標籤/搜索