文章首發於github Blog。javascript
本文根據Vue源碼v2.x進行分析。這裏只梳理最源碼中最主要的部分,略過非核心的一些部分。響應式更新主要涉及到Watcher
,Dep
,Observer
這幾個主要類。html
本文主要弄清楚如下幾個容易搞混的問題:vue
Watcher
,Dep
,Observer
這幾個類之間的關係?Dep
中的 subs
存儲的是什麼?Watcher
中的 deps
存儲的是什麼?Dep.target
是什麼,該值是何處賦值的?本文直接重新建Vue實例入手,一步一步揭開Vue的響應式原理,假設有如下簡單的Vue代碼:java
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
watch: {
counter: function(val, oldVal) {
console.log('counter changed...')
}
}
})
複製代碼
從Vue的生命週期可知,首先進行init
初始化操做,這部分代碼在instance/init.js
中。react
src/core/instance/init.js
git
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
github
src/core/instance/state.js
web
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)
}
}
複製代碼
Vue實例爲它的每個data都實現了getter/setter
方法,這是實現響應式的基礎。關於getter/setter
可查看MDN web docs。 簡單來講,就是在取值this.counter
的時候,能夠自定義一些操做,再返回counter的值;在修改值this.counter = 10
的時候,也能夠在設置值的時候自定義一些操做。initData(vm)
的實如今源碼中的instance/state.js
。express
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
類。
首先看看該類的構造方法,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
方法,該方法最重要的有兩處。
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)
}
}
複製代碼
這裏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
類。
根據咱們的例子,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類之間的關聯關係,圖中仍是以咱們的例子進行描述:
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類的源碼。
上圖中關聯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
。
到這裏,主要的三個類都研究完了,如今基本能夠回答文章開頭的幾個問題了。
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實例。
這裏看一個計算屬性的例子:
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)
。更詳細信息請查看源碼。