本文主要講解了下我平時在工做開發中遇到的關於 Hooks 的一些缺點和問題,並嘗試配合 Mobx 解決這些問題的經歷。我以爲二者的配合能夠極大的下降開發過程當中有可能出現的問題以及極大的提升開發體驗,並且學習成本也是很是的低。若是你對 Hooks 以及 Mobx 有興趣,想知道更進一步的瞭解,那麼這篇文章適合你。這篇文章會介紹以下內容,方便你決定是否要仔細閱讀,節省時間:html
另外 Hooks 自己真的就是一個理解上很是簡單的東西,因此本文也不長,我也不喜歡去寫什麼萬字長文,又不是寫教程,並且讀者看着標題就失去興趣了。前端
首先,在這裏我再也不說 Hooks 的優勢,由於他的優勢用過的人都清楚是怎麼回事,這裏主要講解一下他存在的缺點,以及如何用 Mobx 來進行改進。node
換句話說,形成這種緣由主要是由於 Hooks 每次都會建立一個全新的閉包,而閉包內全部的變量其實都是全新的。而每次都會建立閉包數據,而從性能角度來說,此時緩存就是必要的了。而緩存又會牽扯出一堆問題。react
說到底,也就是說沒有一個公共的空間來共享數據,這個在 Class 組件中,就是 this,在 Vue3 中,那就是 setup 做用域。而 Hooks 中,除非你願意寫 useRef
+ ref.current
不然是沒有辦法找到共享做用域。設計模式
而 mobx 和 Hooks 的結合,能夠很方便在 Hooks 下提供一個統一的做用域來解決上面遇到的問題,所謂雙劍合併,劍走天下。api
在傳統的使用 mobx 的過程當中,你們應該都知道 observer
這個 api,對須要可以響應式的組件用這個包裹一下。一樣,這個 api 直接在 hooks 中依舊能夠正常使用。 可是 hooks 並不推薦 hoc 的方式。天然,mobx 也提供了 hookify 的使用方式,那就是 useObserver
。緩存
const store = observable({})
function App() {
return useObserver(() => {
return <div>{store.count}</div>
})
}
複製代碼
看到這裏,相信使用過 mobx 的應該能夠發現,useObserver 的使用幾乎和 Class 組件的 render 函數的使用方式一致。事實上也確實如此,並且他的使用規則也很簡單,直接把須要返回的 Node 用該 hooks 包裹後再返回就能夠了。閉包
通過這樣處理的組件,就能夠成功監聽數據的變化,當數據變化的時候,會觸發組件的重渲染。至此,第一個 api 就瞭解完畢了框架
簡單來說,就是在 Hooks 的環境下封裝的一個更加方便的 observable。就是給他一個函數,該函數返回一個須要響應式的對象。能夠簡單的這樣理解異步
const store = useLocalStore(() => ({key: 'value'}))
// equal
const [store] = useState(() => obserable({key: 'value'}))
複製代碼
而後就沒有了,極其簡單的一個 api 使用。然後面要講的一些最佳實踐更多的也是圍繞這個展開,後文簡化使用 local store 代指。
簡單來說,就是在保留 Hooks 的特性的狀況下,解決上面 hooks 所帶來的問題。
第一點,因爲 local store 的存在,做爲一個不變的對象存儲數據,咱們就能夠保證不一樣時刻對同一個函數的引用保持不變,不一樣時刻都能引用到同一個對象或者數據。再也不須要手動添加相關的 deps。由此能夠避免 useCallback 和 useRef 的過分使用,也避免不少 hooks 所面臨的的閉包的坑(老手請自動忽略)。依賴傳遞性和緩存雪崩的問題均可以獲得解決
直接上代碼,主要關注註釋部分
// 要實現一個方法,只有當鼠標移動超過多少像素以後,纔會觸發組件的更新
// props.size 控制移動多少像素才觸發回調
function MouseEventListener(props) {
const [pos, setPos] = useState({x: 0, y: 0})
const posRef = useRef()
const propsRef = useRef()
// 這裏須要用 Ref 存儲最新的值,保證回調裏面用到的必定是最新的值
posRef.current = pos
propsRef.current = propsRef
useEffect(() => {
const handler = (e) => {
const newPos = {x: e.xxx, y: e.xxx}
const oldPos = posRef.current
const size = propsRef.current.size
if (
Math.abs(newPos.x - oldPos.x) >= size
|| Math.abs(newPos.y - oldPos.y) >= size
) {
setPos(newPos)
}
}
// 當組件掛載的時候,註冊這個事件
document.addEventListener('mousemove', handler)
return () => document.removeEventListener('mousemove', handler)
// 固然這裏也能夠監聽 [pos.x, pos.y],可是性能很差
}, [])
return (
props.children(pos.x, pos.y)
)
}
// 用 mobx 改寫以後,這種使用方式遠比原生 hooks 更加符合直覺。
// 不會有任何 ref,任何 current 的使用,任何依賴的變化
function MouseEventListenerMobx(props) {
const state = useLocalStore(target => ({
x: 0,
y: 0,
handler(e) {
const nx = e.xxx
const ny = e.xxx
if (
Math.abs(nx - state.x) >= target.size ||
Math.abs(ny - state.y) >= target.size
) {
state.x = nx
state.y = ny
}
}
}), props)
useEffect(() => {
document.addEventListener('mousemove', state.handler)
return () => document.removeEventListener('mousemove', state.handler)
}, [])
return useObserver(() => props.children(state.x, state.y))
}
複製代碼
第二,就是針對異步數據的批量更新問題,mobx 的 action 能夠很好的解決這個問題
// 組件掛載以後,拉取數據並從新渲染。不考慮報錯的狀況
function AppWithHooks() {
const [data, setData] = useState({})
const [loading, setLoading] = useState(true)
useEffect(async () => {
const data = await fetchData()
// 因爲在異步回調中,沒法觸發批量更新,因此會致使 setData 更新一次,setLoading 更新一次
setData(data)
setLoading(false)
}, [])
return (/* ui */)
}
function AppWithMobx() {
const store = useLocalStore(() => ({
data: {},
loading: true,
}))
useEffect(async () => {
const data = await fetchData()
runInAction(() => {
// 這裏藉助 mobx 的 action,能夠很好的作到批量更新,此時組件只會更新一次
store.data = data
store.loading = false
})
}, [])
return useObserver(() => (/* ui */))
}
複製代碼
不過也有人會說,這種狀況下用 useReducer
不就行了麼?確實,針對這個例子是能夠的,可是每每業務中會出現不少複雜狀況,好比你在異步回調中要更新本地 store 以及全局 store,那麼就算是 useReducer
也要分別調用兩次 dispatch ,一樣會觸發兩次渲染。而 mobx 的 action 就不會出現這樣的問題。// 若是你強行 ReactDOM.unstable_batchedUpdates
我就不說啥了,勇士受我一拜
知道了上面的兩個 api,就能夠開始愉快的使用起來了,只不過這裏給你們一下小 tips,幫助你們更好的理解、更好的使用這兩個 api。(不想用並且也不敢用「最佳實踐」這個詞,感受太絕對,這裏面有一些我本身也沒有打磨好,只能算是 tips 來幫助你們拓展思路了)
對於 store 內的函數要獲取 store 的數據,一般咱們會使用 this 獲取。好比
const store = useLocalStore(() => ({
count: 0,
add() {
this.add++
}
}))
const { add } = store
add() // boom
複製代碼
這種方式通常狀況下使用徹底沒有問題,可是 this 依賴 caller,並且沒法很好的使用解構語法,因此這裏並不推薦使用 this,而是採用一種 no this
的準則。直接引用自身的變量名
const store = useLocalStore(() => ({
count: 0,
add() {
store.count++
}
}))
const { add } = store
add() // correct,不會致使 this 錯誤
複製代碼
在某些狀況下,咱們的 local store 可能須要獲取 props 上的一些數據,而經過 source 能夠很方便的把 props 也轉換成 observable 的對象。
function App(props) {
const store = useLocalStore(source => ({
doSomething() {
// source 這裏是響應式的,當外界 props 發生變化的時候,target 也會發生變化
if (source.count) {}
// 若是這裏直接用 props,因爲閉包的特性,這裏的 props 並不會發生任何變化
// 而 props 每次都是不一樣的對象,而 source 每次都是同一個對象引用
// if (props.count) {}
}
// 經過第二個參數,就能夠完成這樣的功能
}), props)
// return Node
}
複製代碼
固然,這裏不只僅能夠用於轉換 props,能夠將不少非 observable 的數據轉化成 observable 的,最多見的好比 Context、State 之類,好比
const context = useContext(SomeContext)
const [count, setCount] = useState(0)
const store = useLocalStore(source => ({
getCount() {
return source.count * context.multi
}
}), {...props, ...context, count})
複製代碼
有的時候,默認的 observable 的策略可能會有一些性能問題,好比爲了避免但願針對一些大對象所有響應式。能夠經過返回自定義的 observable 來實現。
const store = useLocalStore(() => observable({
hugeObject: {},
hugeArray: [],
}, {
hugeObject: observable.ref,
hugeArray: observable.shallow,
}))
複製代碼
甚至你以爲自定義程度不夠的話,能夠直接返回一個自定義的 store
const store = useLocalStore(() => new ComponentStore())
複製代碼
默認的使用方式下,最方便高效的類型定義就是經過實例推導,而不是經過泛型。這種方式既能兼顧開發效率也能兼顧代碼可讀性和可維護性。固然了,你想用泛型也是能夠的啦
// 使用這種方式,直接經過對象字面量推導出類型
const store = useLocalStore(() => ({
todos: [] as Todo[],
}))
// 固然你能夠經過泛型定義,只要你不以爲煩就行
const store = useLocalStore<{
todos: Todo[]
}>(() => ({todos: []}))
複製代碼
可是這個僅僅建議用做 local store 的時候,也就是相關的數據是在本組件內使用。若是自定義 Hooks 話,建議仍是使用預約義類型而後泛型的方式,能夠提供更好的靈活性。
當使用 useObserver api 以後,就意味着失去了 observer 裝飾器默認支持的淺比較 props 跳過渲染的能力了,而此時須要咱們本身手動配合 memo 來作這部分的優化
另外,memo 的性能遠比 observer 的性能要高,由於 memo 並非一個簡單的 hoc
export default memo(function App(){
const xxx = useLocalStore(() => ({}))
return useObserver(() => {
return (<div/>)
})
})
複製代碼
上面的這幾個 Hooks 均可以經過 useLocalStore 代替,內置 Hooks 對 Mobx 來講是毫無必要。並且這幾個內置 api 的使用也會致使緩存的問題,建議作以下遷移
常用 useEffect 知道他有一個功能就是監聽依賴變化的能力,換句話說就是能夠當作 watcher 使用,而 mobx 也有本身的監聽變化的能力,那就是 reaction,那麼究竟使用哪一種方式更好呢?
這邊推薦的是,兩個都用,哈哈哈,沒想到吧。
useEffect(() =>
reaction(() => store.count, () => console.log('changed'))
, [])
複製代碼
說正經的,針對非響應式的數據使用 useEffect,而響應式數據優先使用 reaction。固然若是你全程拋棄原生 hooks,那麼只用 reaction 也能夠的。
邏輯拆分和組合,是 Hooks 很大的一個優點,在 mobx 加持的時候,這個有點依舊能夠保持。甚至在還更加簡單。
function useCustomHooks() {
// 推薦使用全局 Store 的規則來約束自定義 Hooks
const store = useLocalStore(() => ({
count: 0,
setCount(count) {
store.count = count
}
}))
return store
}
function App() {
// 此時這個 store 你能夠從兩個角度來思考
// 第一,他是一個 local store,也就是每個都會初始化一個新的
// 第二,他能夠做爲全局 store 的 local 化,也就是你能夠將它按照全局 store 的方式來使用
const store = useCustomHook()
return (
// ui
)
}
複製代碼
Mobx 自己就提供了做爲全局 Store 的能力,這裏只說一下和 Hooks 配合的使用姿式
當升級到 mobx-react@6 以後,正式開始支持 hooks,也就是你能夠簡單的經過這種方式來使用
export function App() {
return (
<Provider sa={saStore} sb={sbStore}> <Todo/> </Provider>
)
}
export function Todo() {
const {sa, sb} = useContext(MobxProviderContext)
return (
<div>{sa.foo} {sb.bar}</div>
)
}
複製代碼
這句話怎麼理解數據共享和組件通信呢?舉個例子
曾經關注過 Hooks 的發展,發現不少人在 Hooks 誕生的時候開始嘗試用 Context + useReducer 來替換掉 Redux,我以爲這是對 Context 的某種曲解。
緣由就是 Context 的更新問題,若是做爲全局 Store,那麼必定要在根組件上掛載,而 Context 檢查是否發生變化是經過直接比較引用,那麼就會形成任意一個組件發生了變化,都會致使從 Provider 開始的整個組件樹發生從新渲染的狀況。
function App() {
const [state, dispatch] = useReducer(reducer, init)
return (
// 每次當子組件調用 dispatch 以後,會致使 state 發生變化,從而致使 Provider 的 value 變化
// 進而讓全部的子組件觸發刷新
<GlobalContext.Provider value={{...state, dispatch}}>
{/* child node */}
</GlobalContext.Provider>
)
}
複製代碼
而若是你想避免這些問題,那就要再度封裝一層,這和直接使用 Redux 也就沒啥區別了。
主要是 Context 的更新是一個性能消耗比較大的操做,當 Provider 檢測到變化的時候,會遍歷整顆 Fiber 樹,比較檢查每個 Consumer 是否要更新。
專業的事情交給專業的來作,使用 Redux Mobx 能夠很好的避免這個問題的出現。
知道 Redux 的應該清楚他是如何定義一個 Store 吧,官方其實已經給出了比較好的最佳實踐,但在生產環境中,使用起來依舊不少問題和麻煩的地方。因而就誕生了不少基於 Redux 二次封裝的庫,基本都自稱簡化了相關的 API 的使用和概念,可是這些庫其實大大增長了複雜性,引入了什麼 namespace/modal 啥的,我也記不清了,反正看到這些就自動勸退了,不喜歡在已經很麻煩的東西上爲了簡化而作的更加麻煩。
而 Mobx 這邊,官方也有了一個很好的最佳實踐。我以爲是頗有道理,並且是很是易懂易理解的。
但仍是那個問題,官方在有些地方仍是沒有進行太多的約束,而在開發中也遇到了相似的問題,因此這裏在基於官方的框架下有幾點意見和建議:
是的,基本上這樣就能夠寫好一個 Store 了,沒有什麼花裏胡哨的概念,也沒有什麼亂七八糟的工具,約定俗成就足以。我向來推崇沒有規則就是最大的規則,沒有約束就是最大的約束。不少東西能約定俗成就約定俗成,落到紙面上就足夠了。徹底不必作一堆 lint/tools/library 去約束,既增長了前期開發成本,又增長了後期維護成本,就問問你司內部有多少 dead 的工具和庫?
俗話說的話,「秦人不暇自哀然後人哀之,後人哀之而不鑑之,亦使後人而復哀後人也」,這就是現狀(一巴掌打醒)
不過以上的前提是要求大家的開發團隊有足夠的開發能力,不然新手不少或者同步約定成本高的話,搞個庫去約束到也不是不行(滑稽臉)
說了這麼多,也不是說是萬能的,有這個幾個缺點
useObserver
方法。Mobx 在我司的項目中已經使用了好久了,但 Hooks 也是剛剛使用沒多久,但願這個能給你們幫助。也歡迎你們把遇到的問題一塊兒說出來,你們一塊兒找解決辦法。
我始終以爲基於 Mutable 的開發方式永遠是易於理解、上手難度最低的方式,而 Immutable 的開發方式是易維護、比較穩定的方式。這二者不必非此即彼,而 Mobx + React 能夠認爲很好的將二者整合在一塊兒,在須要性能的地方能夠採用 Immutable 的方式,而在不須要性能的地方,能夠用 Mutable 的方式快速開發。
固然了,你就算不用 Mobx 也徹底沒有問題,畢竟原生的 Hooks 的坑踩多了以後,習慣了也沒啥問題,一些小項目,我也會只用原生 Hooks 的(防槓聲明)。