VueJS 響應式原理及簡單實現
Vue 的響應式模型指的是:
vm.$watch() watch
上面的三種方式追根揭底,都是通過回撥的方式去更新檢視或者通知觀察者更新資料
Vue的響應式原理是基於觀察者模式和JS的API: Object.defineProperty()
和 Proxy
物件
主要物件
每一個被觀察的物件對應一個Observer例項,一個Observer例項對應一個Dep例項,Dep和Watcher是多對多的關係,附上官方的圖,有助於理解:

1. Observer
一個被觀察的物件會對應一個Observer例項,包括 options.data
。
一個Observer例項會包含被觀察的物件和一個Dep例項。
export class Observer { value: any; dep: Dep; vmCount: number; } 複製程式碼
2. Dep
Dep例項的作用是收集被觀察物件(值)的訂閱者。
一個Observer例項對應一個Dep例項,該Dep例項的作用會在 Vue.prototype.$set
和 Vue.prototype.$del
中體現——通知觀察者。
一個Observer例項的每一個屬性也會對應一個Dep例項,它們的getter都會用這個Dep例項收集依賴,然後在被觀察的物件的屬性發生變化的時候,通過Dep例項通知觀察者。
options.data
就是一個被觀察的物件,Vue會遍歷 options.data
裡的每一個屬性,如果屬性也是物件的話,它也會被設計成被觀察的物件。
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; } 複製程式碼
3. Watcher
一個Watcher對應一個觀察者,監聽被觀察物件(值)的變化。
Watcher會維護一個被觀察者的舊值,並在被通知更新的時候,會呼叫自身的 this.getter()
去獲取最新的值並作為要不要執行回撥的依據。
Watcher分為兩類:
-
檢視更新回撥,在資料更新(setter)的時候,watcher會執行
this.getter()
——這裡Vue把this.getter()
作為檢視更新回撥(也就是重新計算得到新的vnode)。 -
普通回撥,在資料更新(setter)的時候,會通知Watcher再次呼叫
this.getter()
獲取新值,如果新舊值對比後需要更新的話,會把新值和舊值傳遞給回撥。
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; } 複製程式碼
使 options.data
成為響應式物件的過程
Vue使用 initData()
初始化 options.data
,並在其中呼叫了 observe
方法,接著:
- 原始碼中的
observe
方法是過濾掉不是物件或陣列的其它資料型別,言外之意Vue僅支援物件或陣列的響應式設計,當然了這也是語言的限制,因為Vue使用API:Object.defineProperty()
來設計響應式的。 - 通過
observe
方法過濾後,把傳入的value再次傳入new Observer(value)
- 在Observer建構函式中,把Observer例項連線到value的屬性
__ob__
;如果value是陣列的話,需要修改原型上的一些變異方法,比如push、pop
,然後呼叫observeArray
遍歷每個元素並對它們再次使用observe
方法;如果value是普通物件的話,對它使用walk
方法,在walk
方法裡對每個可遍歷屬性使用defineReactive
方法 - 在
defineReactive
方法裡,需要建立Dep的例項,作用是為了收集Watcher例項(觀察者),然後判斷該屬性的property.configurable
是不是false(該屬性是不是不可以設定的),如果是的話返回,不是的話繼續,對該屬性再次使用observe
方法,作用是深度遍歷,最後呼叫Object.defineProperty
重新設計該屬性的descriptor - 在descriptor裡,屬性的getter會使用之前建立的Dep例項收集Watcher例項(觀察者)——也是它的靜態屬性
Dep.target
,如果該屬性也是一個物件或陣列的話,它的Dep例項也會收集同樣的Watcher例項;屬性的setter會在屬性更新值的時候,新舊值對比判斷需不需要更新,如果需要更新的話,更新新值並對新值使用observe
方法,最後通知Dep例項收集的Watcher例項——dep.notify()
。至此響應設計完畢 - 看一下觀察者的建構函式——
constructor (vm, expOrFn, cb, options, isRenderWatcher)
,vm表示的是關聯的Vue例項,expOrFn用於轉化為Watcher例項的方法getter並且會在初始化Watcher的時候被呼叫,cb會在新舊值對比後需要更新的時候被呼叫,options是一些配置,isRenderWatcher表示這個Watcher例項是不是用於通知檢視更新的 - Watcher建構函式中的
expOrFn
會在被呼叫之前執行Watcher例項的get()
方法,該方法會把該Watcher例項設定為Dep.target,所以expOrFn
裡的依賴收集的目標將會是該Watcher例項 - Watcher例項的value屬性是響應式設計的關鍵,它就是被觀察物件的getter的呼叫者——
value = this.getter.call(vm, vm)
,它的作用是保留舊值,用以對比新值,然後確定是否需要呼叫回撥
總結:
__ob__
Vue提供的其它響應式API
Vue除了用於更新檢視的觀察者API,還有一些其它的API
1. Vue例項的computed屬性
構造Vue例項時,傳入的 options.computed
會被設計成既是觀察者又是被觀察物件,主要有下面的三個方法:initComputed、defineComputed、createComputedGetter
function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null) { warn( `Getter is missing for computed property "${key}".`, vm ) } if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } } export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } Object.defineProperty(target, key, sharedPropertyDefinition) } function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } } } 複製程式碼
2. Vue例項的watch屬性
在例項化Vue的時候,會把 options.watch
裡的屬性都遍歷了,然後對每一個屬性呼叫 vm.$watch()
function initWatch (vm: Component, watch: Object) { for (const key in watch) { 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) } } } function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) } 複製程式碼
vm.$watch
被作為一個獨立的API匯出。
3. Vue.prototype.$watch
Vue.prototype.$watch
是Vue的公開API,可以用來觀察 options.data
裡的屬性。
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 const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } } 複製程式碼
4. Vue.prototype.$set
Vue.prototype.$set
用於在操作響應式物件和陣列的時候通知觀察者,也包括給物件新增屬性、給陣列新增元素。
Vue.prototype.$set = set /** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) ob.dep.notify() return val } 複製程式碼
ob.dep.notify()
之所以可以通知觀察者,是因為在 defineReactive
裡有如下程式碼:
let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, 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.notify() } }) 複製程式碼
上面的 childOb.dep.depend()
也為響應式物件的 __ob__.dep
添加了同樣的Watcher例項。所以 Vue.prototype.$set
和 Vue.prototype.$del
都可以在內部通知觀察者。
5. Vue.prototype.$del
Vue.prototype.$del
用於刪除響應式物件的屬性或陣列的元素時通知觀察者。
Vue.prototype.$del = del /** * Delete a property and trigger change if necessary. */ export function del (target: Array<any> | Object, key: any) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } if (!hasOwn(target, key)) { return } delete target[key] if (!ob) { return } ob.dep.notify() } 複製程式碼
簡單實現響應式設計
- 實現Watcher類和Dep類,Watcher作用是執行回撥,Dep作用是收集Watcher
class Watcher { constructor(cb) { this.callback = cb } update(newValue) { this.callback && this.callback(newValue) } } class Dep { // static Target constructor() { this.subs = [] } addSub(sub) { this.subs.push(sub) } notify(newValue) { this.subs.forEach(sub => sub.update(newValue)) } } 複製程式碼
- 處理觀察者和被觀察者
// 對被觀察者使用 function observe(obj) { let keys = Object.keys(obj) let observer = {} keys.forEach(key => { let dep = new Dep() Object.defineProperty(observer, key, { configurable: true, enumerable: true, get: function () { if (Dep.Target) dep.addSub(Dep.Target) return obj[key] }, set: function (newValue) { dep.notify(newValue) obj[key] = newValue } }) }) return observer } // 對觀察者使用 function watching(obj, key) { let cb = newValue => { obj[key] = newValue } Dep.Target = new Watcher(cb) return obj } 複製程式碼
- 檢驗程式碼
let subscriber = watching({}, 'a') let observed = observe({ a: '1' }) subscriber.a = observed.a console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`) observed.a = 2 console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`) 複製程式碼
- 結果:
subscriber.a: 1, observed.a: 1 subscriber.a: 2, observed.a: 2 複製程式碼
參考
深入理解Vue響應式原理 360080" rel="nofollow,noindex">vue.js原始碼 - 剖析observer,dep,watch三者關係 如何具體的實現資料雙向繫結 50行程式碼的MVVM,感受閉包的藝術 Vue.js 技術揭祕