文章首發於github Blog。
本文根據Vue原始碼v2.x進行分析。這裡只梳理最原始碼中最主要的部分,略過非核心的一些部分。響應式更新主要涉及到Watcher
,Dep
,Observer
這幾個主要類。
本文主要弄清楚以下幾個容易搞混的問題:
Watcher
,Dep
,Observer
這幾個類之間的關係?Dep
中的subs
儲存的是什麼?Watcher
中的deps
儲存的是什麼?Dep.target
是什麼,該值是何處賦值的?
本文直接從新建Vue例項入手,一步一步揭開Vue的響應式原理,假設有以下簡單的Vue程式碼:
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
watch: {
counter: function(val, oldVal) {
console.log('counter changed...')
}
}
})
複製程式碼
1. Vue例項初始化
從Vue的生命週期可知,首先進行init
初始化操作,這部分程式碼在instance/init.js
中。
src/core/instance/init.js
initLifecycle(vm) // vm生命週期相關變數初始化操作
initEvents(vm) // vm事件相關初始化
initRender(vm) // 模板解析相關初始化
callHook(vm, 'beforeCreate') // 呼叫beforeCreate鉤子函式
initInjections(vm) // resolve injections before data/props
initState(vm) // vm狀態初始化(重點在這裡)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 呼叫created鉤子函式
複製程式碼
上述原始碼中的initState(vm)
是要研究的重點,裡面實現了props
,methods
,data
,computed
,watch
的初始化操作。這裡根據上述例子,重點看data
和watch
,原始碼位置在instance/state.js
src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm) // 對vm的data進行初始化,主要是通過Observer設定對應getter/setter方法
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// 對新增的watch進行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製程式碼
2. initData
Vue例項為它的每一個data都實現了getter/setter
方法,這是實現響應式的基礎。關於getter/setter
可檢視MDN web docs。 簡單來說,就是在取值this.counter
的時候,可以自定義一些操作,再返回counter的值;在修改值this.counter = 10
的時候,也可以在設定值的時候自定義一些操作。initData(vm)
的實現在原始碼中的instance/state.js
。
src/core/instance/state.js
while (i--) {
...
// 這裡將data,props,methods上的資料全部代理到vue例項上
// 使得vm.counter可以直接訪問
}
// 這裡略過上面的程式碼,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)
複製程式碼
這裡observe()
方法將data變成可觀察的,為什麼說是可觀察的?主要是實現了getter/setter
方法,讓Watcher
可以觀察到該資料的變化。下面看看observe
的實現。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // 重點在這裡,響應式的核心所在
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
複製程式碼
這裡只關注new Observer(value)
,這是該方法的核心所在,通過Observer
類將vue的data
變成響應式。 根據我們的例子,此時入參value
的值是{ counter: 1 }
。 下面就具體看看Observer
類。
3. Observer
首先看看該類的構造方法,new Observer(value)
首先執行的是該構造方法。作者的註釋說了,Observer Class將每個目標物件的鍵值(即data中的資料)轉換成getter/setter
形式,用於進行依賴收集和通過依賴通知更新。
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value) // 遍歷data物件中{counter : 1, ..} 中的每個鍵值(如counter),設定其setter/getter方法。
}
}
...
}
複製程式碼
這裡最核心的就是this.walk(value)
方法,this.observeArray(value)
是對陣列資料的處理,實現對應的變異方法,這裡先不考慮。
繼續看walk()
方法,註釋中已說明walk()
做的是遍歷data物件中的每一設定的資料,將其轉為setter/getter
。
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
複製程式碼
那麼最終將對應資料轉為getter/setter
的方法就是defineReactive()
方法。從方法命名上也容易知道該方法是定義為可響應的,結合最開始的例子,這裡呼叫就是defineReactive(...)
如圖所示:
原始碼如下:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// dep 為當前資料的依賴例項
// dep 維護著一個subs列表,儲存依賴與當前資料(此時是當前資料是counter)的觀察者(或者叫訂閱者)。觀察者即是Watcher例項。
const dep = new Dep() ---------------(1)
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
// 定義getter與setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 這裡在獲取值之前先進行依賴收集,如果Dep.target有值的話。
if (Dep.target) { -----------------(2)
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 依賴收集完後返回值
return value
},
...
}
複製程式碼
先看getter
方法,該方法最重要的有兩處。
- 為每個data宣告一個
dep
例項物件,隨後dep
就被對應的data給閉包引用了。舉例來說就是每次對counter
取值或修改時,它的dep例項都可以訪問到,不會消失。 - 根據
Dep.target
來判斷是否收集依賴,還是普通取值。這裡Dep.target
的賦值後面再將,這裡先知道有這麼一回事。
然後再看下setter
方法,原始碼如下:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 這裡對資料的值進行修改
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 最重要的是這一步,即通過dep例項通知觀察者我的資料更新了
dep.notify()
}
複製程式碼
到這裡基本上Vue例項data的初始化就基本結束,通過下圖回顧下initData
的過程:
隨後要進行的是watch
的初始化:
export function initState (vm: Component) {
...
if (opts.data) {
initData(vm) // 對vm的data進行初始化,主要是通過Observer設定對應getter/setter方法
}
// initData(vm) 完成後進行 initWatch(..)
...
// 對新增的watch進行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製程式碼
4. initWatch
這裡initWatch(vm, opts.watch)
對應到我們的例子中如下所示:
initWatch
原始碼如下:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
// handler 是觀察物件的回撥函式
// 如例子中counter的回撥函式
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
複製程式碼
createWatcher(vm, key, handler)
是根據入參構建Watcher
例項資訊,原始碼如下:
function createWatcher (
vm: Component,
keyOrFn: string | Function,
handler: any,
options?: Object
) {
// 判斷是否是物件,是的話提取物件裡面的handler方法
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 判斷handler是否是字串,是的話說明是vm例項上的一個方法
// 通過vm[handler]獲取該方法
// 如 handler='sayHello', 那麼handler = vm.sayHello
if (typeof handler === 'string') {
handler = vm[handler]
}
// 最後呼叫vm原型鏈上的$watch(...)方法建立Watcher例項
return vm.$watch(keyOrFn, handler, options)
}
複製程式碼
$watch
是定義在Vue原型鏈上的方法,原始碼如下:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 建立Watcher例項物件
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
// 該方法返回一個函式的引用,直接呼叫該函式就會呼叫watcher物件的teardown()方法,從它註冊的列表中(subs)刪除自己。
return function unwatchFn () {
watcher.teardown()
}
}
複製程式碼
經過一系列的封裝,這裡終於看到了建立Watcher例項物件了。下面將詳細講解Watcher
類。
5. Watcher
根據我們的例子,new Watcher(...)
如下圖所示:
首先執行Watcher
類的構造方法,原始碼如下所示,省略了部分程式碼:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
this.cb = cb // 儲存傳入的回撥函式
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = [] // 儲存觀察資料當前的dep例項物件
this.newDeps = [] // 儲存觀察資料最新的dep例項物件
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
// 獲取觀察物件的get方法
// 對於計算屬性, expOrFn為函式
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 通過parsePath方法獲取觀察物件expOrFn的get方法
this.getter = parsePath(expOrFn)
...
}
// 最後通過呼叫watcher例項的get()方法,
// 該方法是watcher例項關聯觀察物件的關鍵之處
this.value = this.lazy
? undefined
: this.get()
}
複製程式碼
parsePath(expOrFn)
的具體實現方法如下:
/**
* Parse simple path.
*/
const bailRE = /[^\w.$]/ // 匹配不符合包含下劃線的任意單詞數字組合的字串
export function parsePath (path: string): any {
// 非法字串直接返回
if (bailRE.test(path)) {
return
}
// 舉例子如 'counter'.split('.') --> ['counter']
const segments = path.split('.')
// 這裡返回一個函式給this.getter
// 那麼this.getter.call(vm, vm),這裡vm就是返回函式的入參obj
// 實際上就是呼叫vm例項的資料,如 vm.counter,這樣就觸發了counter的getter方法。
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
複製程式碼
這裡很巧妙的返回了一個方法給this.getter
, 即:
this.getter = function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
複製程式碼
this.getter
將在this.get()
方法內呼叫,用來獲取觀察物件的值,並觸發它的依賴收集,這裡即是獲取counter
的值。
Watcher構造方法的最後一步,呼叫了this.get()
方法,該方法原始碼如下:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 該方法實際上是設定Dep.target = this
// 把Dep.target設定為該Watcher例項
// Dep.target是個全域性變數,一旦設定了在觀察資料中的getter方法就可使用了
pushTarget(this)
let value
const vm = this.vm
try {
// 呼叫觀察資料的getter方法
// 進行依賴收集和取得觀察資料的值
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 此時觀察資料的依賴已經收集完
// 重置Dep.target=null
popTarget()
// 清除舊的deps
this.cleanupDeps()
}
return value
}
複製程式碼
關鍵步驟已經在上面程式碼中註釋了,下面給出一個Observer,Watcher類之間的關聯關係,圖中還是以我們的例子進行描述:
- 紅色箭頭:Watcher類例項化,呼叫watcher例項的
get()
方法,並設定Dep.target
為當前watcher例項,觸發觀察物件的getter
方法。 - 藍色箭頭:
counter
物件的getter
方法被觸發,呼叫dep.depend()
進行依賴收集並返回counter
的值。依賴收集的結果:1.counter
閉包的dep例項的subs
新增觀察它的watcher例項w1;2. w1的deps
中新增觀察物件counter
的閉包dep。 - 橙色箭頭:當
counter
的值變化後,觸發subs
中觀察它的w1執行update()
方法,最後實際上是呼叫w1的回撥函式cb。
Watcher類中的其他相關方法都比較直觀這裡就直接略過了,詳細請看Watcher類的原始碼。
6. Dep
上圖中關聯Observer和Watcher類的是Dep,那麼Dep是什麼呢?
Dep可以比喻為出版社,Watcher好比讀者,Observer好比東野圭吾相關書籍。比如讀者w1對東野圭吾的白夜行(我們例子中的counter)感興趣,讀者w1一旦買了東野圭吾的書,那麼就會自動在這本書的出版社(Dep例項)裡面註冊填w1資訊,一旦該出版社有了東野圭吾這本書最新訊息(比如優惠折扣)就會通知w1。
現在看下Dep的原始碼:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 儲存觀察者watcher例項的陣列
this.subs = []
}
// 新增觀察者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除觀察者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 進行依賴收集
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知觀察者資料有變化
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製程式碼
Dep類比較簡單,對應方法也非常直觀,這裡最主要的就是維護了儲存有觀察者例項watcher的一個陣列subs
。
7. 總結
到這裡,主要的三個類都研究完了,現在基本可以回答文章開頭的幾個問題了。
Q1:Watcher
,Dep
,Observer
這幾個類之間的關係?
A1:Watcher
是觀察者觀察經過Observer
封裝過的資料,Dep
是Watcher
和觀察資料間的紐帶,主要起到依賴收集和通知更新的作用。
Q2:Dep
中的subs
儲存的是什麼?
A2: subs
儲存的是觀察者Watcher例項。
Q3:Watcher
中的deps
儲存的是什麼?
A3:deps
儲存的是觀察資料閉包中的dep
例項。
Q4:Dep.target
是什麼, 該值是何處賦值的?
A4:Dep.target
是全域性變數,儲存當前的watcher例項,在new Watcher()
的時候進行賦值,賦值為當前Watcher例項。
8. 擴充套件
這裡看一個計算屬性的例子:
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
computed: {
result: function() {
return 'The result is :' + this.counter + 1;
}
}
})
複製程式碼
這裡的result
的值是依賴與counter
的值,通過result
更能體現出Vue的響應式計算。計算屬性是通過initComputed(vm, opts.computed)
初始化的,跟隨原始碼追蹤下去會發現,這裡也有Watcher例項的建立:
watchers[key] = new Watcher(
vm, // 當前vue例項
getter || noop, // result對應的方法 function(){ return 'The result is :' + this.counter + 1;}
noop, // noop是定義的一個空方法,這裡沒有回撥函式用noop代替
computedWatcherOptions // { lazy: true }
)
複製程式碼
示意圖如下所示:
這裡計算屬性result
因為依賴於this.counter
,因此設定一個watcher用來觀察result
的值。隨後通過definedComputed(vm, key, userDef)
來定義計算屬性。在計算獲取result
的時候,又會觸發this.counter
的getter
方法,這樣使得result
的值依賴於this.counter
的值。
最後會為計算屬性result
定義它的setter/getter
屬性:Object.defineProperty(target, key, sharedPropertyDefinition)
。更詳細資訊請檢視原始碼。