說明
資料綁定原理
前面已經講過Vue資料綁定的原理了,現在從源碼來看一下資料綁定在Vue中是如何實作的。
首先看一下Vue.js官網介紹響應式原理的這張圖。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SNwMzN2kDOxY2YwEzY5MmZyYzXyEzM0UTM3EzLchDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
這張圖比較清晰地展示了整個流程,首先通過一次渲染操作觸發Data的getter(這裡保證隻有視圖中需要被用到的data才會觸發getter)進行依賴收集,這時候其實Watcher與data可以看成一種被綁定的狀态(實際上是data的閉包中有一個Deps訂閱者,在修改的時候會通知所有的Watcher觀察者),在data發生變化的時候會觸發它的setter,setter通知Watcher,Watcher進行回調通知元件重新渲染的函數,之後根據diff算法來決定是否發生視圖的更新。
Vue在初始化元件資料時,在生命周期的beforeCreate與created鈎子函數之間實作了對data、props、computed、methods、events以及watch的處理。
initData
這裡來講一下initData,可以參考源碼instance下的state.js檔案,下面所有的中文注釋都是我加的,英文注釋是尤大加的,請不要忽略英文注釋,英文注釋都講到了比較關鍵或者晦澀難懂的點。
加注釋版的vue源碼也可以直接通過傳送門檢視,這些是我在閱讀Vue源碼過程中加的注釋,持續更新中。
initData主要是初始化data中的資料,将資料進行Observer,監聽資料的變化,其他的監視原理一緻,這裡以data為例。
function initData (vm: Component) {
/*得到data資料*/
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
/*判斷是否是對象*/
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
/*周遊data對象*/
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
//周遊data中的資料
while (i--) {
/*保證data中的key不與props中的key重複,props優先,如果有沖突會産生warning*/
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
/*判斷是否是保留字段*/
/*這裡是我們前面講過的代理,将data上面的屬性代理到了vm執行個體上*/
proxy(vm, `_data`, keys[i])
}
}
/*Github:https://github.com/answershuto*/
// observe data
/*從這裡開始我們要observe了,開始對資料進行綁定,這裡有尤大大的注釋asRootData,這步作為根資料,下面會進行遞歸observe進行對深層對象的綁定。*/
observe(data, true /* asRootData */)
}
其實這段代碼主要做了兩件事,一是将_data上面的資料代理到vm上,另一件事通過observe将所有資料變成observable。
proxy
接下來看一下proxy代理。
/*添加代理*/
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
這裡比較好了解,通過proxy函數将data上面的資料代理到vm上,這樣就可以用app.text代替app._data.text了。
observe
接下來是observe,這個函數定義在core檔案下observer的index.js檔案中。
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
/*
嘗試建立一個Observer執行個體(__ob__),如果成功建立Observer執行個體則傳回新的Observer執行個體,如果已有Observer執行個體則傳回現有的Observer執行個體。
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
/*判斷是否是一個對象*/
if (!isObject(value)) {
return
}
let ob: Observer | void
/*這裡用__ob__這個屬性來判斷是否已經有Observer執行個體,如果沒有Observer執行個體則會建立一個Observer執行個體并指派給__ob__這個屬性,如果已有Observer執行個體則直接傳回該Observer執行個體*/
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
/*這裡的判斷是為了確定value是單純的對象,而不是函數或者是Regexp等情況。*/
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
/*如果是根資料則計數,後面Observer中的observe的asRootData非true*/
ob.vmCount++
}
return ob
}
Vue的響應式資料都會有一個__ob__的屬性作為标記,裡面存放了該屬性的觀察器,也就是Observer的執行個體,防止重複綁定。
Observer
接下來看一下建立的Observer。Observer的作用就是周遊對象的所有屬性将其進行雙向綁定。
/**
* 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 {
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
/*
将Observer執行個體綁定到data的__ob__屬性上面去,之前說過observe的時候會先檢測是否已經有__ob__對象存放Observer執行個體了,def方法定義可以參考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16
*/
def(value, '__ob__', this)
if (Array.isArray(value)) {
/*
如果是數組,将修改後可以截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組資料變化響應的效果。
這裡如果目前浏覽器支援__proto__屬性,則直接覆寫目前數組對象原型上的原生數組方法,如果不支援該屬性,則直接覆寫數組對象的原型。
*/
const augment = hasProto
? protoAugment /*直接覆寫原型的方法來修改目标對象*/
: copyAugment /*定義(覆寫)目标對象或數組的某一個方法*/
augment(value, arrayMethods, arrayKeys)
/*Github:https://github.com/answershuto*/
/*如果是數組則需要周遊數組的每一個成員進行observe*/
this.observeArray(value)
} else {
/*如果是對象則直接walk進行綁定*/
this.walk(value)
}
}
/**
* 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)
/*walk方法會周遊對象的每一個屬性進行defineReactive綁定*/
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
/*數組需要周遊每一個成員進行observe*/
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Observer為資料加上響應式屬性進行雙向綁定。如果是對象則進行深度周遊,為每一個子對象都綁定上方法,如果是數組則為每一個成員都綁定上方法。
如果是修改一個數組的成員,該成員是一個對象,那隻需要遞歸對數組的成員進行雙向綁定即可。但這時候出現了一個問題:如果我們進行pop、push等操作的時候,push進去的對象根本沒有進行過雙向綁定,更别說pop了,那麼我們如何監聽數組的這些變化呢?
Vue.js提供的方法是重寫push、pop、shift、unshift、splice、sort、reverse這七個數組方法。修改數組原型方法的代碼可以參考observer/array.js以及observer/index.js。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
//.......
if (Array.isArray(value)) {
/*
如果是數組,将修改後可以截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組資料變化響應的效果。
這裡如果目前浏覽器支援__proto__屬性,則直接覆寫目前數組對象原型上的原生數組方法,如果不支援該屬性,則直接覆寫數組對象的原型。
*/
const augment = hasProto
? protoAugment /*直接覆寫原型的方法來修改目标對象*/
: copyAugment /*定義(覆寫)目标對象或數組的某一個方法*/
augment(value, arrayMethods, arrayKeys)
/*如果是數組則需要周遊數組的每一個成員進行observe*/
this.observeArray(value)
} else {
/*如果是對象則直接walk進行綁定*/
this.walk(value)
}
}
}
/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
*/
/*直接覆寫原型的方法來修改目标對象或數組*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
/*定義(覆寫)目标對象或數組的某一個方法*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
/*取得原生數組的原型*/
const arrayProto = Array.prototype
/*建立一個新的數組對象,修改該對象上的數組的七個方法,防止污染原生數組方法*/
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
/*這裡重寫了數組的這些方法,在保證不污染原生數組原型的情況下重寫數組的這些方法,截獲數組的成員發生的變化,執行原生數組操作的同時dep通知關聯的所有觀察者進行響應式處理*/
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
/*将數組的原生方法緩存起來,後面要調用*/
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
/*調用原生的數組方法*/
const result = original.apply(this, args)
/*數組新插入的元素需要重新進行observe才能響應式*/
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
/*dep通知所有注冊的觀察者進行響應式處理*/
ob.dep.notify()
return result
})
})
從數組的原型建立一個Object.create(arrayProto)對象,通過修改此原型可以保證原生數組方法不被污染。如果目前浏覽器支援__proto__這個屬性的話就可以直接覆寫該屬性則使數組對象具有了重寫後的數組方法。如果沒有該屬性的浏覽器,則必須通過周遊def所有需要重寫的數組方法,這種方法效率較低,是以優先使用第一種。
在保證不污染不覆寫數組原生方法添加監聽,主要做了兩個操作,第一是通知所有注冊的觀察者進行響應式處理,第二是如果是添加成員的操作,需要對新成員進行observe。
但是修改了數組的原生方法以後我們還是沒法像原生數組一樣直接通過數組的下标或者設定length來修改數組,可以通過Vue.set以及splice方法。
Watcher
Watcher是一個觀察者對象。依賴收集以後Watcher對象會被儲存在Deps中,資料變動的時候會由Deps通知Watcher執行個體,然後由Watcher執行個體回調cb進行視圖的更新。
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: ISet;
newDepIds: ISet;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
/*_watchers存放訂閱者執行個體*/
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
/*把表達式expOrFn解析成getter*/
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
/*獲得getter的值并且重新進行依賴收集*/
get () {
/*将自身watcher觀察者執行個體設定給Dep.target,用以依賴收集。*/
pushTarget(this)
let value
const vm = this.vm
/*
執行了getter操作,看似執行了渲染操作,其實是執行了依賴收集。
在将Dep.target設定為自身觀察者執行個體以後,執行getter操作。
譬如說現在的的data中可能有a、b、c三個資料,getter渲染需要依賴a跟c,
那麼在執行getter的時候就會觸發a跟c兩個資料的getter函數,
在getter函數中即可判斷Dep.target是否存在然後完成依賴收集,
将該觀察者對象放入閉包中的Dep的subs中去。
*/
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
/*如果存在deep,則觸發每個深層對象的依賴,追蹤其變化*/
if (this.deep) {
/*遞歸每一個對象或者數組,觸發它們的getter,使得對象或數組的每一個成員都被依賴收集,形成一個“深(deep)”依賴關系*/
traverse(value)
}
/*将觀察者執行個體從target棧中取出并設定給Dep.target*/
popTarget()
this.cleanupDeps()
return value
}
/**
* Add a dependency to this directive.
*/
/*添加一個依賴關系到Deps集合中*/
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(this)
}
}
}
/**
* Clean up for dependency collection.
*/
/*清理依賴收集*/
cleanupDeps () {
/*移除所有觀察者對象*/
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
/*
排程者接口,當依賴發生改變的時候進行回調。
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步則執行run直接渲染視圖*/
this.run()
} else {
/*異步推送到觀察者隊列中,由排程者調用。*/
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
/*
排程者工作接口,将被排程者回調。
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
/*
即便值相同,擁有Deep屬性的觀察者以及在對象/數組上的觀察者應該被觸發更新,因為它們的值可能發生改變。
*/
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
/*設定新的值*/
this.value = value
/*觸發回調渲染視圖*/
if (this.user) {
try {
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)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
/*擷取觀察者的值*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
/*收集該watcher的所有deps依賴*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
/*将自身從所有依賴收集訂閱清單删除*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
/*從vm執行個體的觀察者清單中将自身移除,由于該操作比較耗費資源,是以如果vm執行個體正在被銷毀則跳過該步驟。*/
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
Dep
來看看Dep類。其實Dep就是一個釋出者,可以訂閱多個觀察者,依賴收集之後Deps中會存在一個或多個Watcher對象,在資料變更的時候通知所有的Watcher。
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
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)
}
/*依賴收集,當存在Dep.target的時候添加觀察者對象*/
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()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
/*依賴收集完需要将Dep.target設為null,防止後面重複添加依賴。*/
defineReactive
接下來是defineReactive。defineReactive的作用是通過Object.defineProperty為資料定義上getter\setter方法,進行依賴收集後閉包中的Deps會存放Watcher對象。觸發setter改變資料的時候會通知Deps訂閱者通知所有的Watcher觀察者對象進行試圖的更新。
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/*在閉包中定義一個dep對象*/
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
/*如果之前該對象已經預設了getter以及setter函數則将其取出來,新定義的getter/setter中會将其執行,保證不會覆寫之前已經定義的getter/setter。*/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
/*對象的子對象遞歸進行observe并傳回子節點的Observer對象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/*如果原本對象擁有getter方法則執行*/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/*進行依賴收集*/
dep.depend()
if (childOb) {
/*子對象進行依賴收集,其實就是将同一個watcher觀察者執行個體放進了兩個depend中,一個是正在本身閉包中的depend,另一個是子元素的depend*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是數組則需要對每一個成員都進行依賴收集,如果數組的成員還是數組,則遞歸。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/*通過getter方法擷取目前值,與新值進行比較,一緻則不需要執行下面的操作*/
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方法則執行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新進行observe,保證資料響應式*/
childOb = observe(newVal)
/*dep對象通知所有的觀察者*/
dep.notify()
}
})
}