資料觀察系統是Vue實現資料繫結、非同步更新的核心模組,資料觀察系統的實現也是Vue原始碼裡最為複雜的部分,在仔細研究具體實現之前,先對整個資料繫結的邏輯架構進行一個充分的認識,會更有助於解讀原始碼。
先說明一下,因為三個類的名稱比較容易讓人誤解,所以在以後把Observer稱作觀察目標,Watcher稱作監視器,Dep稱作依賴物件。
資料繫結邏輯架構
Vue的資料觀察系統是基於釋出者/訂閱者模式,資料更新觸發重新整理頁面的過程主要依賴資料觀察系統裡鐵三角關係。在這個系統中,主要角色分別是 Observer
、Dep
、Watcher
這三個物件,對於每一個角色在觀察資料更新的流程中各自承擔的職責需要深入進行理解。下面請出三個主角登場,來介紹一下它們:
Observer
Observer
相當於觀察目標類,在資料繫結邏輯架構中的職責是收集需要觀察的資料物件,進行變數存取器的包裝,並遞迴地對每一個需要觀察的物件註冊釋出者物件,再由釋出者去註冊相應的監視器。這裡非常巧妙的是觸發通知監視器資料更新的事件的註冊,一般的釋出訂閱模式需要建立一個事件管理器或者排程中心來統一管理各種事件的註冊,然而Vue的資料繫結不需要這樣的機制,它借用 Object.defineProperty
方法來為每一個被監視的資料設定了存取器,依靠資料的存取行為自然地實現了事件的觸發。在初始化Vue例項中設定的 data
屬性時,對這些輸入的資料物件對行了依賴追蹤,包裝後的變數存放在 _data
屬性中,這個過程中釋出者和監視器的依賴新增是不可見的;而通過配置 watch
屬性顯式設定的監視器,就可以在例項的 _watchers
私有屬性中檢視到。每個元件初始化後有一個唯一的 _watcher
物件,它是一個用來監視在 data
中註冊的資料變動從而更新檢視的監視器,它也預設被新增到了各屬性的依賴監視陣列中。在每個修改為可觀察狀態的屬性中,都含有一個 Dep
例項即釋出者,這個物件的 subs
屬性就是用來存放依賴的所有監視器 Watcher
例項物件,subs
可以理解為訂閱者,即所有訂閱了該資料物件變動的監視器的陣列集合。之所以需要在一開始為資料收集依賴,參考另一些開發者的總結是由於並非所有的資料都值得監視,要知道監視沒有用到的資料就是對效能的浪費,在例項觀察中也確實發現,頁面中沒有用到的屬性,沒有被初始化為依賴項,這樣即便改變了它的數值,頁面也不會觸發多餘的重新整理。
Dep
Dep
在Vue的資料觀察者系統裡充當釋出者的角色,它不僅用來觸發資料更新和建立依賴的事件,還用來存放每一個可監視資料所依賴的監視器,這個正是在第一步收集依賴時的重要一環。例項初始化的過程中收集了所有需要跟蹤變化的資料,在運用 Observer
重新包裝每一個屬性的同時,建立了各自的 dep
物件,並在get和set方法中分別使用了 Dep
的兩個方法:depend
建立依賴,notify
通知變動。另外 Dep
還負責維護依賴監視器的增減。在構造 Dep
類的過程中,定義了全域性的 Dep.target
物件和 targetStack
陣列,targetStack
陣列是用來存放待執行的 watcher
棧,Dep.target
是用來指代當前的監視器,必須唯一,它的存在對於建立監視器的依賴起到重要作用,在重置資料的 getter
時,當它存在時才執行建立資料與監視器的依賴,即只有顯式配置了 watch
或建立了 computed
變數時才會在例項的私有屬性裡看到監視器。
Watcher
Watcher
是這個架構中的監視器,充當觀察者的角色。在Vue例項初始化的過程中,一定會預設建立一個監視器,這個監視器就是用來監視例項物件的資料變化用來更新檢視的,例項的私有屬性 _watcher
用來存放它。在建立可觀察的資料時,每一個資料的 Dep
物件會收集監視器並建立依賴,當資料變化時,Dep
物件通知所有的監視器執行更新,執行更新有兩種模式,如果依賴是通過配置 computed
變數建立的,則會立即觸發相關的更新操作,如果資料的 dep.subs
陣列中沒有依賴的監視器,則預設惰性更新模式。Watcher
類最主要的作用是通知檢視更新,眾所周知檢視的更新是非常花費時間,會影響程式效能,為了儘量減少檢視更新導致的效能損失,在通知檢視執行更新操作之前會有一個緩衝時段,在這個時段中會收集最後一次監視器收到的變更,減少不必要的重複更新,實現最優效能。
架構圖示
充分了解了資料觀察系統的三個主角之後,再來看看官網貼出的示意圖,就會發現終於能摸清Vue的資料觀察系統的架構了,只不過渲染檢視的具體實現與資料觀察系統的互動暫時還沒有去摸索,以後會仔細地去探索,現在終於比較清晰地弄懂了Vue的資料繫結的原理了。
一個簡單的例項
為了更清晰初步瞭解資料繫結相關的初始化過程,建立了一個非常簡單的例項,data配置了兩個屬性,其中 name
變數並不在頁面中使用,還顯式設定了一個依賴 msg
的監視器。
new Vue({
data () {
return {
msg: 'hello',
name: ''
}
},
watch: {
'msg' (value) {
console.log('msg更新了')
}
}
})
複製程式碼
下面截圖是例項的相關監視器私有屬性,_watcher
是跟蹤頁面渲染的監視器,每個例項唯一;_wacthers
是例項所擁有的所有監視器的集合。顯式設定的 watcher
在是陣列中的第一個物件。這裡雖然看不到 Observer
背後的包裝過程,但改變了 msg
屬性之後,可以看到監視器執行的回撥顯示。
從Vue物件例項化著手到開始分析資料繫結的核心實現,這一路過來還沒有真正遇到值得困擾的問題。但未曾想到的是,資料繫結這個Vue的核心特色功能竟然讓我苦苦研讀了好幾天,似乎以前對於設計模式的瞭解顯得那樣無力。期間去搜尋了一些前人做的分析說明文章以求從各個角度深入理解,但大多數解讀讀完後依然覺得沒能很透徹地理解這個模組,後來讀到了一個簡易實現Vue觀察者系統的文章,讓我忽然對核心邏輯是如何實現的有了比較清晰的認識,而且對於設計模式也有了更深入的理解。也許第一次讀原始碼的時候太多非核心的技術實現干擾了對於核心部分的理解,也因為之前的一些知識不牢固,所以從這一次學習中得到了一個很好的經驗,要更加關注本質。