首發於大搜車技術部落格:blog.souche.com/ru-he-zi-ji…
前言
mobx 是一個非常優雅的狀態管理庫,具有相當大的自由度,並且使用非常簡單,本文通過自己實現一個 mini 版的 mobx 來探究一下類似的 FRP 模式在 js 中的實現。
本文主要講述瞭如何自己實現一個 mobx,主要是其核心幾個 api 的實現。目的不是要重新造一個輪子,只是通過造輪子的過程,瞭解 mobx 的核心原理,以及一些具體實現的時候需要趟的坑,從而對RFP之類的程式設計正規化有更深入的瞭解。
所以,不要將此專案應用於專案中,除非你真的想節省那一點點頻寬(打包後 6K,GZip後 2.3K),用此專案瞭解 mobx 的原理即可。
另外,s-mobx 的實現和 mobx 的實現細節可能並不一致。
關於 s-mobx
github: github.com/xinyu198736…
npm: npm install s-mobx --save
核心組成
s-mobx 最最核心是兩個功能。
- Observable 。用來包裝一個屬性為 被觀察者
- autorun 。用來包裝一個方法為 觀察者
整個 s-mobx 就是圍繞這兩個功能做包裝。
依賴收集
autorun 是個神奇的函式,被他包裝過的方法,就會變為觀察者函式,並且這裡有一個很重要的特性,這個函式只會觀察自己依賴到的設為 observable 的值。
例如
autorun(function(){
console.log(person.name);
});複製程式碼
假設person物件身上有很多個屬性是 observable 的,修改這些屬性值的時候不會觸發 autorun 包裝過的函式,只有修改 name 屬性的時候才會觸發。
這裡的原理就是依賴收集
那如何實現依賴收集呢?
這時候需要引申出一個很簡單的管理類,在 s-mobx 中,我們叫做 dependenceManager,這個工具類中管理了一個依賴的 map,結構是一個全域性唯一的 ID 和 對應的監聽的函式的陣列。
這個全域性唯一的 ID 實際上代表的就是各個被設定為 observable 的屬性值,是 Observable 類的一個屬性 obID。
當一個被 observable 包裝的屬性值發生 set 行為的時候,就會觸發 dependenceManager.trigger(obID); 從而觸發遍歷對應的監聽函式列表,並且執行,這就是 autorun 的基本原理。
那這個依賴的map 是如何收集上來的呢?
其實也很簡單,也是 dependenceManager 的操作,在執行 autorun(handler) 的時候會執行以下的程式碼(實際上也就這三句程式碼):
dependenceManager.beginCollect(handler);
handler();
dependenceManager.endCollect();複製程式碼
這裡 dependenceManager 標記現在開始收集依賴,然後執行 handler 函式,執行結束之後,標記當前收集結束。這裡的收集操作可以巢狀。具體實現見 dependenceManager。
在執行 handler 函式的時候,怎麼知道他依賴了什麼 observable 屬性值的?
這個是通過 observable 的 get 動作來實現的,每個被 observable 過的值在 get 的時候都會判斷當前是否正在收集依賴,如果是的話,就會把這個值 和 當前正在收集依賴的 handler 關聯起來儲存在 dependenceManager 中。
這就是整個 s-mobx 核心的原理。
其他的程式碼大部分只是在實現如何包裝 observable。
Observable
包裝物件值的 Observable ,核心原理是 Object.defineProperty ,給被包裝的屬性套上 get 和 set 的鉤子,在 get 中響應依賴收集,在 set 中觸發監聽函式。
陣列的包裝稍微麻煩,在 s-mobx 中使用 Proxy 來包裝,但是相容性不是很好,在 mobx 中,作者自己模擬了一個陣列物件的操作,然後包裝在原生陣列上。
另外對於 Object 物件,為其進行了遞迴包裝,每一級 Object 都繫結了一個 observable。
具體的程式碼見 s-observable (維護 Observable),s-extendObservable(包裝到具體物件屬性上)
Computed
Computed 是一種特殊的型別,他即是觀察者,也是被觀察者,然後它最大的特性是,他的計算不是每次呼叫的時候發生的,而是在每次依賴的值發生改變的時候計算的,呼叫只是簡單的返回了最後一次的計算結果。
這樣理解就明白了,其實在扮演觀察者的時候, Computed 只是 autorun 的一個變種。
Computed 中有一個方法,叫做 _reComputed,當被 computed 包裝的方法中依賴的 observable 值發生變化的時候,就會觸發 _reComputed 方法重新計算 Computed 的值。這裡的具體實現,其實就是把 _reComputed 當做 autorun 的handler 來處理,執行了一次依賴收集。
另外 Computed 還有一個特性就是可以被別人依賴,所以它也暴露了一個 get 的鉤子,在鉤子裡的操作和 observable 中的 get 鉤子做了同樣的處理。
所以,當用 @computed 包裝一個 class 的方法的時候,將其放入 autorun 中會執行兩次依賴收集,一次是收集 computed 對其他 observable 的依賴,另一次是收集 handler 對當前屬性方法的依賴。這裡 dependenceManager 提供了一種機制,可以巢狀收集依賴,採用了類似堆疊的機制。
observer
在 mobx-react 中,可以使用 @observer 對 react 物件進行包裝,使其 render 方法成為一個觀察者。
在 s-mobx 中直接整合這個功能,實現的程式碼:
var ReactMixin = {
componentWillMount: function() {
autorun(() => {
this.render();
this.forceUpdate();
});
}
};
function observer(target) {
const targetCWM = target.prototype.componentWillMount;
target.prototype.componentWillMount = function() {
targetCWM && targetCWM.call(this);
ReactMixin.componentWillMount.call(this);
};
}複製程式碼
這裡給 react 元件的 prototype 做了一次 mixin,為其加入了一個 autorun,autorun的作用就是繫結元件 render 方法和其依賴的值的觀察關係。當依賴的值發生變化的時候會觸發 autorun 的引數 handler,handler中會強制執行 render() 方法和 forceUpdate()
這裡每次都強制重新渲染,沒有做很好的優化,在mobx中有個方法:isObjectShallowModified 來判斷是否需要強制重新渲染,可以考慮直接引入進來。
decorator
mobx 的最大特色就是簡單的註解使用方式,也就是 @observable @observer @computed 這些 decorator。
decorator 的實現其實很簡單,不過有些坑需要規避,例如 在 decorator 中出現的target,是class 的prototype,而不是class的例項。但是在 return 出來的 descriptor 中,set 和 get 鉤子中的this,則是 class 的例項。在實現一些複雜邏輯的時候要注意一下這個點。
具體的程式碼可以看 s-decorator
function observable(target, name, descriptor) {
var v = descriptor.initializer.call(this);
// 如果值是物件,為其值也建立observable
if(typeof (v) === `object`) {
createObservable(v);
}
var observable = new Observable(v);
return {
enumerable: true,
configurable: true,
get: function() {
return observable.get();
},
set: function(v) {
if(typeof (v) === `object`) {
createObservable(v);
}
return observable.set(v);
}
};
};複製程式碼
小結
至此,一個簡化版的 mobx 基本就完成了,mobx 中常用的功能基本都做了實現。原理的話其實也很簡單,希望以後有人問起來,大家能夠說清楚 mobx 中的一些模式和實現,這就夠了。