vue源碼解讀-數(shù)據(jù)響應(yīng)式原理(通俗易懂)
一、Object 的變化偵測
1.1 API 的引入
1.1.1 Object.defineProperty()
在vue2.x中,我們經(jīng)常遇到當(dāng)數(shù)據(jù)的值改變之后,該值在頁面上被引用的部分也會更新這種情況。那么今天我們就來解開這神奇的面紗。
Object.defineProperty()可以用于監(jiān)聽某一對象對應(yīng)的屬性,監(jiān)聽類型主要分為值特性和訪問器特性。它就是vue2.x響應(yīng)式數(shù)據(jù)實現(xiàn)的基本原理。
1.1.2 值特性的配置
目標(biāo)屬性是否允許被刪除、遍歷訪問、覆蓋和該屬性的值的配置。
let data = { }
Object.defineProperty(data, 'age', {
configurable: false, // 被刪除時,靜默失敗
writable: false, // 被重寫時,靜默失敗
enumerable: false, // 不可以枚通過for/in進(jìn)行枚舉
value: 23,// 該屬性的值
})
console.log(data.age); //23
delete data.age // 靜默失敗
console.log(data.age); //23
data.age=30 // 靜默失敗
console.log(data.age); //23
for (const key in data) {
console.log(key,'key'); // 靜默失敗 age屬性不能被訪問!
}
1.1.3 訪問器特性的配置
目標(biāo)屬性在被訪問和賦值等操作完成之前進(jìn)行劫持。
let person = {
_name:'Jone'
}
Object.defineProperty(person, 'name', {
get: () => {
console.log('GET');
return person._name
},
set: (value) => {
console.log('SET');
person._name=value
}
})
console.log(person.name); // 'GET' 'Jone'
person.name='Mike' // 'SET'
通過上面的代碼,我們疑惑為什么不配置一個value值屬性而是要借助一個第三者屬性_name完成呢?這個問題官方給出了解釋:
訪問器屬性不能和值屬性中的(writable和value)同時配置。
我們深思一下:如果說我為一個屬性即配置了value屬性又為他配置了get訪問器屬性。那么當(dāng)我們訪問該屬性的時候,是以get訪問器為準(zhǔn)還是以value為準(zhǔn)呢?
1.2 如何實現(xiàn)數(shù)據(jù)的劫持
如果要對 data 中數(shù)據(jù)進(jìn)行深層次的劫持,我們可以使用 深度優(yōu)先搜索算法 實現(xiàn):
如果當(dāng)前的鍵指向值的類型為基本數(shù)據(jù)類型,則使用 Object.defineProperty() 這一個 API 實現(xiàn)數(shù)據(jù)的劫持。
如果當(dāng)前的鍵指向值的類型為復(fù)雜數(shù)據(jù)類型中的 Object類型,則需要將這個 Object 中的鍵值對進(jìn)行劫持。
1.3 實現(xiàn)數(shù)據(jù)的劫持
class Vue {
constructor(rest) {
let { data, watch } = rest
this.$data = typeof data === 'function' ? data() : data
this.initData(this.$data)
// 開始遞歸
this.observe(this.$data)
}
}
function observe(data) {
new Observer(data)
}
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
for (const key in data) {
reactive(data, key, data[key])
}
}
}
function reactive(object, key, val) {
let isArray = val instanceof Array
let isObject = val instanceof Object
// 如果鍵指向的對象為數(shù)組類型,本小節(jié)暫不處理。
if(isArray) return
// 如果鍵指向的對象為對象類型,則再對該對象進(jìn)行遞歸。
if (isObject) {
return observe(val)
}
// 數(shù)據(jù)的劫持操作
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() {
return val
},
set(value) {
if (val !== value) {
val = value
}
}
})
}
過上述代碼,我們不難發(fā)現(xiàn):遞歸邏輯在實現(xiàn)的過程中,并不是函數(shù)自身的調(diào)用,而是將三個函數(shù)首位相接完成了遞歸的邏輯。下圖是對三個函數(shù)實現(xiàn)遞歸邏輯的展示:

1.4 為何需要進(jìn)行依賴收集
在上文中我們對數(shù)據(jù)實現(xiàn)了劫持操作,如果只是劫持?jǐn)?shù)據(jù)其實并沒有什么作用,因為我們需要的功能是當(dāng)數(shù)據(jù)變化后對引用該數(shù)據(jù)的部分進(jìn)行更新操作,所以我們還需要知道以下兩個內(nèi)容:
響應(yīng)式數(shù)據(jù)在何處被引用(模板還是計算屬性還是其他地方)。
當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化后,通知引用該數(shù)據(jù)的部分進(jìn)行更新。
1.5 在何處收集、在何時通知更新
1.5.1 何處收集
舉個例子:
<template>
<p>{{ name }}</p>
</template>
該模板中引用了響應(yīng)式數(shù)據(jù) name 的值。換句話說:該模板中首先訪問響應(yīng)式數(shù)據(jù) name 屬性的值,再將對應(yīng)的值放在模板對應(yīng)的位置。所以,當(dāng) name 屬性被訪問的時候,在被訪問的位置打上標(biāo)注。換言之:該位置依賴了 name 屬性的值,需要將這部分邏輯收集起來。
恰巧, API 中 get()訪問器的用處不就是當(dāng)數(shù)據(jù)被訪問時,進(jìn)行攔截操作嗎?所以我們應(yīng)該在 get()函數(shù)中進(jìn)行依賴收集這個動作。
1.5.2 何時通知更新
當(dāng)響應(yīng)式數(shù)據(jù)的值發(fā)生變化之后,引用該數(shù)據(jù)的邏輯部分應(yīng)當(dāng)更新。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,我們在哪里得知呢?是不是可以在 set()訪問器進(jìn)行通知更新呢?
1.6 收集依賴的介紹
經(jīng)過分析,我們知道需要在 get() 函數(shù)中進(jìn)行依賴的收集。那么收集到的依賴存放到哪里呢?我們是不是考慮將依賴存放到一個數(shù)組中或者一個對象中呢?
function reactive(object, key, val) {
let dep = []
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() {
// 依賴收集處,Dep.target 將他看做依賴。
dep.push(Dep.target)
return val
},
set(value) {
if (val !== value) {
// 依賴的觸發(fā)
dep.forEach(cb=>cb())
val = value
}
}
})
}
通過上述代碼,我們新增一個數(shù)組dep,用于存放被收集的依賴。值得注意的是,由于 get() 和 set()函數(shù)中均引用了其父級作用域中聲明的變量 dep,形成了閉包。
但是這樣寫耦合度較低,我們可以封裝一個單獨的Dep類讓它專門負(fù)責(zé)依賴收集。
class Dep {
constructor() {
// 依賴收集的中心
this.subs = []
}
// 依賴的收集
add() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
// 觸發(fā)依賴對應(yīng)的回調(diào)函數(shù)
update() {
let subs = this.subs.slice()
subs.forEach(watch => {
// 觸發(fā)依賴的回調(diào)函數(shù)
watch.run()
})
}
}
然后我們改造一下依賴收集的動作對應(yīng)的函數(shù)。
function reactive(object, key, val) {
let dep = new Dep()
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() {
// 依賴收集處
dep.append()
return val
},
set(value) {
if (val !== value) {
// 依賴的觸發(fā)
dep.update(value)
val = value
}
}
})
}
1.7 依賴的介紹
1.7.1 依賴是什么
當(dāng)響應(yīng)式數(shù)據(jù)被訪問的時,收集 誰。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,通知 誰進(jìn)行更新;這個誰就是依賴。由于響應(yīng)式數(shù)據(jù)既有可能在模板中被引用,也有可能被引用在 computed 中,所以我們不妨封裝一個類實例,當(dāng)需要收集的時候,直接收集該實例。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時候,也只通知他一個,再有他通知其他地方進(jìn)行更新。我們?yōu)檫@個實例起個名字吧,叫 Watcher 。
1.7.2 依賴函數(shù)封裝
當(dāng)響應(yīng)式數(shù)據(jù)被訪問時,我們需要實例化一個對象,這個對象被收集的目標(biāo)。當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,該實例對象需要通知引用部分進(jìn)行更新。
// 依賴構(gòu)造函數(shù)
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
this.get()
}
get() {
// 依賴收集的對象
Dep.target = this
// Object.defineProperty 中的 get 函數(shù)會被調(diào)用。調(diào)用之后,依賴進(jìn)行收集。
this.vm[this.key]
Dep.target = undefined
}
run() {
// 通知更新的能力
// 為了防止this指針出現(xiàn)錯誤,我們重新綁定this指向。
this.cb.apply(this.vm)
}
}
1.8 模擬實現(xiàn) vue 中的 watch 選項
在 vue 中,提供了一個 watch 偵聽器選項,它的功能是當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,執(zhí)行對應(yīng)的回調(diào)函數(shù)。結(jié)合之前的邏輯,我們封裝一個屬于我們的 watch 選項。
import { arrayProto } from './array.js'
class Vue {
constructor(rest) {
let { data, watch } = rest
this.$data = typeof data === 'function' ? data() : data
// 初始化響應(yīng)式數(shù)據(jù)
this.initData(this.$data)
for (const key in this.$data) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return this.$data[key]
},
set(value) {
this.$data[key] = value
}
})
}
// 初始化所有的偵聽器
this.initWatch(watch)
}
initData = () => {
observe(this.$data)
}
initWatch(watch) {
for (const key in watch) {
this.$watch(key, watch[key])
}
}
// 響應(yīng)式數(shù)據(jù)在在偵聽器中被引用
$watch(key, cb) {
new Watcher(this, key, cb)
}
}
// 依賴的收集容器
class Dep {
constructor() {
this.deps = []
}
// 收集
append() {
if (Dep.target) {
this.deps.push(Dep.target)
}
}
// 觸發(fā)
update(newValue) {
let subs = this.deps.slice()
subs.forEach(watch => {
watch.run(newValue)
})
}
}
// 依賴
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.cb = cb
this.key = key
this.get()
}
get() {
Dep.target = this
this.vm[this.key]
Dep.target = undefined
}
run(newValue) {
// 將變化前和變化后的值傳給對應(yīng)的回調(diào)函數(shù)。
this.cb.call(this.vm, this.vm[this.key], newValue)
}
}
// 遞歸實現(xiàn)
export function observe(data) {
if (typeof data !== 'object') { return }
new Observer(data)
}
class Observer {
constructor(data) {
if (Array.isArray(data)) {
// 數(shù)組需要單獨處理
} else {
this.walk(data)
}
}
walk(data) {
for (const key in data) {
if (typeof data[key] === 'object') {
observe(data[key])
}
reactive(data, key, data[key])
}
}
}
function reactive(object, key, val) {
let dep = new Dep()
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() {
// 依賴收集處
dep.append()
return val
},
set(value) {
if (val !== value) {
// 依賴的觸發(fā)
dep.update(value)
val = value
}
}
})
}
// 測試數(shù)據(jù)
let vm = new Vue({
data() {
return {
name: 'zs',
age: 23,
sex: 'nan',
hobby: [1, [2], 3, 4]
}
},
watch: {
age() {
console.log('age變化了,哈哈哈');
},
hobby() {
console.log('hobby變化了,hobby', this.hobby);
},
}
})
vm.name = 'll' // name變化了,hhh
console.log(vm, 'vv');
vm.$data.hobby.push()
二、Array 的變化偵測
2.1 如何實現(xiàn)數(shù)據(jù)的劫持
如果要對數(shù)組中的每一個元素實現(xiàn)數(shù)據(jù)的劫持,我們依然可以通過 Object.defineProperty() 這個內(nèi)置的API遞歸實現(xiàn)。但是,如果用戶聲明了一個擁有100個元素的數(shù)組,那在對該數(shù)組進(jìn)行遞歸劫持時,是不是會占用大量的內(nèi)存呢?所以,vue在對數(shù)組進(jìn)行劫持操作時,并沒有采用這種方法。
所謂數(shù)組的劫持,通俗的來說就是當(dāng)數(shù)組中的元素被訪問(或者被修改)時,外界可以感知到。
當(dāng)數(shù)組中的元素被訪問時,指向數(shù)組的變量名一定會收到通知,所以我們依舊在 get 中實現(xiàn)劫持?jǐn)?shù)組被訪問的操作。那如何實現(xiàn)當(dāng)數(shù)組中的元素被修改時也能被外界感知這一功能呢?我們是不是可以通過劫持能夠改變數(shù)組中元素的實例方法完成呢?
2.2 數(shù)組的劫持
經(jīng)過上文得知,我們需要對原型對象中能夠改變數(shù)組中數(shù)據(jù)的7個實例方法進(jìn)行劫持操作。既要實現(xiàn)數(shù)組的劫持,又要完成其對應(yīng)的功能。
const ArrayProto = Array.prototype
export const array = Object.create(ArrayProto)
// 被劫持的 7 個方法
let methods = [ 'push', 'pop','shift', 'unshift','splice','sort', 'reverse']
// 對7個方法進(jìn)行劫持操作
methods.forEach(method => {
array[method] = function (...arg) {
// 原實例對象中的7個實例方法的功能也需要進(jìn)行實現(xiàn)。
let result = ArrayProto[method].apply(this, arg)
// 對于新增加入的數(shù)據(jù),需要進(jìn)行響應(yīng)式處理
let data
switch (method) {
case 'push':
case 'unshift':
data = arg
break
case 'splice':
data = arg[2]
}
data && data.forEach(v => {
observe(v)
})
return result
}
})
下圖是對代碼邏輯的詳細(xì)展示:

2.3 在何處收集、在何時通知更新
對于 Object 類型而言,當(dāng) Object 的鍵被訪問時,將依賴進(jìn)行收集;當(dāng) Object 的鍵對應(yīng)的值發(fā)生變化時,通知更新。所以我們在 get() 和 set() 函數(shù)中分別進(jìn)行依賴的收集和通知更新。
那數(shù)組也是如此,在 get() 中進(jìn)行依賴的收集,當(dāng)數(shù)組中的元素發(fā)生變化時通知更新。在元素發(fā)生變化通知更新非常容易理解,為什么依賴收集還是在 get() 中呢?我們舉個例子:
{
list: [1, 2, 3, 4, 5, 6]
}
對于上面的數(shù)組 list ,如果我們想要得到數(shù)組中的任意一個值,一定是需要經(jīng)過 list 這個 key 的。對不對?所以,我們在獲取數(shù)組 list 中的元素時,對應(yīng)的 get() 一定會被觸發(fā)。若數(shù)組使用我們改寫的那七個實例方法,那么需要在那七個實例方法中進(jìn)行通知更新。
function reactive(object, key, val) {
let dep = new Dep()
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() {
dep.append()
// 數(shù)組的依賴收集處
return val
},
set(value) {
if (val !== value) {
dep.update(value)
val = value
}
}
})
}
methods.forEach(method => {
arrayProto[method] = function (...arg) {
// 數(shù)組原來的方法也必須映射過來
let result = ArrayProto[method].apply(this, arg)
let data
switch (method) {
case 'push':
case 'unshift':
data = arg
break
case 'splice':
data = arg[2]
}
data && data.forEach(v => {
observe(v)
})
// 數(shù)組元素發(fā)生變化,通知對應(yīng)的依賴進(jìn)行更新
}
})
2.4 依賴的收集和觸發(fā)
2.4.1 收集的依賴存在何處
在 Object 而言,依賴收集中心是放在一個 dep 中,那數(shù)組收集的依賴能不能也放到 dep 中呢?
function reactive(object, key, val) {
// 我們維護(hù)的的依賴收集中心,對于 Object 而言。
let dep = new Dep()
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() {
// 收集
dep.append()
return val
},
set(value) {
if (val !== value) {
// 通知更新
dep.update(value)
val = value
}
}
})
}
由于 Object 的依賴收集和通知更新處于同一個作用域中,利用函數(shù)閉包中的數(shù)據(jù)在函數(shù)運行完畢之后不會被垃圾回收的特性,我們在 get() 函數(shù)和 set() 函數(shù)父級作用域維護(hù)了一個依賴中心(dep)。這樣 dep 就能常駐內(nèi)存。
經(jīng)過上節(jié)分析,數(shù)組的依賴的收集是在 get() 函數(shù)中,然而數(shù)組的通知更新是在攔截器中進(jìn)行的。所以我們是不是可以仿照 Object 依賴收集和通知更新的方式,在其父作用域維護(hù)一個依賴中心呢?這個位置恰恰在 Observer 類。
import { arrayProto } from './array.js'
class Observer {
constructor(data) {
// 數(shù)組的依賴中心放在這里是不是更合適呢?
this.dep = new Dep()
if (Array.isArray(data)) {
// 理由1
// 如果為數(shù)組,則將我們寫好的原型對象覆蓋數(shù)組原來的原型對象。
// 我們寫好的原型對象中需要訪問到依賴中心。
data.__proto__ = arrayProto
} else {
this.walk(data)
}
}
walk(data) {
// 省略......
// 理由2
// 為 Object 的鍵進(jìn)行劫持,在這個函數(shù)中需要訪問到依賴中心。
reactive(data, key, data[key])
// 省略.....
}
}
2.4.2 收集依賴
經(jīng)過上文分析可以得知,對于數(shù)組而言,依賴的收集應(yīng)該在 get() 函數(shù)中進(jìn)行。觸發(fā)依賴的更新是在 攔截器 方法中進(jìn)行。
在這里我們需要先思考幾個問題:
如果該數(shù)據(jù)已經(jīng)被劫持了,需要被二次劫持嗎?這個功能寫在哪里比較合適呢?
如何知道該數(shù)據(jù)已經(jīng)被劫持了?
回答:
肯定不需要。我們可以將判斷目標(biāo)數(shù)據(jù)是否已經(jīng)被劫持這部分邏輯寫在 observe() 函數(shù)中。請思考為什么?因為 observe() 函數(shù)是遞歸實現(xiàn)數(shù)據(jù)劫持的第一個被調(diào)用的函數(shù)。換言之:如果要將某個數(shù)據(jù)實現(xiàn)被劫持,一定需要調(diào)用 observe() 函數(shù)。比如以下代碼。
methods.forEach(method => {
arrayProto[method] = function (...arg) {
// 。。。。。。省略部分代碼
data && data.forEach(v => {
// 將新加入數(shù)組的元素遞歸實現(xiàn)劫持。
observe(v)
})
}
})
我們可以在被劫持的數(shù)據(jù)中,新增一個唯一的標(biāo)識。請思考應(yīng)該在哪里為數(shù)據(jù)添加這個唯一標(biāo)識呢?應(yīng)該是 Observer 類。因為如果 Observer 類能夠被執(zhí)行,那么 data 一定是復(fù)雜數(shù)據(jù)類型。我們可以先為 data 打上唯一標(biāo)識,然后再對 data 這個復(fù)雜數(shù)據(jù)類型進(jìn)行遍歷。遍歷過程中如果值是復(fù)雜數(shù)據(jù)類型,則值部分的數(shù)據(jù)進(jìn)行遞歸劫持,如果為簡單數(shù)據(jù)類型,則進(jìn)入 reactive() 函數(shù),進(jìn)行劫持操作!
// 為每一個響應(yīng)式數(shù)據(jù)添加__ob__屬性
function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value: value,
enumerable: enumerable || false,
writable: true,
configurable: true
})
}
// 遞歸開始函數(shù)
export function observe(data) {
// 如果不是復(fù)雜數(shù)據(jù)類型,直接返回
if (typeof data !== 'object') { return }
// 目標(biāo)數(shù)據(jù)屬性中,是否存在我們規(guī)定的唯一標(biāo)識 __ob__。
if (Object.hasOwn(data, '__ob__') && data['__ob__'] instanceof Observer) {
// 如果存在則不進(jìn)行二次監(jiān)聽
return
} else {
// 如果不存在。說明該數(shù)據(jù)暫未劫持。
new Observer(data)
}
}
class Observer {
constructor(data) {
this.dep=new Dep()
// 唯一標(biāo)識
def(data, '__ob__', this)
if (Array.isArray(data)) {
data.__proto__ = arrayProto
}
}
walk(data) {
for (const key in data) {
// 如果值為復(fù)雜類型,則需要將值部分進(jìn)行遞歸劫持。
if (typeof data[key] === 'object') {
observe(data[key])
}else{
reactive(data, key, data[key])
}
}
}
}
function reactive(object, key, val) {
// 。。。。。。省略
// 進(jìn)行劫持操作
}
2.4.3 在攔截器中訪問到依賴中心
經(jīng)過上文的分析,我們不難發(fā)現(xiàn):在 Observer 類中,我們?yōu)楫?dāng)前的復(fù)雜數(shù)據(jù)類型全部新增加一個__ob__的屬性,并且其值為當(dāng)前 Observer 當(dāng)前實例對象。但是,Observer 實例對象中是不是存在一個依賴收集中心呢?這個依賴收集中心是不是為了收集數(shù)組的依賴而設(shè)置的呢?
因為我們?yōu)槊恳粋€復(fù)雜數(shù)據(jù)類型中新注入了一個能訪問到數(shù)組依賴中心的屬性,所以在攔截器中只要獲取到數(shù)組實例對象的__ob__屬性,就能拿到數(shù)組的依賴中心。
那問題又來了:我們是不是有需要寫個方法去獲取數(shù)組中的__ob__屬性啊?能不能復(fù)用已有的函數(shù)呢?
答案是:肯定可以??v觀我們封裝過得所有函數(shù),不難發(fā)現(xiàn),引用__ob__屬性較多的有 observe() 函數(shù)和
Observer 類。我們是在 Observer 類中對復(fù)雜數(shù)據(jù)類型添加__ob__屬性的,所以我們考慮二次改變observe()
函數(shù)。在攔截器中,獲取該數(shù)組__ob__屬性指向的對象。
// 改造
export function observe(data) {
if (typeof data !== 'object') {
return
}
let ob
if (Object.hasOwn(data, "__ob__") && data instanceof Observer) {
// 如果存在屬性__ob__,則返回其值。
ob = data['__ob__']
} else {
// 當(dāng)然我們也需要考慮到兼容之前的邏輯。如果該復(fù)雜類型沒有屬性__ob__,
// 那就證明該復(fù)雜類型的數(shù)據(jù)還沒有進(jìn)行響應(yīng)式劫持操作,需要進(jìn)入進(jìn)入下一個
// 環(huán)節(jié),對復(fù)雜數(shù)據(jù)類型進(jìn)行劫持。
ob = new Observer(data)
}
return ob
}
class Observer {
constructor(data) {
// data 一定是復(fù)雜數(shù)據(jù)類型
// 依賴中心
this.dep = new Dep()
// 為復(fù)雜數(shù)據(jù)類型手動添加 __ob__ 屬性。
// 數(shù)組的實例對象中已經(jīng)擁有了依賴中心
def(data, '__ob__', this)
if (Array.isArray(data)) {
// 將被改寫的原型對象覆蓋數(shù)組自帶的原型對象
data.__proto__ = arrayProto
}
// 省略 ......
}
}
// 省略......
}
// 我們給每一個響應(yīng)式數(shù)據(jù)添加__ob__屬性,
export function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value: value,
enumerable: enumerable || false,
writable: true,
configurable: true
})
}
我們通過 def() 函數(shù)對每一個復(fù)雜數(shù)據(jù)類型的數(shù)據(jù)手動添加了一個__ob__屬性,__ob__的值為當(dāng)前 Observer 實例對象,這個實例對象中擁有一個依賴中心 —— dep。從此數(shù)組的實例屬性中增加了一個__ob__。
在被改寫的 7 個實例方法中,我們可以通過 this 獲取數(shù)組中的 __ob__屬性得到數(shù)組的依賴中心,從而進(jìn)行通知!
methods.forEach(method => {
arrayProto[method] = function (...arg) {
// 數(shù)組原來的方法也必須映射過來
let result = ArrayProto[method].apply(this, arg)
let data
switch (method) {
case 'push':
case 'unshift':
data = arg
break
case 'splice':
data = arg[2]
}
data && data.forEach(v => {
observe(v)
})
// 我們把數(shù)組的依賴中心掛在到了數(shù)組實例屬性中,通過 this 可以獲取依賴中心。
this.__ob__.dep.update(...arg)
return result
}
})
2.5 偵測數(shù)組中元素的變化
如果當(dāng)前的元素屬于數(shù)組類型,除了改變其原型對象之外,我們可以通過遍歷該數(shù)組中的元素進(jìn)行深層次的偵測。
class Observer {
constructor(data) {
this.dep = new Dep()
def(data, '__ob__', this)
if (Array.isArray(data)) {
// 改變數(shù)組的原型對象
data.__proto__ = arrayProto
// 如果是數(shù)組,則遍歷它。
this.addressArray(data)
} else {
this.walk(data)
}
}
addressArray(data) {
for (const v of data) {
// 對數(shù)組中的值進(jìn)行深層度的遞歸
observe(v)
}
}
// 省略......
}
2.6 偵測新增元素的變化
對于新加入數(shù)組的元素,我們非常有必要對他們進(jìn)行監(jiān)聽。
methods.forEach(method => {
arrayProto[method] = function (...arg) {
// 數(shù)組原來的方法也必須映射過來
let result = ArrayProto[method].apply(this, arg)
let data
switch (method) {
case 'push':
case 'unshift':
data = arg
break
case 'splice':
data = arg[2]
}
// data && data.forEach(v => {
// observe(v)
// })
// 修改檢測方式
this.__ob__.addressArray(arg)
// 觸發(fā)依賴更新
this.__ob__.dep.update(...arg)
return result
}
})
