思路分析
資料的雙向繫結,就是資料變化了自動更新檢視,檢視變化了自動更新資料,實際上檢視變化更新資料只要通過事件監聽就可以實現了,並不是資料雙向繫結的關鍵點。關鍵還是資料變化了驅動檢視自動更新。
所有接下來,我們詳細瞭解下資料如何驅動檢視更新的。 資料驅動檢視更新的重點就是,如何知道資料更新了,或者說資料更新了要如何主動的告訴我們。可能大家都聽過,vue的資料雙向繫結原理是Object.defineProperty( )對屬性設定一個set/get,是這樣的沒錯,其實get/set只是可以做到對資料的讀取進行劫持,就可以讓我們知道資料更新了。但是你詳細的瞭解整個過程嗎? 先來看張大家都不陌生的圖:
- Observe 類劫持監聽所有屬性,主要給響應式物件的屬性新增 getter/setter 用於依賴收集與派發更新
- Dep 類用於收集當前響應式物件的依賴關係
- Watcher 類是觀察者,例項分為渲染 watcher、計算屬性 watcher、偵聽器 watcher三種
介紹資料驅動更新之前,先介紹下面4個類和方法,然後從資料的入口initState開始按順序介紹,以下類和方法是如何協作,達到資料驅動更新的。
defineReactive
這個方法,用處可就大了。
我們看到他是給物件的鍵值新增get/set
方法,也就是對屬性的取值和賦值都加了攔截,同時用閉包給每個屬性都儲存了一個Dep
物件。
當讀取該值的時候,就把當前這個watcher
(Dep.target
)新增進他的dep裡的觀察者列表,這個watcher
也會把這個dep
新增進他的依賴列表。
當給設定值的時候,就讓這個閉包儲存的dep
去通知他的觀察者列表的每一個watcher
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
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
if (!getter && arguments.length === 2) {
val = obj[key]
}
const setter = property && property.set
let childOb = !shallow && observe(val)
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) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
複製程式碼
Observer
什麼是可觀察者物件呢?
簡單來說:就是資料變更時可以通知所有觀察他的觀察者。
1、取值的時候,能把要取值的watcher(觀察者物件)加入它的dep(依賴,也可叫觀察者管理器)管理的subs列表裡(即觀察者列表);
2、設定值的時候,有了變化,所有依賴於它的物件(即它的dep裡收集到的觀察者watcher)都得到通知。
這個類功能就是把資料轉化成可觀察物件。針對Object型別就呼叫defineReactive方法迴圈把每一個鍵值都轉化。針對Array,首先是對Array經過特殊處理,使它可以監控到陣列發生了變化,然後對陣列的每一項遞迴呼叫Observer進行轉化。
對於Array是如何處理的呢?這個放在下面單獨說。
export class Observer {
/**
*如果是物件就迴圈把物件的每一個鍵值都轉化成可觀察者物件
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* 如果是陣列就對陣列的每一項做轉化
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製程式碼
Dep
這個類功能簡單來說就是管理資料的觀察者的。當有觀察者讀取資料時,儲存觀察者到subs,以便當資料變化了的時候,可以通知所有的觀察者去update,也可以刪除subs裡的某個觀察者。
export default class Dep {
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 這個方法非常繞,Dep.target就是一個Watcher物件,Watcher把這個依賴加進他的依賴列表裡,然後呼叫dep.addSub再把這個Watcher加入到他的觀察者列表裡。
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()
}
}
}
複製程式碼
Watcher
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省去了初始化各種屬性和option
this.dirty = this.lazy // for lazy watchers
// 解析expOrFn,賦值給this.getter
// expOrFn也要明白他是什麼?
// 當是渲染watcher時,expOrFn是updateComponent,即重新渲染執行render
// 當是計算watcher時,expOrFn是計算屬性的計算方法
// 當是偵聽器watcher時,expOrFn是watch屬性的取值表示式,可以去讀取要watch的資料,this.cb就是watch的handler屬性
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 執行this.getter,同時重新進行依賴收集
*/
get () {
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
if (this.deep) {
// 對於deep的watch屬性,處理的很巧妙,traverse就是去遞迴讀取value的值,
// 就會呼叫他們的get方法,進行了依賴收集
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
/**
* 不重複的把當前watcher新增進依賴的觀察者列表裡
*/
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)
}
}
}
/**
* 清理依賴列表:當前的依賴列表和新的依賴列表比對,存在於this.deps裡面,
* 卻不存在於this.newDeps裡面,說明這個watcher已經不再觀察這個依賴了,所以
* 要讓個依賴從他的觀察者列表裡刪除自己,以免造成不必要的watcher更新。然後
* 把this.newDeps的值賦給this.deps,再把this.newDeps清空
*/
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
}
/**
* 當一個依賴改變的時候,通知它update
*/
update () {
if (this.lazy) {
// 對於計算watcher時,不需要立即執行計算方法,只要設定dirty,意味著
// 資料不是最新的了,使用時需要重新計算
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 排程watcher執行計算。
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
this.cb.call(this.vm, value, oldValue)
}
}
}
/**
* 對於計算屬性,當取值計算屬性時,發現計算屬性的watcher的dirty是true
* 說明資料不是最新的了,需要重新計算,這裡就是重新計算計算屬性的值。
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* 把這個watcher所觀察的所有依賴都傳給Dep.target,即給Dep.target收集
* 這些依賴。
* 舉個例子:具體可以看state.js裡的createComputedGetter這個方法
* 當render裡依賴了計算屬性a,當渲染watcher在執行render時就會去
* 讀取a,而a會去重新計算,計算完了渲染watcher出棧,賦值給Dep.target
* 然後執行watcher.depend,就是把這個計算watcher的所有依賴也加入給渲染watcher
* 這樣,即使data.b沒有被直接用在render上,也通過計算屬性a被間接的是用了
* 當data.b發生改變時,也就可以觸發渲染更新了
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
複製程式碼
綜上所述,就是vue資料驅動更新的方法了,下面是對整個過程的簡單概述: 每個vue例項元件都有相應的watcher物件,這個watcher是負責更新渲染的。他會在元件渲染過程中,把屬性記錄為依賴,也就是說,她在渲染的時候就把所有渲染用到的prop和data都新增進watcher的依賴列表裡,只有用到的才加入。同時把這個watcher加入進data的依賴的訂閱者列表裡。也就是watcher儲存了它都依賴了誰,data的依賴裡儲存了都誰訂閱了它。這樣data在改變時,就可以通知他的所有觀察者進行更新了。渲染的watcher觸發的更新就是重新渲染,後續的事情就是render生成虛擬DOM樹,進行diff比對,將不同反應到真實的DOM中。
queueWatcher
下面是Watcher的update方法,可以看的除了是計算屬性和標記了是同步的情況以外,全部都是推入觀察者佇列中,下一個tick時呼叫。也就是資料變化不是立即就去更新的,而是非同步批量去更新的。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
複製程式碼
下面來看看queueWatcher方法
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
複製程式碼
這裡使用了一個 has 的雜湊map用來檢查是否當前watcher的id是否存在,若已存在則跳過,不存在則就push到queue,佇列中並標記雜湊表has,用於下次檢驗,防止重複新增。因為執行更新佇列時,是每個watcher都被執行run,如果是相同的watcher沒必要重複執行,這樣就算同步修改了一百次檢視中用到的data,非同步更新計算的時候也只會更新最後一次修改。
nextTick(flushSchedulerQueue)
把回撥方法flushSchedulerQueue傳遞給nextTick,一次非同步更新,只要傳遞一次非同步回撥函式就可以了,在這個非同步回撥裡統一批量的處理queue中的watcher,進行更新。
function flushSchedulerQueue () {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
resetSchedulerState()
}
複製程式碼
每次執行非同步回撥更新,就是迴圈執行佇列裡的watcher.run方法。
在迴圈佇列之前對佇列進行了一次排序:
- 元件更新的順序是從父元件到子元件的順序,因為父元件總是比子元件先建立。
- 一個元件的user watchers(偵聽器watcher)比render watcher先執行,因為user watchers往往比render watcher更早建立
- 如果一個元件在父元件watcher執行期間被銷燬,它的watcher執行將被跳過
nextTick
export function nextTick (cb?: Function, ctx?: Object) {
// 這個方法裡,我把關於不寫回撥,使用promise的情況處理去掉了,把trycatch都去掉了。
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
setTimeout(flushCallbacks, 0) // 非同步任務進行了簡化
}
}
複製程式碼
下面是非同步的回撥方法flushCallbacks,遍歷執行callbacks裡的方法,也就是遍歷執行呼叫nextTick時傳入的回撥方法。
你可能就要問了,queueWatcher的時候不是控制了只會呼叫一次nextTick嗎,為啥要用callbacks陣列來儲存呢。舉個例子:
你寫了一堆同步語句,改變了data等,然後又呼叫了一個this.$nextTick來做個非同步回撥,這個時候不就又會向callbacks陣列裡push了一個回撥方法嗎。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
複製程式碼
如何把陣列處理成可觀察物件
不考慮相容處理
本質就是改寫陣列的原型方法。當陣列呼叫methodsToPatch這些方法時,就意味者陣列發生了變化,需要通知所有觀察者update。
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (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
})
})
複製程式碼
後記 關於從資料入口initState開始解析的部分,寫在一篇裡篇幅太大,我放在下一篇文章了,記得去讀哦,可以加深理解。
參考文章