dynamic-object 只對外暴露了三個 api:observable
observe
Action
,分別是 動態化對象、 變化監聽 與 懶追蹤輔助函數。javascript
下面以開發角度描述實現思路,同時做爲反思,若是有更優的思路,我會隨時更新。java
本庫包含許多抽象概念,爲了簡化描述,使用固定單詞指代,約定以下:react
單詞 | 含義 |
---|---|
observable | dynamic-object 提供的最重要功能,將對象動態化的函數 |
observe | 監聽其回調函數中當前訪問到的 observable 化的對象的修改,並在值變化時從新出發執行 |
observer | 指代 observe 中的回調函數 |
若是單純的實現 observable
,使用 proxy 很簡單,能夠徹底監聽對象的變化,難點在於如何在 observe
中執行依賴追蹤,並當 observable
對象觸發 set
時,觸發對應 observe
中的 observer
。git
每一個 observable
對象觸發 get
時,都將當前所在的 object
+ key
與當前 observer
對應關係存儲起來,當其被 set
時,拿到對應的 observer
執行便可。github
咱們必須依賴持久化變量才能作到這一點,由於 observable
的 set
過程,與 observer
的 get
的過程是分開的。typescript
變量名 | 類型 | 含義 |
---|---|---|
proxies | WeakMap | 全部代理對象都存儲於此,當重複執行 observable 或訪問對象子屬性時,若是已是 proxy 就從 proxies 中取出返回 |
observers | WeakMap | 任何對象的 key 只要被 get ,就會被記錄在這裏,同時記錄當前的 observer ,當任意對象被 set 時,根據此 map 查詢全部綁定的 observer 並執行,就達到 observe 的效果了 |
currentObserver | Observer | 當前的 observer 。當執行 observe 時,當前 observer 指向其第一個回調函數,這樣當代理被訪問時,保證其綁定的 observer 是其當前所在的回調函數。 |
對於 observable(obj)
,按照如下步驟分析:redux
若是傳入的 obj
自己已經是 proxy
,也就是存在於 proxies
,直接返回 proxies.get(obj)
。這種狀況考慮到可能將對象 observable
執行了屢次。(proxies
保存原對象與代理各一份,保證傳入的是已代理的原對象,仍是代理自己,均可以被查找到)api
若是沒有重複,new Proxy
生成代理返做爲返回值。代理涉及到三處監聽處理:get
set
deleteProperty
。緩存
get(target, key, receiver)複製代碼
先判斷 currentObserver
是否爲空,若是爲空,說明是在 observer
以外訪問了對象,此時不作理會。babel
若是 currentObserver
不爲空,將 object
+ key
-> currentObserver
的映射記錄到 observers
對象中。同時爲 currentObserver.observedKeys
添加當前的映射引用,當 unobserve
時,須要讀取 observer.observedKeys
屬性,將 observers
中全部此 observer
的依賴關係刪除。
最後,若是 get
取的值不是對象(typeof obj !== "object"
),那麼是基本類型,直接返回便可。若是是對象,那麼:
proxies
存在,直接返回 proxy
引用。eg: const name = obj.name
,這時 name
變量也是一個代理,其依賴也可追蹤。proxies
不存在,將這個對象從新按照如上流程處理一遍,這就是惰性代理,好比訪問到 a.b.c
,那麼會分別將 a
b
c
各走一遍 get 處理,這樣不管其中哪一環,都是代理對象,可追蹤,相反,若是 a
對象還存在其餘字段,由於沒有被訪問到,因此不會進行處理,其值也不是代理,由於沒有訪問的對象也不必追蹤。set(target, key, value, receiver)複製代碼
若是新值與舊值不一樣,或 key === "length"
時,就認爲產生了變化,找到當前 object
+ key
對應的 observers
隊列依次執行便可。有兩個注意點:
observer
綁定關係清空:由於 observer
時會觸發新一輪綁定,這樣實現了條件的動態綁定。currentObserver
爲當前 observer
,再執行 observer
時就能夠將 set
正確綁定上。刪除屬性時,直接觸發對應 observer
。
這些類型的特色是有明確封裝方法,其實更容易設置追蹤,此次不使用 proxy,而是複寫這些對象的方法,在 get
set
中加上鉤子。
馬上執行當前回調 observer
,執行規則與 4.4 小節的 observers
隊列執行機制相同。
有人會有疑惑,爲何 observe
要當即執行內部回調呢?若是初始化不不輸出,結果可能會好看一些:
import { observable, observe } from "dynamic-object"
const dynamicObj = observable({
a: 1
})
observe(() => {
console.log('a:', dynamicObj.a)
})
dynamicObj.a = 2複製代碼
以上會輸出兩次,分別是 a: 1
和 a: 2
。另外,可能會以爲這樣與 react 結合,會不會致使初始化時增長沒必要要的渲染?
這兩個都是很好的問題,但結論是:初始化執行是必要的:
observer
是什麼(除非作靜態分析,但稍稍複雜些就不可能了)。render
函數,將初始化的 observe
綁定與後續 render
函數分離,達到首次 render 是 observe
初始化觸發,後續 render 依靠依賴追蹤自動觸發 的效果,在 dynamic-react
章節會有深刻介紹。Action
是用於寫標準 action 的裝飾器,有如下兩種寫法:
@Action setUserName() {..}
Action(setUserName)複製代碼
起做用是將回調函數中發生的變動臨時存儲起來,當執行完時統一觸發,而且同一個 observer
的屢次 set
行爲只會觸發一次,而且執行時,獲取到的是最終值,全部值的中間變化過程都會被忽略。
好比: 當 dynamicObj.a
初始值爲 1 時,下面的代碼不會觸發 observer
執行:
Action(()=> {
dynamicObj.a = 2
dynamicObj.a = 1
})複製代碼
要達到上面效果,須要額外定義一個持久化變量 trackingDeep
,每次 Action
執行時,這個變量先自增 1,執行 observer
時,若是 trackingDeep
不爲 0,就把 observer
存儲在隊列中,當回調函數執行完後,深度減 1,開始執行存儲的隊列,一樣,若是深度不爲 1 就跳過,深度爲 0 就執行。
咱們假象這種場景:
class Test {
@Action setUser(info) {
this.userStore.account = info.account
this.setName(info.name)
}
@Action setName(name) {
this.userStore.name = name
}
}複製代碼
當調用 setUser
時,其內部又調用了 setName
,那麼執行 setUser
時,trackingDeep
爲 1,以後又執行到 setName
使得 trackingDeep
變成 2,內層 Action
執行完畢,trackingDeep
變回 1,此時隊列不會執行,調用棧回退到 setName
後,trackingDeep
終於變成 0,隊列執行,此時observer
僅觸發了一次。
Tips: 這裏有個優化點,當
trackingDeep
不爲 0 時,終止dynamic-object
的依賴收集行爲。這麼作的好處是,當 react render 函數中,同步調用 action 時,不會綁定到這個 action 用到的變量。
Action
的概念存在一個嚴重的缺點(但不致命),同時也是 mobx
庫一直沒有解決的問題,那就是對於異步 action 迫不得已(除非爲異步 action 分段使用 Action
,這也是 mobx 官方推薦的方式,也有 babel 插件來解決,但這樣很 hack)。
咱們思考以下代碼:
class Test {
@Action async getUser() {
this.isLoading = true
const result = await fetch()
this.isLoading = false
this.user = result
}
}複製代碼
首先咱們不但願它是忽略中間態的,不然初始將 isLoading
設置爲 true 就沒有意義了。
比較好的途徑是,將這個異步 action 觸發的 observer
塞入到隊列中,每當遇到 await
就執行並清空隊列,同時還能夠支持 timeout
設定,好比設置爲 100ms 時,若是 fetch 函數在 100ms 內執行完畢,就不會執行以前的隊列,達到肉眼沒法識別的間隔內不觸發 loading 的效果。
理想很美好,惋惜難點不在如何實現如上的設定,而是咱們沒辦法將隊列分隔開,考慮以下代碼:
handleClick() {
this.props.Test.getUser()
this.props.Test.getArticle()
}複製代碼
getUser
與 getArticle
都是異步的,若是咱們將緩存隊列共用一個,那麼 getArticle
執行到 await
時,順便會邪惡的把 getUser
隊列中 observer
給執行了,縱使 getUser
的 await
尚未結束(可能出現 loading 在數據還沒加載完成就消失)。
有人說,將 getUser
與 getArticle
隊列分開不就好了嗎?是的,但目前 javascript 還作不到這一點,見此處討論。不管是 defineProperty
仍是 proxy
,都沒法在 set
觸發時,知道本身是從哪一個閉包中被觸發的。只知道觸發的對象,以及被訪問的 key,是沒辦法將 getUser
getArticle
放在不一樣隊列執行 observer
的。
目前個人作法與 mobx 同樣,async
函數會打破 Action
的庇護,失去了收集後統一執行的特性,但保證了程序的正確運行。目前的解決方法是,爲同步區域再套一層 Action
,或者乾脆將異步與同步分開寫!
說實話,這個問題被 redux 用概念巧妙規避了,咱們必須將這個函數拆成兩個 dispatch。回頭想一想,若是咱們也這麼作,也徹底能夠規避這個問題,拆成兩個 action 便可!但我但願有一天,能找到完美的解決方法。
另外但願表達一點,redux 的成功在於定義了許多概念與規則,只要咱們遵照,就能寫出維護性很棒的代碼,其實 oo 思想也是同樣!咱們在使用 oo 時,將對 fp 的耐心拿出來,同樣能寫出維護性很棒的代碼。
dynamic-react 是 dynamic-object 在 react 上的應用,相似於 mobx-react 相比於 mobx。實現思路與 mobx-react 很接近,可是簡化了許多。
dynamic-react 只暴露了兩個接口 Provider
與 Connect
,分別用於 數據初始化 與 綁定更新與依賴注入
Provider 將接收到的全部參數全局透傳到組件,所以實現很簡單,將接收到的全部字段存在 context 中便可。
這個裝飾器用於 react 組件,分別提供了綁定更新與依賴注入的功能。
因爲 dynamic-react 是與 dynamic-object 結合使用的,所以會將全量 store 數據注入到 react 組件中,因爲依賴追蹤的特性,不會形成沒必要要的渲染。
注入經過高階組件方式,從 context 中取出 Provider 階段注入的值,直接灌給自組件便可,注意組件自身的 props 須要覆蓋注入數據:
export default function Connect(componentClass: any): any {
return class InjectWrapper extends React.Component<any, any>{
// 取 context
static contextTypes = {
dyStores: React.PropTypes.object
}
render() {
return React.createElement(componentClass, {
...this.context.dyStores,
...this.props,
})
}
}
}複製代碼
見如上代碼,咱們經過拿到當前子組件的實例:componentClass.prototype || componentClass
將其生命週期函數重寫爲,先執行自定義函數鉤子,再執行其自身,並且自定義函數鉤子綁定上當前 this
,能夠在自定義勾子修改當前實例的任意字段,後續重寫 render 也是依賴此實現的。
最重要階段是在 willMount 生命週期完成的,由於對於 observer
來講,只要在初始化時綁定了引用,以後更新都是從 observe
中自動觸發的。
總體思路是複寫 render 方法:
observe
包裹住原始 render 方法執行,所以綁定了依賴,將此時 render 結果直接返回便可。observe
自動觸發的(或者 state、props 傳參變化,這些無論),此時能夠肯定是由數據流變更致使的刷新,所以能夠調用 componentWillReact
生命週期。而後調用 forceUpdate
生命週期,由於重寫了 render 的緣故,視圖不會自動刷新。注意第一次調用時,不管如何會觸發一次
observer
,爲了忽略這次渲染,咱們設置一個是否渲染的 flag,當 observer 渲染了,普通 render 就再也不執行,由此避免observe
初始化一定執行一次帶來初始渲染兩次的問題。
在 componentWillUnmount
時 unobserve
掉當前組件的依賴追蹤,給 shouldComponentUpdate
加上 pureRender,以及在 componentDidMount
與 componentDidUpdate
時通知 devTools 刷新,這裏與 mobx-react 實現思路徹底一致。
最後給出 dynamic-object 的項目地址,歡迎提出建議和把玩。