寫在前面:本文爲個人在日常工作和學習中的一些總結,非權威性資料,請帶着自己的思考^-^。
說起響應式,首先會想到Vue實例中的data屬性,例如:對data中的某一屬性重新賦值,如果該屬性用在了頁面渲染上面,則頁面會自動進行重新渲染,這裏就以data作爲切入點,來看一下Vue中的響應式是怎樣的一個實現思路。
Vue實例創建階段
在創建Vue實例的時候,執行到了一個核心方法:initState,該方法會對methods/props/methods/data/computed/watch進行初始化,此時我們只關注data的初始化:
function initData(vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
...
proxy(vm, '_data', key)
...
}
...
observe(data, true)
}
代碼中省略了一些和當前研究的內容無關的代碼,用...表示;
可以看到這個方法主要做了兩件事:
- 將data代理到vm._data,即:訪問data中的屬性key vm[key]將觸發getter,返回vm._data[key],賦值同理;作用是顯而易見的:以後我們想要訪問/賦值data中的某個屬性key時,直接this[key]這樣就可以了,無需this._data[key]這樣
- 執行observe(data, true)函數,遍歷data中的每一個屬性,通過defineObject將其設爲響應式的,即:在正常使用場景中,我們訪問this[key](其中key爲data中的一個屬性)時,會由於1中緣故觸發getter進而訪問this._data[key],這樣就又觸發了當前添加的getter執行某些操作
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)
}
observe代碼:
observe (value: any, asRootData: ?boolean): Observer | void {
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert && // 新添加屬性轉爲reactive
!isServerRendering() && // 非服務端渲染
(Array.isArray(value) || isPlainObject(value)) && // 數組或者對象
Object.isExtensible(value) && // 可擴展對象
!value._isVue // 非Vue實例
) {
ob = new Observer(value) // 創建Observer實例
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this) // value.__ob__ = this, 且__ob__爲不可枚舉屬性
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys) // value.__proto__ = Array.prototype
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive (
obj: Object, // vm instance of Vue
key: string, // '$attr' 等屬性名
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
let childOb = !shallow && observe(val) // 對當前屬性的值繼續進行observe
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
},
set: function reactiveSetter (newVal) {
...
}
})
}
仍然逃脫不了粘貼代碼,但是找不出比代碼更直觀的解釋了...
不過核心方法是defineReactive,它仍然是使用defineProperty對vm._data中的每一個屬性設置了getter/setter,至於getter/setter中的內容,先不去管他。
到了這裏,對於data的初始化已經告一段落。
模板編譯/掛載階段
這裏是Vue.prototype.$mount的執行階段,此階段其實包含了對於模板的編譯、對編譯結果進行轉化生成render函數、render函數的執行進行掛載
這個階段對於data的操作只存在於render函數的執行進行掛載時,核心函數的執行:new Watcher(vm, updateComponent, noop, null, true)
Watcher代碼:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function, // 回調函數
options?: ?Object,
isRenderWatcher?: boolean // render時實例的Watcher
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
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
this.deps = [] // 依賴列表
this.newDeps = [] // 新的依賴列表
this.depIds = new Set() // 依賴ids
this.newDepIds = new Set() // 新的依賴ids
// parse expression for getter
// 將表達式 expOrFn包裝爲getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
* 源碼其實已經給出了註釋,這裏是進行render和依賴收集
*/
get () {
pushTarget(this) // 爲Dep.target賦值爲當前Watcher實例
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 這裏是render函數
} catch (e) {
...
} finally {
if (this.deep) {
traverse(value)
}
popTarget() // 彈出Deptarget
this.cleanupDeps() // 本次添加的依賴落入this.deps,同時清空this.newDeps
}
return value
},
addDep (dep: Dep) { // 將Dep實例添加至this.newDeps隊列中,這裏的Dep實例產生自通過defineReactive爲data屬性定義getter/setter時,也就是說這裏的Dep實例對應一個data屬性
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)
}
}
}
...
}
從上面的代碼可以看出,響應式相關的核心在於所謂的“依賴收集”,也就是在render函數執行的過程中勢必會對頁面渲染需要的data屬性進行讀取,這就觸發了響應data屬性的getter,還記得之前省略掉的observe函數中執行defineReactive函數時有關data屬性getter函數相關的代碼嗎?
defineReactive (
obj: Object, // vm instance of Vue
key: string, // '$attr' 等屬性名
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 在$mount函數中new Watcher進行依賴收集的時候已經爲Dep.target賦值爲Watcher實例
dep.depend() // 這裏的Dep實例對應當前data屬性,此處會將當前dep實例放入watcher的依賴列表中
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// setter相關代碼
...
}
})
}
// dep.depend 代碼:
...
depend () {
if (Dep.target) { // 此處Dep.target已被賦值爲Watcher實例
Dep.target.addDep(this)
}
}
...
頁面首次渲染時的小結
頁面的首次渲染基本上包含上述兩個大的過程,這裏先主要基於data進行討論
new Vue(options)中主要做了:
- 將data函數轉爲data對象賦值給vm._data,此時訪問data屬性可以通過vm._data[key];
- 通過defineProperty進行一層代理,訪問vm[key] 將返回vm._data[key],方便使用;
- 遍歷vm._data,執行defineReactive函數,爲vm._data的每一個屬性添加getter/setter,在getter中進行依賴收集,在setter進行通知響應;
$mount中主要做了:
- 編譯模板/生成渲染render函數;
- 通過實例Watcher對象,在執行render函數時,頁面渲染所用到的data屬性會被訪問,從而觸發vm._data[key]的getter,
在getter中將當前屬性對應的dep實例添加至Watcher實例的deps列表中,同時將Watcher實例添加進dep的subs觀察者列表中;
當data屬性值發生變化時
爲什麼data屬性變化了,頁面會重新渲染得到更新呢?前面做了很多鋪墊,接下來看一下data屬性的變更會進行哪些操作
還記得前面提到得通過defineReactive函數爲vm._data[key]設置得setter嗎?當data變化時會觸發該setter
...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) { // 如果更新前後值相同,則直接返回
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) // 如果newVal爲引用類型,則對其屬性也進行劫持
dep.notify() // 這裏纔是屬性更新觸發操作得核心,它會通知Watcher進行相應得更新
}
...
// dep.notify方法
...
notify () {
const subs = this.subs.slice() // 這裏存放的是觀察者Watcher列表
for (let i = 0, l = subs.length; i < l; i++) { // 通知每一個Watcher,執行其update方法,進行相應更新
subs[i].update()
}
}
...
// Watcher.prototype.update方法
...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) { // 同步更新
this.run()
} else {
queueWatcher(this) // 這個方法是nextTick中去執行Watcher.prototype.run方法,也就是說data屬性更新觸發setter然後通知Watcher去update這個過程通常並非同步執行的,而是會先被放入一個隊列,異步執行,落地到我們使用中:我們不用擔心同時修改多個data屬性帶來嚴重的性能問題,因爲其觸發的更新並非同步執行的;還有一點是Watcher.prototype.run方法中會執行get方法(還記得在首次渲染進行依賴收集的時候有這個方法嗎?)該方法中會執行render進行vnode生成,當然會訪問到data中的屬性,這樣就是一個依賴更新的過程,是不是一個閉環?
}
}
...
queueWatcher(this)
這個方法是nextTick中去執行Watcher.prototype.run方法,也就是說data屬性更新觸發setter然後通知Watcher去update這個過程通常並非同步執行的,而是會先被放入一個隊列,異步執行,落地到我們使用中:我們不用擔心同時修改多個data屬性帶來嚴重的性能問題,因爲其觸發的更新並非同步執行的;
還有一點是Watcher.prototype.run方法中會執行get方法(還記得在首次渲染進行依賴收集的時候有這個方法嗎?)該方法中會執行render進行vnode生成,當然會訪問到data中的屬性,這樣就是一個依賴更新的過程,是不是一個閉環?
另外不能忽略的一點是,在這個方法執行中還會觸發updated 鉤子函數,當然這裏不做深入研究,只做一個大致瞭解,因爲Vue中的細節很多,但是它不影響我們瞭解主要流程。
最後
放兩張在debugg源碼時寫的兩張圖,只有自己能看懂當初想到哪裏。。。
THE END