Vue做爲一種MVVM框架,可以實現數據的雙向綁定,讓Vue技術棧的前端從業者擺脫了繁瑣的DOM操做,這徹底得益於Vue框架開發者對原生Object對象的深度應用。Vue實現數據響應系統的核心技術就是數據劫持和訂閱-發佈,基本思想就是經過對數據操做進行截獲,在數據進行getter操做的時候更新依賴,即依賴收集過程(更新訂閱對象集);在數據進行setter時通知全部依賴的組件一併進行更新操做(通知訂閱對象)。Vue數據響應系統原理示意圖以下:javascript
接下來對Vue數據響應系統的這兩個核心技術進行探究:html
Vue數據劫持主要利用Object.defineProperty來實現對對象屬性的攔截操做,攔截工做主要就Object.defineProperty中的getter和setter作文章,看一栗子:前端
var person = { name: '小明', } var name = person.name; Object.defineProperty(person, 'name', { get: function() { console.log('name getter方法被調用'); return name; }, set: function(val) { console.log('name setter方法被調用'); name = '李' + val; } }) console.log(person.name); person.name = '小亮'; console.log(person.name); // 結果 name getter方法被調用 小明 name setter方法被調用 name getter方法被調用
能夠看到,設置了屬性的存取器後,在設置和獲取person.name的時候,自定義的getter和setter將會被調用,這就給在這兩個方法調用時進行其它內部的自定義操做提供了可能。瞭解了Object.defineProperty的基本使用後,咱們開始從Vue源碼角度開始逐步瞭解其具體的應用,在Vue的_init初始化過程當中(傳送門)有一個initState操做,該操做主要依次對options中定義的props、methods、data、computed、watch屬性進行初始化(主要分析對data的初始化initData),該過程就主要是實現了將options.data變得可觀察:
src/core/instance/state.jsjava
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} ... // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] ... else if (!isReserved(key)) { // 設置data的數據代理 proxy(vm, `_data`, key) } } // observe data // 將data數據屬性變爲可觀察 observe(data, true /* asRootData */) }
繼續看對observe的定義:react
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 若是已經處於觀察中,則返回觀察對象,不然將value變爲可觀察 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 建立觀察器 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } src/core/observer/index.js // Observer 監聽器類 class Observer { constructor (value: any) { this.value = value this.dep = new Dep() // 訂閱器 this.vmCount = 0 def(value, '__ob__', this) // 爲value添加__ob__屬性 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 處理數組,數組中的每一個對象都會進行可觀察化操做 this.observeArray(value) } else { // 處理對象,主要遍歷對象中全部屬性,並將全部屬性變得可觀察 this.walk(value) } } } /** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 定義一個訂閱器 const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 判斷是否有自定義的getter, 沒有則直接取值 const value = getter ? getter.call(obj) : val // 判斷是經過watcher進行依賴蒐集調用仍是直接數據調用方式(好比在template目標中直接使用該值,此時 // Dep.target爲undefined,該值爲具備全局性,指向當前的執行的watcher) if (Dep.target) { dep.depend() // 將屬性自身放入依賴列表中 // 若是子屬性爲對象,則對子屬性進行依賴收集 if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { // 處理數組的依賴收集 dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 判斷是否有自定義的getter, 沒有則直接設置值,不然經過getter取值 const value = getter ? getter.call(obj) : val ... if (setter) { setter.call(obj, newVal) } else { val = newVal } // 從新計算該對象的數據是否須要進行觀察 childOb = !shallow && observe(newVal) // 設置數據後通知全部的watcher訂閱者 dep.notify() } }) }
經過上面步驟將options中data的的屬性變得可觀察,defineReactive方法中的閉包方法set比較好理解,就如上面所說,設置新值newVal,並判斷該值是不是爲非基本數據類型,如若不是,可能就須要重新將newVal變得可觀察;而後通知訂閱器中全部的訂閱者,進行視圖更新等操做;get中的Dep.target不太好理解,我也是研究了一兩天才明白,這裏先不說,反正就是知足Dep.target不爲undefined,則進行依賴收集,不然就是普通的數據獲取操做,返回數據便可。git
假使你們都曉得訂閱-發佈是個什麼狀況(不太清除可自行百度,不在此佔據篇幅了),那麼,咱們要知道,誰是訂閱者,訂閱的目標是什麼?有了這個疑問,咱們接下來看看相關的數據結構定義github
// src/core/observer/dep.js export default class Dep { static target: ?Watcher; //target全局 id: number; subs: Array<Watcher>; 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() 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) } // 循環對訂閱者進行更新操做(調用watcher的update方法) for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { // 將當前的watcher推入堆棧中,關於爲何要推入堆棧,主要是要處理模板或render函數中嵌套了多層組件,須要遞歸處理 targetStack.push(target) // 設置當前watcher到全局的Dep.target,經過在此處設置,key使得在進行get的時候對當前的訂閱者進行依賴收集 Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] } // src/core/observer/watcher.js 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)) { // 將訂閱的watcher添加到當前的發佈者watcher中 dep.addSub(this) } } }
以前在getter的依賴收集過程當中,當Dep.target成立時,會執行dep.depend()
方法,能夠看到,Dep中定義的depend()方法會調用Dep.target
的addDep()
方法,這個過程傳遞的參數就是在defineReactive中定義的dep對象,那麼Dep.target究竟是什麼東西呢,這個能夠看Watcher對應get方法中的定義:express
// Watcher -> get() get () { 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) } // 彈出 popTarget() // 清除依賴,主要是針對有些發生了改變的依賴進行更新,好比新添加了依賴或者去除了原有依賴 this.cleanupDeps() } return value } // Dep-> pushTarget() export function pushTarget (target: ?Watcher) { // 將當前的watcher推入堆棧中,關於爲何要推入堆棧,主要是要處理模板或render函數中嵌套了多層組件,須要遞歸處理 targetStack.push(target) // 設置當前watcher到全局的Dep.target,經過在此處設置,key使得在進行get的時候對當前的訂閱者進行依賴收集 Dep.target = target }
由於js是單線程執行,同一時刻只能執行一個Watcher,執行當前Watcher實例時候,Dep.target指向當前Watcher訂閱者,當在執行下一個Watcher訂閱者的get方法時候,即指向下一個訂閱者,即Dep.target永遠指向的是當前的Watcher訂閱者,而後將當前訂閱者添加到dep對應的subs訂閱列表中,同時訂閱者內部也須要記錄有哪些訂閱目標(dep),便於進行依賴收集過程的更新操做。用一張圖來表述:數組
在數據劫持的閉包方法setter代碼中,經過調用dep.notify()
進行數據更新通知,這個階段的主要工做就是將當前訂閱目標dep更新消息通知到訂閱列表中的訂閱者(Watcher),而後訂閱者利用註冊的回調方法進行視圖渲染等操做。數據結構
// src/core/observer/dep.js notify () { ... // 循環對訂閱者進行更新操做(調用Watcher的update方法) for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } // src/core/observer/watcher.js update () { /* istanbul ignore else */ if (this.lazy) { // 是否爲懶加載(這個什麼時候執行?) this.dirty = true } else if (this.sync) { // 是否爲同步方式更新 this.run() } else { // 加入到訂閱者更新隊列(最終也要執行run方法) queueWatcher(this) } }
$emsp;在run()中主要調用Watcher -> get()
,在這說明一下get()
中this.getter.call(vm, vm)
的getter來源主要有兩種-函數或表達式,函數好比render function、computed中的getter(), 表達式相似於"person.name"這種類型,通過parsePath
轉換成爲getter()方法。傳送門中對Watcher的種類進行了總結。
以上就是我對Vue數據響應式系統的一些學習,因爲工做緣由,先後花了一週左右的時間才寫完,初步涉獵,估計仍是有很多理解上的不到位,但願有幸能被你們看到並指出,這一過程當中參考了很多的前人好文,太多就羅列一二,但願對你們也有幫助:
另外,歡迎去本人git 相互學習和star,不勝感激。