1. 引言
react-easy-state 是個比較有趣的庫,利用 Proxy 建立了一個非常易用的全域性資料流管理方式。
import React from "react";
import { store, view } from "react-easy-state";
const counter = store({ num: 0 });
const increment = () => counter.num++;
export default view(() => <button onClick={increment}>{counter.num}</button>);
複製程式碼
上手非常輕鬆,通過 store
建立一個資料物件,這個物件被任何 React 元件使用時,都會自動建立雙向繫結,任何對這個物件的修改,都會讓使用了這個物件的元件重渲染。
當然,為了實現這一點,需要對所有元件包裹一層 view
。
2. 精讀
這個庫利用了 nx-js/observer-util 做 Reaction 基礎 API,其他核心功能分別是 store
view
batch
,所以我們就從這四個點進行解讀。
Reaction
這個單詞名叫 “反應”,是實現雙向繫結庫的最基本功能單元。
擁有最基本的兩個單詞和一個概念:observable
observe
與自動觸發執行的特性。
import { observable, observe } from "@nx-js/observer-util";
const counter = observable({ num: 0 });
const countLogger = observe(() => console.log(counter.num));
// 會自動觸發 countLogger 函式內回撥函式的執行。
counter.num++;
複製程式碼
在第 35 期精讀 精讀《dob - 框架實現》 “抽絲剝繭,實現依賴追蹤” 一節中有詳細介紹實現原理,這裡就不贅述了。
有了一個具有反應特性的函式,與一個可以 “觸發反應” 的物件,那麼實現雙向繫結更新 View 就不遠了。
store
react-easy-state 的 store
就是 observable(obj)
包裝一下,唯一不同是,由於支援本地資料:
import React from 'react'
import { view, store } from 'react-easy-state'
export default view(() => {
const counter = store({ num: 0 })
const increment = () => counter.num++
return <button={increment}>{counter.num}</div>
})
複製程式碼
所以當監測到在 React 元件內部建立 store
且是 Hooks 環境時,會返回:
return useMemo(() => observable(obj), []);
複製程式碼
這是因為 React Hooks 場景下的 Function Component 每次渲染都會重新建立 Store,會導致死迴圈。因此利用 useMemo
並將依賴置為 []
使程式碼在所有渲染週期內,只在初始化執行一次。
更多 Hooks 深入解讀,可以閱讀 精讀《useEffect 完全指南》。
view
根據 Function Component 與 Class Component 的不同,分別進行兩種處理,本文主要介紹對 Function Component 的處理方式,因為筆者推薦使用 Function Component 風格。
首先最外層會套上 memo
,這類似 PureComponent
的效果:
return memo(/**/);
複製程式碼
然後構造一個 forceUpdate
用來強制渲染元件:
const [, forceUpdate] = useState();
複製程式碼
之後,只要利用 observe
包裹元件即可,需要注意兩點:
- 使用剛才建立的
forceUpdate
在store
修改時呼叫。 observe
初始化不要執行,因為初始化元件自己會渲染一次,再渲染一次就會造成浪費。
所以作者通過 scheduler
lazy
兩個引數完成了這兩件事:
const render = useMemo(
() =>
observe(Comp, {
scheduler: () => setState({}),
lazy: true
}),
[]
);
return render;
複製程式碼
最後別忘了在元件銷燬時取消監聽:
useEffect(() => {
return () => unobserve(render);
}, []);
複製程式碼
batch
這也是雙向繫結資料流必須解決的經典問題,批量更新合併。
由於修改物件就觸發渲染,這個過程太自動化了,以至於我們都沒有機會告訴工具,連續的幾次修改能否合併起來只觸發一次渲染。 尤其是 For 迴圈修改變數時,如果不能合併更新,在某些場景下程式碼幾乎是不可用的。
所以 batch
就是為解決這個問題誕生的,讓我們有機會控制合併更新的時機:
import React from "react";
import { view, store, batch } from "react-easy-state";
const user = store({ name: "Bob", age: 30 });
function mutateUser() {
// this makes sure the state changes will cause maximum one re-render,
// no matter where this function is getting invoked from
batch(() => {
user.name = "Ann";
user.age = 32;
});
}
export default view(() => (
<div>
name: {user.name}, age: {user.age}
</div>
));
複製程式碼
react-easy-state
通過 scheduler
模組完成 batch
功能,核心程式碼只有五行:
export function batch(fn, ctx, args) {
let result;
unstable_batchedUpdates(() => (result = fn.apply(ctx, args)));
return result;
}
複製程式碼
利用 unstable_batchedUpdates
,可以保證在其內執行的函式都不會觸發更新,也就是之前建立的 forceUpdate
雖然被呼叫,但是失效了,等回撥執行完畢時再一起批量更新。
同時程式碼裡還對 setTimeout
setInterval
addEventListener
WebSocket
等公共方法進行了 batch
包裝,讓這些回撥函式中自帶 batch
效果。
4. 總結
好了,react-easy-state
神奇的效果解釋完了,希望大家在使用第三方庫的時候都能理解背後的原理。
PS:最後,筆者目前不推薦在 Function Component 模式下使用任何三方資料流庫,因為官方功能已經足夠好用了!
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
special Sponsors
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)