首發於大搜車技術博客:blog.souche.com/ru-he-zi-ji…javascript
mobx 是一個很是優雅的狀態管理庫,具備至關大的自由度,而且使用很是簡單,本文經過本身實現一個 mini 版的 mobx 來探究一下相似的 FRP 模式在 js 中的實現。java
本文主要講述瞭如何本身實現一個 mobx,主要是其核心幾個 api 的實現。目的不是要從新造一個輪子,只是經過造輪子的過程,瞭解 mobx 的核心原理,以及一些具體實現的時候須要趟的坑,從而對RFP之類的編程範式有更深刻的瞭解。react
因此,不要將此項目應用於項目中,除非你真的想節省那一點點帶寬(打包後 6K,GZip後 2.3K),用此項目瞭解 mobx 的原理便可。git
另外,s-mobx 的實現和 mobx 的實現細節可能並不一致。github
github: github.com/xinyu198736…npm
npm: npm install s-mobx --save
編程
s-mobx 最最核心是兩個功能。api
整個 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 的基本原理。
其實也很簡單,也是 dependenceManager 的操做,在執行 autorun(handler) 的時候會執行如下的代碼(實際上也就這三句代碼):
dependenceManager.beginCollect(handler);
handler();
dependenceManager.endCollect();複製代碼
這裏 dependenceManager 標記如今開始收集依賴,而後執行 handler 函數,執行結束以後,標記當前收集結束。這裏的收集操做能夠嵌套。具體實現見 dependenceManager。
這個是經過 observable 的 get 動做來實現的,每一個被 observable 過的值在 get 的時候都會判斷當前是否正在收集依賴,若是是的話,就會把這個值 和 當前正在收集依賴的 handler 關聯起來存儲在 dependenceManager 中。
這就是整個 s-mobx 核心的原理。
其餘的代碼大部分只是在實現如何包裝 observable。
包裝對象值的 Observable ,核心原理是 Object.defineProperty ,給被包裝的屬性套上 get 和 set 的鉤子,在 get 中響應依賴收集,在 set 中觸發監聽函數。
數組的包裝稍微麻煩,在 s-mobx 中使用 Proxy 來包裝,可是兼容性不是很好,在 mobx 中,做者本身模擬了一個數組對象的操做,而後包裝在原生數組上。
另外對於 Object 對象,爲其進行了遞歸包裝,每一級 Object 都綁定了一個 observable。
具體的代碼見 s-observable (維護 Observable),s-extendObservable(包裝到具體對象屬性上)
Computed 是一種特殊的類型,他便是觀察者,也是被觀察者,而後它最大的特性是,他的計算不是每次調用的時候發生的,而是在每次依賴的值發生改變的時候計算的,調用只是簡單的返回了最後一次的計算結果。
這樣理解就明白了,其實在扮演觀察者的時候, Computed 只是 autorun 的一個變種。
Computed 中有一個方法,叫作 _reComputed,當被 computed 包裝的方法中依賴的 observable 值發生變化的時候,就會觸發 _reComputed 方法從新計算 Computed 的值。這裏的具體實現,其實就是把 _reComputed 當作 autorun 的handler 來處理,執行了一次依賴收集。
另外 Computed 還有一個特性就是能夠被別人依賴,因此它也暴露了一個 get 的鉤子,在鉤子裏的操做和 observable 中的 get 鉤子作了一樣的處理。
因此,當用 @computed 包裝一個 class 的方法的時候,將其放入 autorun 中會執行兩次依賴收集,一次是收集 computed 對其餘 observable 的依賴,另外一次是收集 handler 對當前屬性方法的依賴。這裏 dependenceManager 提供了一種機制,能夠嵌套收集依賴,採用了相似堆棧的機制。
在 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 來判斷是否須要強制從新渲染,能夠考慮直接引入進來。
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 中的一些模式和實現,這就夠了。