首先咱們此次的源碼分析不只僅是經過源碼分析其實現原理,咱們偶爾還會經過Vue項目編寫的測試用例瞭解更多細節。html
根據官方的指導圖來看,數據(data
)在變動的時候會觸發setter
而引發通知事件(notify
),告知Watcher
數據已經變了,而後Watcher
再出發從新渲染事件(re-render
),會調用組件的渲染函數去從新渲染DOM
。vue
每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。react
其實看完官方介紹的我仍是一臉懵逼,畢竟我更但願知道它的實現細節,因此咱們一步一步的來看,首先是圖中的Data
(紫色)部分。數組
Vue
使用的是MVVM模式,Model
層的改變會更新View-Model
層,那麼它是如何檢測到數據層的改變的呢?瀏覽器
從官方指導文檔-深刻響應式原理中咱們其實已經知道Vue
是使用Object.defineProperty()
實現數據劫持的,而且該屬性沒法經過其餘兼容方法完美的實現,正是由於如此,Vue纔不支持IE8如下的瀏覽器。閉包
好了咱們重頭開始,查看源碼咱們能夠看見順着Vue
對象的實例化過程,其中有個步驟叫作initState(vm)
,這個方法中作的一部分事情就是觀測組件中聲明的data
,它調用了initData(vm)
。async
// instance/state.js
function initData (vm: Component) {
1. 代理data,props,methods到實例上,以便直接用this就能夠調用
2. observe(data, true /* asRootData */)
}
複製代碼
到這裏,終於進入正題。ide
在initData
方法中調用了observe
方法,並將data
做爲參數傳了進去,根據函數名和參數咱們其實能夠猜到,這個方法就是用來觀測數據變化的。那首先咱們從單元測試來看一看observe
有啥須要注意的:函數
// test/unit/modules/observer/observer.spec.js
// it("create on object")
const obj = {
a: {},
b: {}
}
// 也能夠是如下方法建立的
// const obj = Object.create(null)
// obj.a = {}
// obj.b = {}
const ob1 = observe(obj)
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
// should've walked children
expect(obj.a.__ob__ instanceof Observer).toBe(true)
expect(obj.b.__ob__ instanceof Observer).toBe(true)
// should return existing ob on already observed objects
const ob2 = observe(obj)
expect(ob2).toBe(ob1)
複製代碼
// test/unit/modules/observer/observer.spec.js
// it("create on array")
// on array
const arr = [{}, {}]
const ob1 = observe(arr)
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(arr)
expect(arr.__ob__).toBe(ob1)
// should've walked children
expect(arr[0].__ob__ instanceof Observer).toBe(true)
expect(arr[1].__ob__ instanceof Observer).toBe(true)
複製代碼
咱們能夠看到,observe
方法爲obj
和其子對象都綁定了一個Observer
實例,若是是數組的話,則會遍歷數組給數組中的每個對象也綁定一個Observer
實例。實際上就是循環加上遞歸,給每個數組或對象(plainObject
)都綁定一個Observer
實例,而且重複調用observe
方法只會獲得同一實例,也就是單例模式。源碼分析
上面咱們能夠看到observe
方法是響應化data
的一個入口,而它實際上又是經過實例化Observer
類實現的,那麼Observer
實例化的過程當中究竟作了哪些事呢。源碼中,該類的代碼有一段註釋:
Observer class that is attached to each observed object. Once attached, the observer converts the target object's property keys into getter/setters that collect dependencies and dispatch updates.
Observer類會被關聯在每一個被觀測的對象上。一旦關聯上,這個觀測器就會把目標對象上的每一個屬性都轉換爲getter/setter,以便用來收集依賴和分發更新事件。
再來看看源碼:
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
0. 把傳入的value綁定給this.value
1. 新建Dep實例綁定給this.dep
2. 將this綁定在傳入的value的原型屬性"__ob__"上
3. 若是value是數組,遍歷數組對每一個元素調用 observe(數組第i個元素)
4. 不是數組,則對對象的每一個可枚舉的屬性調用 defineReactive
}
}
複製代碼
我總結出了這個方法主要作了這三件事:
1. 將對象標記爲依賴 2. 循環觀測數組元素 3. 響應化對象的每一個可枚舉屬性
接下來咱們重點看看響應化數據這個功能是如何實現的。
話很少說,直接先上源碼歸納:
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
0. 設置閉包實例 const dep = new Dep()
1. 若是 property.configurable === false 直接 return
2. 設置閉包變量 val 的值
3. let childOb = !shallow && observe(val) 觀測該屬性
4. Object.defineProperty(obj, key, {...}) !!!
}
複製代碼
從源碼歸納中我們能夠看到defineReactive
其實主要作了這三件事:
1. 將屬性標記爲依賴
2. 遞歸觀測屬性
3. 數據劫持
而數據劫持這裏,使用到的就是咱們前面提到的Object.defineProperty
!咱們來細品:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = 調用自帶getter或者獲取閉包變量val
if (Dep.target) {
// 依賴收集
}
return value
},
set: function reactiveSetter (newVal) {
調用自帶setter.call(obj, newVal)或者設置閉包變量val = newVal
// 從新觀測新值
childOb = !shallow && observe(newVal)
// 依賴變動通知
dep.notify()
}
})
複製代碼
咱們把注意力放到重點上,一些小細節代碼就沒放上來。
首先,這裏設置了屬性的set
和get
(若是不瞭解的同窗還須要先學習defineProperty)。在set
中,會更新閉包變量val
的值(若是屬性有自帶setter則會調用setter),而且它會調用依賴的通知方法,這個方法會告訴依賴的全部觀測者並調用每一個觀測者的update
方法(咱們稍後再細講),這也就是官網提到的:
當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染
而在get
中,附加功能就只有依賴收集,那爲何把依賴收集放到get
中呢。我們反向思考一下,若是要收集依賴,那麼就要調用屬性的get
也就是獲取屬性值,哪裏會獲取到屬性值呢,固然是模板裏,也就是模板渲染的時候,要把佔位符替(好比{{ msg }}
)換爲實際值,這個時候就會進行依賴收集。而模板沒有用到的屬性,則不會進行依賴收集。官網也有提到:
它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。
OK,看懂了嗎,接下來讓咱們本身來簡單的復現一下以上功能。
首先是observe
方法,注意事項是返回Observer
實例,而且是單例:
function observe(value) {
let ob
if (value.hasOwnProperty("__ob__")) {
ob = value.__ob__
} else if (Array.isArray(value) || ArrisPlainObject(value)) {
ob = new Observer(value)
}
return ob
}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]"
}
複製代碼
其次是Observer
對象,它會標記依賴,綁定觀測實例到數據上,會處理數組,響應化全部屬性:
class Observer {
constructor(value) {
this.value = value
// this.dep = new Dep()
// 掛載實例到value上
const proto = Object.getPrototypeOf(value)
proto.__ob__ = this
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray(value) {
// 觀測數組的每一個元素
value.forEach((item) => {
observe(item)
})
}
walk(value) {
// 響應化對象全部可枚舉屬性
Object.keys(value).forEach((key) => {
defineReactive(value, key)
})
}
}
複製代碼
最後是defineReactive
,它會遞歸觀測屬性,標記依賴,響應化傳入的屬性:
function defineReactive(obj, key, val) {
// 建立閉包依賴標記
// const dep = new Dep()
// 閉包存儲實際值
val = obj[key]
// 遞歸觀測屬性
observe(val)
Object.defineProperty(obj, key, {
set(newVal) {
val = newVal
observe(newVal)
// dep.notify()
},
get() {
// 收集依賴
return val
},
})
}
複製代碼
考慮到咱們尚未了解Dep
,因此相關代碼先忽略。而且咱們實現的是一個最簡版本,沒有考慮到過多的邊緣狀況。
接下來咱們試驗一下:
var data = {
a: 0,
b: {
c: {
d: 1,
},
},
}
observe(data)
複製代碼
咱們再控制檯中打印出data
,發現咱們已經爲data,a,b,c
綁定好了__ob__
。只不過如今它還不能收集依賴以及更新依賴。
我稱其爲依賴標記,由於它會和被觀測的數據進行綁定,也就是說咱們把響應式數據看作是一個依賴,而這個依賴標記會去處理和依賴有關的事情,好比記錄觀測者,分發更新事件等。
這個類其實十分簡單,功能也很明確。咱們先來看看源碼歸納:
class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製代碼
咱們先來看看這明明白白的三個功能:
1. 添加訂閱者addSub
2. 刪除訂閱者removeSub
3. 通知訂閱者更新notify
而且咱們能夠看出依賴裏面存儲的訂閱者是一個Watcher
數組。也就是說實際和Watcher
交互的是Dep
。他還有一個靜態屬性target
該屬性指向的也是一個Watcher
實例。
讓咱們咱們再回過頭來看看Observer
中的相關操做。
export function defineReactive () {
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = 獲取value
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
// ...
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
複製代碼
先講解最簡單的set
,在更新響應式值的時候只調用了該值所屬的dep.notify()
,它會通知全部訂閱者說我這邊數據變了,麻煩你更新同步一下。
而在獲取(「接觸」)該值的時候,調用了值得get
,開始了依賴收集。首先若是有收集者,也就是Dep.target
,那麼該值做爲一個依賴被收入,若是該值是一個數組或者對象,那麼該值被觀測後的Observer
也做爲一個依賴被收入,而且若是是數組的話,會循環收入每一個元素也做爲依賴。總結一下:
若是當前有收集者 Dep.target -- 依賴+1
若是當前值是對象或數組 -- 依賴+1
若是當前值是數組 -- 依賴+n
複製代碼
Observer
特地將和Watcher
相關的代碼抽分出來爲Dep
,目的也是讓整個數據響應過程更加鬆散,可能某天觀測數據變動方法再也不是Observer
的時候,還能繼續進行依賴收集和更新通知。
另外Dep
還提供了兩個靜態方法,用來修改Dep.target
。
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
複製代碼
從這裏咱們能夠看出,Dep.target
是全局惟一的,一個時間點只會有一個Dep.target
。 那具體哪裏會用到它呢?注意到Dep
的定義中,target
的類型是Watcher
,因此咱們須要在瞭解Watcher
以後才能知道它會在何時被設置上。
因爲Dep
的功能主要和Watcher
相關,而且其功能很簡單,因此在咱們掌握Wathcer
以後再來實現它。
接下來咱們來到了最關鍵的一步,它能將我們劫持的數據真正的用於視圖更新,並在視圖更新時同步數據。
以前提了不少Watcher
,咱們從上文知道,它會在某個時間點成爲收集者Dep.target
去收集依賴addDep
,它會在數據變動時響應通知update
。咱們先來看看它的構造函數:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 初始化屬性 deps,newDeps,depIds,newDepIds...
// 設置getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// 獲取監聽的值
this.value = this.get()
}
}
複製代碼
這裏有個參數expOrFn
有點隱晦,它能夠是一個函數或者一個表達式,做爲表達式,它一般是a.b
,msg
這樣的值。你可能有點熟悉了,當咱們在Vue組件中自定義watch的時候,用的也是相似的表達式。
watch: {
msg (val, newVal) {}
}
複製代碼
沒錯,源碼註釋裏有提到,$watch() 和 指令都是用的Watcher
。 而expOrFn
是用來轉換爲獲取組件中的值的getter
。好比expOrFn === 'msg'
,實際上被轉換爲了如下內容:
// 簡單表示
this.getter = function (obj) {
return obj.msg
}
複製代碼
不過這裏的this.value = this.get()
用的卻不是直接的getter
,這是爲何呢? 咱們再來看看get()
方法:
class Watcher {
// ...
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
// 遞歸收集可收集依賴
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
}
複製代碼
這裏用到了咱們前面提到的Dep
的兩個靜態方法pushTarget
,popTarget
。咱們知道這兩個方法是用來設置和取消Dep.target
的,而咱們在Observer
中瞭解到,在獲取屬性值的get
方法中,會根據Dep.target
來蒐集依賴。
而在這裏的watcher.get
方法中,咱們能夠看到,首先添加了當前Watcher
做爲Dep.target
,而後獲取屬性的值觸發屬性的get
方法,調用dep.depend()
讓Wathcer
收集當前依賴。咱們把Observer
中屬性的get
方法中收集依賴摺合一下:
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
const dep = new Dep()
// ...
if (Dep.target) {
// wathcer.addDep(dep)
Dep.target.addDep(dep)
}
// ...
}
}
複製代碼
而這裏的addDep
就是Watcher
的收集依賴的方法:
class Watcher {
constructor () {
// ...
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
}
// ...
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
}
複製代碼
而這個方法主要的做用就是去重而後存儲目標依賴dep
到watcher.newDeps
裏。而後將watcher
存儲到dep.subs
裏,創建一個雙向引用。
而後接下來就是遞歸收集子對象依賴(若是有),而後清除Dep.target
引用,最後調用this.cleanupDeps
,而這個方法作的事也很簡單:
舊依賴列表有而新依賴列表沒有的這些依賴,因爲新依賴中已經沒有了dep -> watcher
的引用,因此對應的也要清除dep <- wathcer
引用,這裏調用了dep.removeSub(this)
,就是告訴你和我撇清關係,個人內心已經沒有你了。
將newDeps
和newDepIds
賦值給deps
和depIds
,而後清空newDeps
和newDepIds
,從新開始生活。
到這裏,Watcher
的初始化就已經完成了。
固然咱們的分析還沒結束,只不過咱們須要短暫的總結一下,消化以前的概念,才能更深入的理解接下來的步驟。
上圖是咱們目前所瞭解到的一個關係圖,黃色的流程表示依賴收集的過程,綠色的表示數據變動的過程。
目前爲止,咱們只是瞭解瞭如何劫持數據,而且在數據變動時更新它的觀測者。好比:
假設咱們已經收集完依賴了,也就是每一個響應化屬性都有一個訂閱列表subs
,這裏面裝着和該屬性相關的觀測者。當屬性出現變動時,因爲數據被劫持(set
),這些觀測者都會獲得通知,調用各自的update
,去執行本身的回調函數。
可是,何時纔會收集依賴呢?咱們寫的{{msg}}
模板表達式怎麼和data.msg
關聯起來的呢?Watcher
是何時實例化的?
咱們下一篇繼續分析。
原文連接:個人博客