Vue數(shù)據(jù)綁定簡析小結(jié)
作為MVVM框架的一種,Vue最為人津津樂道的當是數(shù)據(jù)與視圖的綁定,將直接操作DOM節(jié)點變?yōu)樾薷?data 數(shù)據(jù),利用 Virtual Dom 來 Diff 對比新舊視圖,從而實現(xiàn)更新。不僅如此,還可以通過 Vue.prototype.$watch 來監(jiān)聽 data 的變化并執(zhí)行回調(diào)函數(shù),實現(xiàn)自定義的邏輯。雖然日常的編碼運用已經(jīng)駕輕就熟,但未曾去深究技術(shù)背后的實現(xiàn)原理。作為一個好學的程序員,知其然更要知其所以然,本文將從源碼的角度來對Vue響應式數(shù)據(jù)中的觀察者模式進行簡析。
初始化 Vue 實例
在閱讀源碼時,因為文件繁多,引用復雜往往使我們不容易抓住重點,這里我們需要找到一個入口文件,從 Vue 構(gòu)造函數(shù)開始,拋開其他無關因素,一步步理解響應式數(shù)據(jù)的實現(xiàn)原理。首先我們找到 Vue 構(gòu)造函數(shù):
// src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// src/core/instance/init.js
Vue.prototype._init = function (options) {
...
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 初始化vm實例的$options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
initLifecycle(vm) // 梳理實例的parent、root、children和refs,并初始化一些與生命周期相關的實例屬性
initEvents(vm) // 初始化實例的listeners
initRender(vm) // 初始化插槽,綁定createElement函數(shù)的vm實例
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 掛載組件到節(jié)點
}
}
為了方便閱讀,我們?nèi)コ?flow 類型檢查和部分無關代碼??梢钥吹剑趯嵗疺ue組件時,會調(diào)用 Vue.prototype._init ,而在方法內(nèi)部,數(shù)據(jù)的初始化操作主要在 initState (這里的 initInjections 和 initProvide 與 initProps 類似,在理解了 initState 原理后自然明白),因此我們重點來關注 initState 。
// src/core/instance/state.js
export function initState (vm) {
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)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
首先初始化了一個 _watchers 數(shù)組,用來存放 watcher ,之后根據(jù)實例的 vm.$options ,相繼調(diào)用 initProps 、 initMethods 、 initData 、 initComputed 和 initWatch 方法。
initProps
function initProps (vm, propsOptions) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
...
defineReactive(props, key, value)
if (!(key in vm)) {
proxy(vm, '_props', key)
}
}
toggleObserving(true)
}
在這里, vm.$options.propsData 是通過父組件傳給子組件實例的數(shù)據(jù)對象,如 <my-element :item="false"></my-element> 中的 {item: false} ,然后初始化 vm._props 和 vm.$options._propKeys 分別用來保存實例的 props 數(shù)據(jù)和 keys ,因為子組件中使用的是通過 proxy 引用的 _props 里的數(shù)據(jù),而不是父組件傳遞的 propsData ,所以這里緩存了 _propKeys ,用來 updateChildComponent 時能更新 vm._props 。接著根據(jù) isRoot 是否是根組件來判斷是否需要調(diào)用 toggleObserving(false) ,這是一個全局的開關,來控制是否需要給對象添加 __ob__ 屬性。這個相信大家都不陌生,一般的組件的 data 等數(shù)據(jù)都包含這個屬性,這里先不深究,等之后和 defineReactive 時一起講解。因為 props 是通過父傳給子的數(shù)據(jù),在父元素 initState 時已經(jīng)把 __ob__ 添加上了,所以在不是實例化根組件時關閉了這個全局開關,待調(diào)用結(jié)束前在通過 toggleObserving(true) 開啟。
之后是一個 for 循環(huán),根據(jù)組件中定義的 propsOptions 對象來設置 vm._props ,這里的 propsOptions 就是我們常寫的
export default {
...
props: {
item: {
type: Object,
default: () => ({})
}
}
}
循環(huán)體內(nèi),首先
const value = validateProp(key, propsOptions, propsData, vm)
validateProp 方法主要是校驗數(shù)據(jù)是否符合我們定義的 type ,以及在 propsData 里未找到 key 時,獲取默認值并在對象上定義 __ob__ ,最后返回相應的值,在這里不做展開。
這里我們先跳過 defineReactive ,看最后
if (!(key in vm)) {
proxy(vm, '_props', key)
}
其中 proxy 方法:
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
在 vm 不存在 key 屬性時,通過 Object.defineProperty 使得我們能通過 vm[key] 訪問到 vm._props[key] 。
defineReactive
在 initProps 中,我們了解到其首先根據(jù)用戶定義的 vm.$options.props 對象,通過對父組件設置的傳值對象 vm.$options.propsData 進行數(shù)據(jù)校驗,返回有效值并保存到 vm._props ,同時保存相應的 key 到 vm.$options._propKeys 以便進行子組件的 props 數(shù)據(jù)更新,最后利用 getter/setter 存取器屬性,將 vm[key] 指向?qū)?vm._props[key] 的操作。但其中跳過了最重要的 defineReactive ,現(xiàn)在我們將通過閱讀 defineReactive 源碼,了解響應式數(shù)據(jù)背后的實現(xiàn)原理。
// src/core/observer/index.js
export function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
const dep = new Dep()
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
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
...
}
首先 const dep = new Dep() 實例化了一個 dep ,在這里利用閉包來定義一個依賴項,用以與特定的 key 相對應。因為其通過 Object.defineProperty 重寫 target[key] 的 getter/setter 來實現(xiàn)數(shù)據(jù)的響應式,因此需要先判斷對象 key 的 configurable 屬性。接著
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
arguments.length === 2 意味著調(diào)用 defineReactive 時未傳遞 val 值,此時 val 為 undefined ,而 !getter || setter 判斷條件則表示如果在 property 存在 getter 且不存在 setter 的情況下,不會獲取 key 的數(shù)據(jù)對象,此時 val 為 undefined ,之后調(diào)用 observe 時將不對其進行深度觀察。正如之后的 setter 訪問器中的:
if (getter && !setter) return
此時數(shù)據(jù)將是只讀狀態(tài),既然是只讀狀態(tài),則不存在數(shù)據(jù)修改問題,繼而無須深度觀察數(shù)據(jù)以便在數(shù)據(jù)變化時調(diào)用觀察者注冊的方法。
Observe
在 defineReactive 里,我們先獲取了 target[key] 的 descriptor ,并緩存了對應的 getter 和 setter ,之后根據(jù)判斷選擇是否獲取 target[key] 對應的 val ,接著是
let childOb = !shallow && observe(val)
根據(jù) shallow 標志來確定是否調(diào)用 observe ,我們來看下 observe 函數(shù):
// src/core/observer/index.js
export function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
let 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
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
首先判斷需要觀察的數(shù)據(jù)是否為對象以便通過 Object.defineProperty 定義 __ob__ 屬性,同時需要 value 不屬于 VNode 的實例( VNode 實例通過 Diff 補丁算法來實現(xiàn)實例對比并更新)。接著判斷 value 是否已有 __ob__ ,如果沒有則進行后續(xù)判斷:
- shouldObserve:全局開關標志,通過toggleObserving來修改。
- !isServerRendering():判斷是否服務端渲染。
- (Array.isArray(value) || isPlainObject(value)):數(shù)組和純對象時才允許添加__ob__進行觀察。
- Object.isExtensible(value):判斷value是否可擴展。
- !value._isVue:避免Vue實例被觀察
滿足以上五個條件時,才會調(diào)用 ob = new Observer(value) ,接下來我們要看下 Observer 類里做了哪些工作
// src/core/observer/index.js
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
構(gòu)造函數(shù)里初始化了 value 、 dep 和 vmCount 三個屬性,為 this.value 添加 __ob__ 對象并指向自己,即 value.__ob__.value === value ,這樣就可以通過 value 或 __ob__ 對象取到 dep 和 value 。 vmCount 的作用主要是用來區(qū)分是否為 Vue 實例的根 data , dep 的作用這里先不介紹,待與 getter/setter 里的 dep 一起解釋。
接著根據(jù) value 是數(shù)組還是純對象來分別調(diào)用相應的方法,對 value 進行遞歸操作。當 value 為純對象時,調(diào)用 walk 方法,遞歸調(diào)用 defineReactive 。當 value 是數(shù)組類型時,首先判斷是否有 __proto__ ,有就使用 __proto__ 實現(xiàn)原型鏈繼承,否則用 Object.defineProperty 實現(xiàn)拷貝繼承。其中繼承的基類 arrayMethods 來自 src/core/observer/array.js :
// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
這里為什么要對數(shù)組的實例方法進行重寫呢?代碼里的 methodsToPatch 這些方法并不會返回新的數(shù)組,導致無法觸發(fā) setter ,因而不會調(diào)用觀察者的方法。所以重寫了這些變異方法,使得在調(diào)用的時候,利用 observeArray 對新插入的數(shù)組元素添加 __ob__ ,并能夠通過 ob.dep.notify 手動通知對應的被觀察者執(zhí)行注冊的方法,實現(xiàn)數(shù)組元素的響應式。
if (asRootData && ob) {
ob.vmCount++
}
最后添加這個 if 判斷,在 Vue 實例的根 data 對象上,執(zhí)行 ob.vmCount++ ,這里主要為了后面根據(jù) ob.vmCount 來區(qū)分是否為根數(shù)據(jù),從而在其上執(zhí)行 Vue.set 和 Vue.delete 。
getter/setter
在對 val 進行遞歸操作后(假如需要的話),將 obj[key] 的數(shù)據(jù)對象封裝成了一個被觀察者,使得能夠被觀察者觀察,并在需要的時候調(diào)用觀察者的方法。這里通過 Object.defineProperty 重寫了 obj[key] 的訪問器屬性,對 getter/setter 操作做了攔截處理, defineReactive 剩余的代碼具體如下:
...
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) {
...
childOb = !shallow && observe(newVal)
dep.notify()
}
})
首先在 getter 調(diào)用時,判斷 Dep.target 是否存在,若存在則調(diào)用 dep.depend 。我們先不深究 Dep.target ,只當它是一個觀察者,比如我們常用的某個計算屬性,調(diào)用 dep.depend 會將 dep 當做計算屬性的依賴項存入其依賴列表,并把這個計算屬性注冊到這個 dep 。這里為什么需要互相引用呢?這是因為一個 target[key] 可以充當多個觀察者的依賴項,同時一個觀察者可以有多個依賴項,他們之間屬于多對多的關系。這樣當某個依賴項改變時,我們可以根據(jù) dep 里維護的觀察者,調(diào)用他們的注冊方法?,F(xiàn)在我們回過頭來看 Dep :
// src/core/observer/dep.js
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)
}
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()
}
}
}
構(gòu)造函數(shù)里,首先添加一個自增的 uid 用以做 dep 實例的唯一性標志,接著初始化一個觀察者列表 subs ,并定義了添加觀察者方法 addSub 和移除觀察者方法 removeSub ??梢钥吹狡湓?getter 中調(diào)用的 depend 會將當前這個 dep 實例添加到觀察者的依賴項,在 setter 里調(diào)用的 notify 會執(zhí)行各個觀察者注冊的 update 方法, Dep.target.addDep 這個方法將在之后的 Watcher 里進行解釋。簡單來說就是會在 key 的 getter 觸發(fā)時進行 dep 依賴收集到 watcher 并將 Dep.target 添加到當前 dep 的觀察者列表,這樣在 key 的 setter 觸發(fā)時,能夠通過觀察者列表,執(zhí)行觀察者的 update 方法。
當然,在 getter 中還有如下幾行代碼:
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
這里可能會有疑惑,既然已經(jīng)調(diào)用了 dep.depend ,為什么還要調(diào)用 childOb.dep.depend ?兩個 dep 之間又有什么關系呢?
其實這兩個 dep 的分工是不同的。對于數(shù)據(jù)的增、刪,利用 childOb.dep.notify 來調(diào)用觀察者方法,而對于數(shù)據(jù)的修改,則使用的 dep.notify ,這是因為 setter 訪問器無法監(jiān)聽到對象數(shù)據(jù)的添加和刪除。舉個例子:
const data = {
arr: [{
value: 1
}],
}
data.a = 1; // 無法觸發(fā)setter
data.arr[1] = {value: 2}; // 無法觸發(fā)setter
data.arr.push({value: 3}); // 無法觸發(fā)setter
data.arr = [{value: 4}]; // 可以觸發(fā)setter
還記得 Observer 構(gòu)造函數(shù)里針對數(shù)組類型 value 的響應式轉(zhuǎn)換嗎?通過重寫 value 原型鏈,使得對于新插入的數(shù)據(jù):
if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify()
將其轉(zhuǎn)換為響應式數(shù)據(jù),并通過 ob.dep.notify 來調(diào)用觀察者的方法,而這里的觀察者列表就是通過上述的 childOb.dep.depend 來收集的。同樣的,為了實現(xiàn)對象新增數(shù)據(jù)的響應式,我們需要提供相應的 hack 方法,而這就是我們常用的 Vue.set/Vue.delete 。
// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): 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
}
- 判斷value是否為數(shù)組,如果是,直接調(diào)用已經(jīng)hack過的splice即可。
- 是否已存在key,有的話說明已經(jīng)是響應式了,直接修改即可。
- 接著判斷target.__ob__是否存在,如果沒有說明該對象無須深度觀察,設置返回當前的值。
- 最后,通過defineReactive來設置新增的key,并調(diào)用ob.dep.notify通知到觀察者。
現(xiàn)在我們了解了 childOb.dep.depend() 是為了將當前 watcher 收集到 childOb.dep ,以便在增、刪數(shù)據(jù)時能通知到 watcher 。而在 childOb.dep.depend() 之后還有:
if (Array.isArray(value)) {
dependArray(value)
}
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
在觸發(fā) target[key] 的 getter 時,如果 value 的類型為數(shù)組,則遞歸將其每個元素都調(diào)用 __ob__.dep.depend ,這是因為無法攔截數(shù)組元素的 getter ,所以將當前 watcher 收集到數(shù)組下的所有 __ob__.dep ,這樣當其中一個元素觸發(fā)增、刪操作時能通知到觀察者。比如:
const data = {
list: [[{value: 0}]],
};
data.list[0].push({value: 1});
這樣在 data.list[0].__ob__.notify 時,才能通知到 watcher 。
target[key] 的 getter 主要作用:
將 Dep.target 收集到閉包中 dep 的觀察者列表,以便在 target[key] 的 setter 修改數(shù)據(jù)時通知觀察者
根據(jù)情況對數(shù)據(jù)進行遍歷添加 __ob__ ,將 Dep.target 收集到 childOb.dep 的觀察者列表,以便在增加/刪除數(shù)據(jù)時能通知到觀察者
通過 dependArray 將數(shù)組型的 value 遞歸進行觀察者收集,在數(shù)組元素發(fā)生增、刪、改時能通知到觀察者
target[key] 的 setter 主要作用是對新數(shù)據(jù)進行觀察,并通過閉包保存到 childOb 變量供 getter 使用,同時調(diào)用 dep.notify 通知觀察者,在此就不再展開。
Watcher
在前面的篇幅中,我們主要介紹了 defineReactive 來定義響應式數(shù)據(jù):通過閉包保存 dep 和 childOb ,在 getter 時來進行觀察者的收集,使得在數(shù)據(jù)修改時能觸發(fā) dep.notify 或 childOb.dep.notify 來調(diào)用觀察者的方法進行更新。但具體是如何進行 watcher 收集的卻未做過多解釋,現(xiàn)在我們將通過閱讀 Watcher 來了解觀察者背后的邏輯。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
...
}
}
這是 Vue 計算屬性的初始化操作,去掉了一部分不影響的代碼。首先初始化對象 vm._computedWatchers 用以存儲所有的計算屬性, isSSR 用以判斷是否為服務端渲染。再根據(jù)我們編寫的 computed 鍵值對循環(huán)遍歷,如果不是服務端渲染,則為每個計算屬性實例化一個 Watcher ,并以鍵值對的形式保存到 vm._computedWatchers 對象,接下來我們主要看下 Watcher 這個類。
Watcher 的構(gòu)造函數(shù)
構(gòu)造函數(shù)接受5個參數(shù),其中當前 Vue 實例 vm 、求值表達式 expOrFn (支持 Function 或者 String ,計算屬性中一般為 Function ),回調(diào)函數(shù) cb 這三個為必傳參數(shù)。設置 this.vm = vm 用以后續(xù)綁定 this.getter 的執(zhí)行環(huán)境,并將 this 推入 vm._watchers ( vm._watchers 用以維護實例 vm 中所有的觀察者),另外根據(jù)是否為渲染觀察者來賦值 vm._watcher = this (常用的 render 即為渲染觀察者)。接著根據(jù) options 進行一系列的初始化操作。其中有幾個屬性:
- this.lazy:設置是否懶求值,這樣能保證有多個被觀察者發(fā)生變化時,能只調(diào)用求值一次。
- this.dirty:配合this.lazy,用以標記當前觀察者是否需要重新求值。
- this.deps、this.newDeps、this.depIds、this.newDepIds:用以維護被觀察對象的列表。
- this.getter:求值函數(shù)。
- this.value:求值函數(shù)返回的值,即為計算屬性中的值。
Watcher 的求值
因為計算屬性是惰性求值,所以我們繼續(xù)看 initComputed 循環(huán)體:
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
defineComputed 主要將 userDef 轉(zhuǎn)化為 getter/setter 訪問器,并通過 Object.defineProperty 將 key 設置到 vm 上,使得我們能通過 this[key] 直接訪問到計算屬性。接下來我們主要看下 userDef 轉(zhuǎn)為 getter 中的 createComputedGetter 函數(shù):
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
利用閉包保存計算屬性的 key ,在 getter 觸發(fā)時,首先通過 this._computedWatchers[key] 獲取到之前保存的 watcher ,如果 watcher.dirty 為 true 時調(diào)用 watcher.evaluate (執(zhí)行 this.get() 求值操作,并將當前 watcher 的 dirty 標記為 false ),我們主要看下 get 操作:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (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
}
可以看到,求值時先執(zhí)行 pushTarget(this) ,通過查閱 src/core/observer/dep.js ,我們可以看到:
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
pushTarget 主要是把 watcher 實例進棧,并賦值給 Dep.target ,而 popTarget 則相反,把 watcher 實例出棧,并將棧頂賦值給 Dep.target 。 Dep.target 這個我們之前在 getter 里見到過,其實就是當前正在求值的觀察者。這里在求值前將 Dep.target 設置為 watcher ,使得在求值過程中獲取數(shù)據(jù)時觸發(fā) getter 訪問器,從而調(diào)用 dep.depend ,繼而執(zhí)行 watcher 的 addDep 操作:
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)
}
}
}
先判斷 newDepIds 是否包含 dep.id ,沒有則說明尚未添加過這個 dep ,此時將 dep 和 dep.id 分別加到 newDepIds 和 newDeps 。如果 depIds 不包含 dep.id ,則說明之前未添加過此 dep ,因為是雙向添加的(將 dep 添加到 watcher 的同時也需要將 watcher 收集到 dep ),所以需要調(diào)用 dep.addSub ,將當前 watcher 添加到新的 dep 的觀察者隊列。
if (this.deep) {
traverse(value)
}
再接著根據(jù) this.deep 來調(diào)用 traverse 。 traverse 的作用主要是遞歸遍歷觸發(fā) value 的 getter ,調(diào)用所有元素的 dep.depend() 并過濾重復收集的 dep 。最后調(diào)用 popTarget() 將當前 watcher 移出棧,并執(zhí)行 cleanupDeps :
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
...
}
遍歷 this.deps ,如果在 newDepIds 中不存在 dep.id ,則說明新的依賴里不包含當前 dep ,需要到 dep 的觀察者列表里去移除當前這個 watcher ,之后便是 depIds 和 newDepIds 、 deps 和 newDeps 的值交換,并清空 newDepIds 和 newDeps 。到此完成了對 watcher 的求值操作,同時更新了新的依賴,最后返回 value 即可。
回到 createComputedGetter 接著看:
if (Dep.target) {
watcher.depend()
}
當執(zhí)行計算屬性的 getter 時,有可能表達式中還有別的計算屬性依賴,此時我們需要執(zhí)行 watcher.depend 將當前 watcher 的 deps 添加到 Dep.target 即可。最后返回求得的 watcher.value 即可。
總的來說我們從 this[key] 觸發(fā) watcher 的 get 函數(shù),將當前 watcher 入棧,通過求值表達式將所需要的依賴 dep 收集到 newDepIds 和 newDeps ,并將 watcher 添加到對應 dep 的觀察者列表,最后清除無效 dep 并返回求值結(jié)果,這樣就完成了依賴關系的收集。
Watcher 的更新
以上我們了解了 watcher 的依賴收集和 dep 的觀察者收集的基本原理,接下來我們了解下 dep 的數(shù)據(jù)更新時如何通知 watcher 進行 update 操作。
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.notify 時,我們將 this.subs 拷貝出來,防止在 watcher 的 get 時候 subs 發(fā)生更新,之后調(diào)用 update 方法:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
- 如果是 lazy ,則將其標記為 this.dirty = true ,使得在 this[key] 的 getter 觸發(fā)時進行 watcher.evaluate 調(diào)用計算。
- 如果是 sync 同步操作,則執(zhí)行 this.run ,調(diào)用 this.get 求值和執(zhí)行回調(diào)函數(shù) cb 。
- 否則執(zhí)行 queueWatcher ,選擇合適的位置,將 watcher 加入到隊列去執(zhí)行即可,因為和響應式數(shù)據(jù)無關,故不再展開。
小結(jié)
因為篇幅有限,只對數(shù)據(jù)綁定的基本原理做了基本的介紹,在這畫了一張簡單的流程圖來幫助理解 Vue 的響應式數(shù)據(jù),其中省略了一些 VNode 等不影響理解的邏輯及邊界條件,盡可能簡化地讓流程更加直觀:

最后,本著學習的心態(tài),在寫作的過程中也零零碎碎的查閱了很多資料,其中難免出現(xiàn)紕漏以及未覆蓋到的知識點,如有錯誤,還請不吝指教。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Vue雙向數(shù)據(jù)綁定(MVVM)的原理
- 一文讀懂vue動態(tài)屬性數(shù)據(jù)綁定(v-bind指令)
- vue中的雙向數(shù)據(jù)綁定原理與常見操作技巧詳解
- 原生javascript實現(xiàn)類似vue的數(shù)據(jù)綁定功能示例【觀察者模式】
- Vue表單控件數(shù)據(jù)綁定方法詳解
- vue.js自定義組件實現(xiàn)v-model雙向數(shù)據(jù)綁定的示例代碼
- Vue數(shù)據(jù)綁定實例寫法
- vue3.0中的雙向數(shù)據(jù)綁定方法及優(yōu)缺點
- vue在自定義組件中使用v-model進行數(shù)據(jù)綁定的方法
- vue.js使用v-model實現(xiàn)表單元素(input) 雙向數(shù)據(jù)綁定功能示例
- vue.js的雙向數(shù)據(jù)綁定Object.defineProperty方法的神奇之處
- vue實現(xiàn)的雙向數(shù)據(jù)綁定操作示例
- Vue 數(shù)據(jù)綁定的原理分析
相關文章
一文快速學會阻止事件冒泡的4種方法(原生js阻止,vue中使用修飾符阻止)
冒泡就是事件開始是由最具體的元素接收,然后逐層向上級傳播到較為不具體的元素,這篇文章主要給大家介紹了關于阻止事件冒泡的4種方法,文中介紹的方法分別是原生js阻止以及vue中使用修飾符阻止的相關資料,需要的朋友可以參考下2023-12-12
vue轉(zhuǎn)react useEffect的全過程
這篇文章主要介紹了vue轉(zhuǎn)react useEffect的全過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
vue使用vuedraggable插件實現(xiàn)拖拽效果
這篇文章主要介紹了vue使用vuedraggable插件實現(xiàn)拖拽效果,本文分步驟介紹了安裝vuedraggable插件的方法及頁面引入的方法,需要的朋友可以參考下2024-04-04

