前文中,已經分析了在vuejs源碼中是如何定義Vue類,以及如何添加實例屬性和靜態方法:大數據進階-讀懂vuejs源碼1。vue
Vue實例化時調用_init,本文將深刻該方法內部作了哪些事情及vuejs如何實現數據響應式。node
在core/instance/index.js
文件中定義了Vue的構造函數:react
function Vue (options) { // 執行_init方法,此方法在initMixin中定義 this._init(options) }
_init方法定義在core/instance/init.js
中:web
Vue.prototype._init = function (options?: Object) { // 。。。 // 1. 合併options if (options && options._isComponent) { // 此處有重要的事情作。 initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 2. 初始化屬性 // 初始化$root,$parent,$children initLifecycle(vm) // 初始化_events initEvents(vm) // 初始化$slots/$scopedSlots/_c/$createElement/$attrs/$listeners initRender(vm) // 執行生命週期鉤子 callHook(vm, 'beforeCreate') // 註冊inject成員到vue實例上 initInjections(vm) // resolve injections before data/props // 初始化_props/methods/_data/computed/watch initState(vm) // 初始化_provided initProvide(vm) // resolve provide after data/props // 執行生命週期鉤子 callHook(vm, 'created') // 3. 調用$mount方法 if (vm.$options.el) { vm.$mount(vm.$options.el) } }
在合併options的時候,若是options表示一個組件(_isComponent)則調用了initInternalComponent
函數:express
export function initInternalComponent(vm: Component, options: InternalComponentOptions) { // 此處保留組件之間的父子關係, const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode //... }
此方法中設置了組件之間的父子關係,在後續的註冊及渲染組件的時候會用到。segmentfault
定義在core/instance/inject.js
文件中。數組
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
在上面的代碼中能夠看出,若是provide是一個函數,那麼會調用這個函數,並將this指向vm實例。因爲initProvide在_init方法中最後被調用,所以可以訪問到實例的屬性。閉包
定義在core/instance/inject.js
文件中。app
export function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { // 遍歷result屬性,利用Object.defineProperty將其添加到vue實例上 // ... } }
此方法調用resolveInject方法獲取全部inject值。框架
export function resolveInject(inject: any, vm: Component): ?Object { if (inject) { const result = Object.create(null) const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) for (let i = 0; i < keys.length; i++) { // .... const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } source = source.$parent } // ... } return result } }
在resolveInject方法中會從當前實例出發,延着parent一直向上找,直到找到_provided中存在。
此時整個Vue定義和初始化流程能夠總結爲以下:
vuejs框架的整個數據響應式實現過程比較複雜,代碼散落在各個文件中。咱們都知道,在定義組件的時候,組件會自動將data屬性中的數據添加上響應式監聽,所以咱們從_init方法中調用initState
函數開始。
在initState函數中:
export function initState (vm: Component) { // ... if (opts.data) { // 處理data數據 initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // ... }
options中的data數據會交由initData方法處理:
function initData(vm: Component) { // ... 1. 獲取data數據,若是data是一個函數,但沒有返回值,會提示錯誤。 // ... 2. 遍歷data全部的屬性,首先判斷在props和methods是否同名,而後將其代理到vue實例上。 // 3. 添加響應式數據監聽 observe(data, true /* asRootData */) }
定義在core/observer/index.js
文件中:
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 經過__ob__屬性判斷該屬性是否添加過響應式監聽 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 ) { // 建立Observer實例,其爲響應式的核心 ob = new Observer(value) } // 經過vmCount能夠判斷某個響應式數據是不是根數據,能夠理解爲data屬性返回的對象是根數據,若是data對象的某個屬性也是一個對象,那麼就再也不是根數據。 // vmCount屬性後續會被用到 if (asRootData && ob) { ob.vmCount++ } return ob }
該方法的核心就是爲data數據建立Observer實例ob, ob對象會爲data添加getter/setter方法,其能夠用來收集依賴並在變化的時候觸發dom更新。
定義在core/observer/index.js
文件中,在其構造函數中,根據傳入data的類型(Array/Object),分別進行處理。
constructor(value: any) { this.value = value // Observer實例上包含dep屬性,這個屬性後續會有很大做用,有些沒法監聽的數據變化能夠由此屬性完成 this.dep = new Dep() this.vmCount = 0 // 爲data添加__ob__屬性 def(value, '__ob__', this) if (Array.isArray(value)) { // ... 處理數組 } else { // 處理對象 this.walk(value) } }
遍歷data的全部屬性,調用defineReactive
函數添加getter/setter。
walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { // 添加數據攔截 defineReactive(obj, keys[i]) } }
數據響應式實現的核心方法,原理是經過Object.defineProperty爲data添加getter/setter攔截,在攔截中實現依賴收集和觸發更新。
export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 1. 建立閉包做用域內的Dep對象,用於收集觀察者,當數據發生變化的時候,會觸發觀察者進行update const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 2. 獲取對象描述中原有的get和set方法 const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) // 3. 添加getter/setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { const value = getter ? getter.call(obj) : val // 靜態屬性target存儲的是當前觀察者。 if (Dep.target) { dep.depend() if (childOb) { // 將觀察者添加到Obsetver實例屬性dep中。 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 } // ... 一些判斷,省略 // 當賦值的時候,若是值爲對象,須要爲新賦值的對象添加響應式 childOb = !shallow && observe(newVal) // 調用set就是爲屬性賦值,賦值說明有新的變化,因此要觸發更新 dep.notify() } }) }
整個defineReactive有兩個地方比較難以理解:
因爲這個地方涉及到後面的編譯部分,因此咱們把這部分邏輯單獨拿出來,用一段簡短的代碼來描述整個過程,以下:
// 模擬Dep let Dep = {} Dep.target = null // 模擬變化數據 let data = { foo: 'foo' } Object.defineProperty(data, 'foo', { get() { if (Dep.target) { console.log(Dep.target) } } }) // 模擬編譯 {{foo}} // 1. 解析到template中須要foo屬性的值 const key = 'foo' // 2. 在foo屬性對應的值渲染到頁面以前,爲Dep.target賦值 Dep.target = () => { console.log('觀察foo的變化') } // 3. 獲取foo屬性的值,此時會觸發get攔截 const value = data[key] // 4. 獲取完成後,須要將Dep.target的值從新賦值null,這樣下一輪解析的時候,可以存儲新的觀察者 Dep.target = null
其實,這是爲了方便在其餘手動觸發更新,因爲defineReactive方法內部的dep對象是閉包做用域,在外部沒法直接訪問,只能經過賦值方式觸發。
若是在Observer對象上保存一份,那麼就能夠經過data.__ob__.dep
的方式訪問到,直接手動調用notify方法就能夠觸發更新,在Vue.set方法內部實現就能夠這種觸發更新方式。
衆所周知,Object.defineProperty是沒法監控到經過push,pop等方法改變數組,此時,vuejs經過另一種方式實現了數組響應式。該方式修改了數組原生的push,pop等方法,在新定義的方法中,經過調用數組對象的__ob__
屬性的notify方法,手動觸發更新。
Observer構造函數中:
if (Array.isArray(value)) { if (hasProto) { // 支持__proto__,那麼就經過obj.__proto__的方式修改原型 protoAugment(value, arrayMethods) } else { // 不支持,就將新定義的方法遍歷添加到數組對象上,這樣能夠覆蓋原型鏈上的原生方法 copyAugment(value, arrayMethods, arrayKeys) } // 遍歷數組項,若是某項是對象,那麼爲該對象添加響應式 this.observeArray(value) }
其中arrayMethods就是從新定義的數組操做方法。
定義在core/Observer/array.js
文件中,該文件主要做了兩件事情:
Array.prototype
的原型對象arrayMethods。const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto)
// 定義全部會觸發更新的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 獲取Array中原生的同名方法 const original = arrayProto[method] // 經過Object.defineProperty爲方法調用添加攔截 def(arrayMethods, method, function mutator(...args) { // 調用原生方法獲取本該獲得的結果 const result = original.apply(this, args) const ob = this.__ob__ let inserted // push,unshift,splice三個方法會向數組中插入新值,此處根據狀況獲取新插入的值 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 若是新插入的值是對象,那麼須要爲對象添加響應式,處理邏輯和data處理邏輯類似 if (inserted) ob.observeArray(inserted) // 手動觸發更新 ob.dep.notify() return result }) })
從上面的處理邏輯能夠看出,下面的數組操做能夠觸發自動更新:
// 修改數組項 [].push(1) [].pop() [].unshift(1) [].shift() [].splice() // 修改數組項順序 [].sort() [].reverse()
而下面的操做不能觸發:
// 修改數組項 [1, 2][0] = 3 [1, 2].length = 0
在添加數據監聽的過程當中用到了Dep類,Dep類至關於觀察者模式中的目標,用於存儲全部的觀察者和發生變化時調用觀察者的update方進行更新。
export default class Dep { // 當前須要添加的觀察者 static target: ?Watcher; // id,惟一標識 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) } // 調用觀察者的addDep方法,將目標添加到每個觀察者中,觀察者會調用addSub方法 depend() { if (Dep.target) { Dep.target.addDep(this) } } // 將觀察者排序,而後依次調用update 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() } } }
Watcher類是觀察者模式中的觀察者,當Dep觸發變化的時候,會調用內部存儲的全部Watcher實例的update方法進行更新操做。
在vuejs中,Watcher可大體分爲三種:Computed Watcher, 用戶Watcher(偵聽器)和渲染Watcher(觸發Dom更新)。
Watcher類包含大量的實例成員,在構造函數中,主要邏輯以下:
constructor( vm: Component, expOrFn: string | Function, cb: Function, options ?: ? Object, isRenderWatcher ?: boolean ) { // ... 根據參數爲實例成員賦值 // 調用get方法 this.value = this.lazy ? undefined : this.get() }
在get方法中,獲取初始值並將自身添加到Dep.target。
get() { // 1. 和下面的popTarget相對應,這裏主要是爲Dep.target賦值 // 因爲存在組件之間的父子關係,因此在pushTarget中還會將當前對象存放到隊列中,方便處理完成子組件後繼續處理父組件 pushTarget(this) let value const vm = this.vm try { // 2. 獲取初始值,並觸發get監聽,Dep會收集該Watcher value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // 實現deep深度監聽 if (this.deep) { traverse(value) } // 3. 將Dep.target值變爲null popTarget() this.cleanupDeps() } return value }
addDep方法用於將當前Watcher實例添加到Dep中。
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方法,將Watcher實例添加到Dep中 dep.addSub(this) } } }
update主要處理兩種狀況:
update() { if (this.lazy) { this.dirty = true } else if (this.sync) { // 用戶添加的監聽器會執行run方法 this.run() } else { // 觸發dom更新會執行此方法, 以隊列方式執行update更新 queueWatcher(this) } }
run方法主要用於在數據變化後,執行用戶傳入的回調函數。
run() { if (this.active) { // 1. 經過get方法獲取變化後的值 const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { // 2. 獲取初始化時保存的值做爲舊值 const oldValue = this.value this.value = value if (this.user) { try { // 3. 調用用戶定義的回調函數 this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
在查找編譯入口那部分講到了platforms/web/runtime/index.js
文件定義了$mount方法,此方法用於首次渲染Dom。
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
其內部執行了mountComponent函數。
定義在core/instance/lifecycle.js
文件中,該函數主要執行三塊內容:
beforeMount
,beforeUpdate
和mounted
生命週期鉤子函數。updateComponent
方法。export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... 1. 觸發生命週期鉤子 // 2. 定義updateComponent方法 let updateComponent if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // ... vm._update(vnode, hydrating) // ... } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 生成watcher實例 new Watcher(vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // ... 觸發生命週期鉤子 return vm }
_update, _render是Vue的實例方法, _render方法用於根據用戶定義的render或者模板生成的render生成虛擬Dom。_update方法根據傳入的虛擬Dom,執行patch,進行Dom對比更新。
至此,響應式處理的整個閉環脈絡已經摸清。