這是一篇純技巧向的文章,跟一年多以前的《揭祕 Vue-3.0 最具潛力的 API》同樣_[0]_,更少的背景鋪墊,更多的代碼,更多的 demo,更快的節奏。html
讓咱們直接進入主題。前端
前一陣子,有開發者發推特表示用 Svelte 實現跟 React 同樣的功能_[1]_,代碼簡潔不少。vue
他給出的 React 代碼以下:react
而 Svelte 代碼以下:redux
用了複雜的 react 處理,佐證 svelte 的優越,這不是一個很公平的對比。api
而後,Vue 也加入了試煉 [2],尤小右給出了他的代碼:markdown
跟上面 Svelte 版本同樣簡潔。app
然後,小右又提供了 ref-sugar 版本,更爲精煉。框架
聽說,在推特上:dom
React 實現一堆人搗鼓了半天也沒弄出個能在 el ref 變動時候從新 observe 的版本...
每當 React hooks 被拉出來溜的時候,通常都是負面的。確實很容易寫出性能有問題的代碼。
關鍵是,你們過輕易使用 useState 了。
在 vue-composition-api 中,reactivity 數據都有 wrapper,好比是 reactive 和 ref。
useHeight 等 custom-vca 裏無論產生多少個 reactivity 對象,不會直接產生 re-render。
只有那些被 return 到最外部,跟 template 綁定的部分,會觸發視圖渲染。
而 react 的 reactivity 就是經過 re-render 實現的,useState 沒有 wrapper,一用就獲得一個觸發渲染的函數。
在這種 reactivity 機制下,須要特殊的心智模型去編寫代碼—— State/Effect 分層。
useHeight 若是是下面這樣:
let [ref, height] = useHeight()
複製代碼
高度變化時,被動 re-render,難以過濾,難以轉換,難以合併。
大部分狀況下,不是提供 state,而是提供 effect,可能更好。像下面這樣:
let [height, setHeight] = useState(0)
let ref = useHeight((height) => {
// do something with height
setHeight(height)
})
複製代碼
由使用者去外部聲明 state ,而後在 useHeight 的 effect callback 裏按需 setHeight。
更新權反轉。
使用者可能結合其它狀態,作 dispatch 到 reducer 裏的總體一次性更新,而不是被動 re-render。
惋惜目前 React 社區還沒創建清晰的 State/Effect 區分。
咱們能夠根據 State/Effect 分層理念,嘗試給出 React hooks 渲染友好的公式:
let handler = useProducer(consumer, options)
複製代碼
producer 生產者接收 consumer callback 消費者回調做爲參數,返回 handler 控制權函數,用以綁定到事件或者其它 consumer 位置。
以 useHeight 做爲案例,驗證上述公式的效用。
咱們要實現的功能以下:
給定一個 textarea,它是 resizable 的;咱們追蹤它的尺寸變化,並展現到文本里,同時咱們增長一個 checkbox 控制,能夠切換監聽 / 不監聽。同時,咱們只監聽必定範圍內的尺寸變化,而忽略超出範圍的。
即更精細的控制:
1)控制監聽 / 非監聽;
2)控制監聽的範圍,只響應符合要求的渲染需求。
功能以下:
個人代碼實現,按照從 low-level 到 high-level 層次迭代上來。
首先實現 useResizeObserver,一個對 dom api 的 low-level 適配。
返回 3 個控制函數:
1)trigger 決定監聽哪一個元素;
2)enable 啓動監聽;
3)disable 禁用監聽;
useResizeObserver 的實現思路,跟你們以前慣常作法不一樣,它不返回 state 出去,不會引發 re-render,而是調用 callback(target),將 resize effect 暴露出來。
而後,咱們基於 useResizeObserver 實現 useHeight,將 observe 的關注點縮小到 offsetHeight 裏,並提供 getCurrentHeight 的新方法。
一樣的思路,咱們還能夠實現 useWidth 等其它監聽。
使用時,在 useHeight(onHeightResizeEffect) 裏,按需將 currentHeight 同步到 setHeight 裏。
經過 observer.trigger 決定監聽哪一個元素,並提供 enable/disable/getCurrentHeight 等控制函數,經過 useEffect 監聽,在合適的實際調度不一樣的控制。
以上,渲染友好、控制精細的 React-Hooks 版本完成。
react-resize-observer 的 demo 地址
代碼或許比 Svelte 和 Vue 的版本長一點,但 State/Effect 的這種實現思路是通用的,在 Vue 裏也適用。
下面演示,基於相同理念,分別用 Rxjs,Vue 和原生 JS 實現相同功能。
實現 rxjs 版本的 use-height。也要支持:1)切換 trigger;2)切換 enable/disable;3)在 enable 狀態下,filter 部分 height 不作響應。
功能以下:
先實現 resizable,適配 low-level 的 dom api 爲 rxjs 的 observable 版本,對應以前 react-hooks 適配的 useResizeObserver。
而後實現 onHeightChange,經過 subject 反轉控制,switchMap 實現切換,返回 state$, trigger, enable, disable 四個字段。
這裏返回 state 不是 state,它的 subscribe(consumer) 裏就帶一個 consumer。性質上是同樣的。
經過 subscribe(state$) 去消費數據,經過 rxjs operators 過濾 / 轉換 / 轉接數據流。在其它 observable 數據流裏,調用 handler 裏的控制方法,disable/enable/trigger 便可。
rxjs-resize-observer 的 demo 地址
接下來,實現 vue/reactivity 版本的 use-height。思路跟 react-hooks 版本和 rxjs 版本也是同樣。
先實現一個將 resize-observer 這個 low-level api 適配爲 vue reactive state 的函數。
定義一個 shallowReactive,監聽 state.trigger 的變化,實現切換 resize-observer 的功能,並將 target 發送給 onResize。
再實現一個 onHeightChange 函數,拓展 ResizableState 爲 HeightChangeState,增長 status 字段提供 enable/disable 切換,增長 height 字段。
上面作了兩個 effect 的組合:
1)將 onResize effect 根據 option.fiter 映射到 state.height;
2)將 state.trigger 同步給 resizeState.trigger
onHeightChange 使用方式以下:
onHeightChange 定義了一個 state-effect bindings,但沒有指定監聽哪一個元素,也沒有指定如何消費數據。
經過 state.trigger 賦值決定監聽誰。
經過 effect 決定如何消費。
功能以下圖:
細心的同窗可能已經發現了,上述 Vue 代碼,只用到了 @vue/reactivity 包,而沒有用 vue 視圖相關的代碼。
沒錯。
vue/reactivity 自身已經足夠強大,能夠獨立實現不少功能。在《揭祕 Vue-3.0 最具潛力的 API》有更多解讀。
這裏提供一個新的應用潛能方向。
vue/reactivity 能夠實現一種 reactive effect bindings 模式,能夠歸納爲 reactive({args, returned}) & effect。
全部 function,都有 args 參數和 return value;將它們映射到一個 reactive state 中去,能夠作到,state.args= 設置時,從新執行 effect,將結果映射回 state.returned。
這個模式對前端開發者很是友好,由於 dom api 大部分就是這樣的。
elem.style.color = 'red',觸發渲染 effect,將結果同步給 computedStyle(elem).style。
回到 vue-resize-observer,上述模式對應的是:
resizable 的 state.trigger 實際上是 ResizeObserver.observe 的參數,它的返回值被定義爲 elem 自己,不符合 vue watch 的條件,所以,從 state.target 字段,變成 onresize effect callback。
當 effect 能夠同步到 state 時,咱們採用 state;當 effect 難以同步到 state 時,咱們採用 effect callback。
onHeightChange 的 on height change effect 能夠同步到 state.height,所以咱們取消了 effect callback。
能夠拓展 filter 等加強操做,去加強 effect -> state 同步的細節控制,拓展 state.status 等附加信息等。
當整個 vue app 由 state-effects-bindings 模式來構建,理想狀況下,最後將自動造成一個大的 app state,整個應用狀態自動可配置化。
appState.menu.status = 'on' 就打開了菜單。
appState.loading.text = 'xxx' 就展現了 loading。
effect(() => appState.menu.status) 就監聽了菜單變化。
而且這個構建過程,不是自上而下的,不是先定義一個 global app state 而後在這個約束下去分配 state 給 sub-component/model。
能夠是自下而上的,每一部分只處理本身關心的部分,並將 state/effect 的能力暴露出去,由更大的部分去將 state/effect 經過 bindings 映射到更大的 state/effects,如此不斷整合,最後造成了一個 app state
如 resizable 只提供了 state.target,而 onHeightChange 在此基礎上拓展了 {height, status} 等狀態。
當咱們認識到,問題無非是 state-effect-bindings 的不斷冒泡時,有沒有 react-hooks,有沒有 rxjs,有沒有 vue-reactivity,不是必要的。
先實現 Resizable,接受一個對象參數,返回一個對象。對象參數是 visitor/consumer,返回對象是 handler。
基於 Resizable 實現 HeightChange,接受更多字段的對象參數,返回更多方法的對象。
而後在 consumer 參數中,指定如何消費數據,如何轉換數據,如何過濾數據。
在 event listener 等 bindings 中,指定如何調度 handler 方法。
vanilla-resize-observer 的 demo 地址
功能以下圖:
儘管用原生 JS 也行,但有 react-hooks, rxjs, vue-reactivity 也是有幫助的,此處主要展現 State/Effect 分層理念在 UI 開發裏的通用性。
如上,咱們演示了 react, rxjs, vue, vanilla-js 在 State/Effect 分層理念下,實現相同功能的代碼實現。
咱們能夠在 State Management 概念的基礎上,再增長一個 Effect Management 的概念。
正如我以前在某個知乎問答裏所說的:
1)當你的項目數據複雜度很低,用 react 自帶的 component-state 就能夠
2)當你的項目數據複雜度通常,lift state 到 root component,而後經過 props 傳遞來管理
3)當你的項目數據複雜度較高,mobx + react 是好的選擇
4)當你的項目數據複雜度很高,redux + react 能夠幫助你維持可預測性和可維護性的降低曲線不那麼陡。全部 state 變化都由 action 規範化。
5)當你的項目數據複雜度很高且數據來源很雜,rxjs 能夠幫助你把全部 input 規範化爲 observable/stream,能夠用統一的方式處理。
思路其實很簡單:
1)當 UI 變化很複雜時,用 component 歸一化處理,即 View-Management
2)當 state 變化很複雜時,用 action/state 歸一化處理, 即 State-Management
3)當 data-input 很複雜時,用 rxjs/observable 等概念歸一化處理,即 Effect-Management。
任意問題,只要足夠廣泛和複雜,就值得抽象出專門化的機制。
Effect Management 在前端開源生態裏,還不是很繁榮,是一個你們能夠努力的方向。不只 rxjs 能夠作 effect 管理,vue/reactivity 和 react-hooks 乃至原生 JS 裸寫都能作,有待你們的發掘。
更具體地說,前端社區能夠往下面幾個方向努力:
1)基於 react-hooks 和 State/Effect 分層理念從新梳理哪些應該暴露爲 effect,哪些應該暴露爲 state,提供一個性能更加良好的版本(只能用在 react 體系)。
2)基於 vue/reactivity 和 State-Effect-Bindings 理念,實現的 state/effect 管理框架(脫離 vue 視圖框架也能使用)。
3)基於 rxjs 和 State-Effect-Observeable 模式,實現 state/effect 管理框架(能夠配合任意視圖框架使用)。
期待有同窗能在上述方向上產生成功案例~
引用來源:
[0] 《揭祕 Vue-3.0 最具潛力的 API》
[1] react 和 svelte 實現 use-height 的對比
[2] vue 做者的 use-height 實現