數(shù)據(jù)驅(qū)動
在我們學(xué)習(xí)Vue.js的過程中,我們經(jīng)??吹饺齻€概念
- 數(shù)據(jù)驅(qū)動
- 數(shù)據(jù)響應(yīng)式
- 雙向數(shù)據(jù)綁定
核心原理分析
- Vue 2.x版本與Vue 3.x版本的響應(yīng)式實現(xiàn)有所不同,我們可以進(jìn)行分別講解
- Vue 2.x響應(yīng)式基于ES5的Object.defineProperty實現(xiàn)
- Vue 3.x響應(yīng)式基于ES6的Proxy實現(xiàn)
回顧defineProperty
我們先定義一個對象
var obj = {
name: 'willam',
age: 18
}
在defineProperty中,第一個參數(shù)為需要進(jìn)行操作的對象,第二個參數(shù)為屬性,第三個為對應(yīng)的操作
Object.defineProperty(obj, 'gender', {
// 值
value: '男',
// 是否可寫
writable: true,
// 控制是否可以枚舉(遍歷
enumerable: true,
// 本次定義之后,再次進(jìn)行重新配置
configurable: true
})
Object.defineProperty(obj, 'gender', {
enumerable: false
})
解釋一下代碼:
賦予值:value
是否可以編輯:writable(這條屬性默認(rèn)值為false,表示只可以讀,不可以寫入)

是否可以枚舉(遍歷):enumerable(這條屬性默認(rèn)值也為false)
for (var k in obj) {
console.log(k, obj[k])
}

在本次定義之后,可否再次進(jìn)行重新配置:configurable:默認(rèn)值為false,true時可以進(jìn)行再次的配置

進(jìn)行屬性操作時,可以通過getter,setter實現(xiàn),訪問器和設(shè)置器,在訪問和設(shè)置時進(jìn)行相應(yīng)的功能設(shè)置
value,writable和get,set無法共存,邏輯沖突
getter指的是:
當(dāng)我們訪問對象的屬性時,會執(zhí)行這個函數(shù)
Object.defineProperty(obj, 'gender', {
get () {
// 甚至可以進(jìn)行額外的操作
console.log('任意需要的自定義操作')
return '男'
},

setter指的是:
當(dāng)我們設(shè)置某個屬性時觸發(fā)的函數(shù)
set (newValue) {
console.log('新的值是',newValue)
this.gender = newValue
}
這樣寫是一個誤區(qū),設(shè)置時觸發(fā)setter,就會造成遞歸

解決辦法:
通過第三方數(shù)據(jù),來存取數(shù)據(jù)
var genderValue = '男'
Object.defineProperty(obj, 'gender', {
get () {
console.log('任意需要的自定義操作')
return genderValue
},
set (newValue) {
console.log('新的值是',newValue)
genderValue = newValue
}
})

模擬Vue2響應(yīng)式原理
- Vue2.x的數(shù)據(jù)響應(yīng)式就是由Object.defineProperty()實現(xiàn)的
- 設(shè)置data之后,遍歷所有的屬性,轉(zhuǎn)換為getter和setter,從而在數(shù)據(jù)變化時進(jìn)行視圖更新操作
我們來寫寫模擬代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始內(nèi)容</div>
<script>
// 聲明一個對象用于進(jìn)行數(shù)據(jù)存儲
let data = {
msg: 'hello'
}
// 模擬一個vue實例
let vm = {}
// 通過數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
Object.defineProperty(vm, 'msg', {
// 可遍歷
enumerable: true,
// 可配置
configurable: true,
// get方法
get () {
console.log('訪問數(shù)據(jù)')
return data.msg
},
// set方法
set (newValue) {
// 更新數(shù)據(jù)
data.msg = newValue
// 數(shù)據(jù)更改,更新視圖中DOM元素內(nèi)容
document.querySelector('#app').textContent = data.msg
}
})
</script>
</body>
</html>
解釋一下代碼,vm的作用就是通過數(shù)據(jù)劫持將data中的數(shù)據(jù)設(shè)置給get與set,并且設(shè)置給vm,最后更改的還是data

改進(jìn)
- 操作中只監(jiān)聽了一個屬性,多個屬性無法處理
- 無法監(jiān)聽數(shù)組變化(Vue里也是同樣存在這個問題)
- 無法處理屬性也為對象的情況
處理多個屬性的情況
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始內(nèi)容</div>
<script>
// 聲明一個對象用于進(jìn)行數(shù)據(jù)存儲
let data = {
msg1: 'hello',
msg2: 'world'
}
// 模擬一個vue實例
let vm = {}
Object.keys(data).forEach(key => {
// 通過數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
// get方法
get () {
console.log('訪問數(shù)據(jù)')
return data[key]
},
// set方法
set (newValue) {
// 更新數(shù)據(jù)
data[key] = newValue
// 數(shù)據(jù)更改,更新視圖中DOM元素內(nèi)容
document.querySelector('#app').textContent = data[key]
}
})
})
</script>
</body>
</html>
這里我們使用到了Object.keys()方法,該方法可以返回一個由內(nèi)部參數(shù)對象的自身可枚舉屬性構(gòu)成的一個數(shù)組,然后我們再將其進(jìn)行forEach遍歷,得到每一個屬性,然后進(jìn)行多個屬性的處理,詳細(xì)邏輯可以通過代碼看的一清二楚
檢測數(shù)組的方法
對數(shù)組的操作是無法實現(xiàn)響應(yīng)式數(shù)據(jù)實現(xiàn)的
Vue通過特定的方法處理可以解決這種問題
- 添加數(shù)組方法支持:
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
- 準(zhǔn)備一個用于存儲處理結(jié)果的對象,準(zhǔn)備替換掉數(shù)組屬性的原型指針
// 存儲處理結(jié)果的對象,準(zhǔn)備替換到數(shù)組數(shù)組實例的原型指針 _proto_
const customProto = {}
- 為了確保原始功能能夠被使用
// 確保原始功能可以使用,this為數(shù)組實例
const result = Array.prototype[method].apply(this, arguments)
- 進(jìn)行其他自定義設(shè)置,比如更新視圖
// 進(jìn)行其他自定義功能設(shè)置,比如,更新視圖
document.querySelector('#app').textContent = this
return result
- 為了避免數(shù)組實例無法再使用我們處理的方法以外的方法:
// 為了避免數(shù)組實例無法再使用其他的數(shù)組方法
customProto.__proto__ = Array.prototype
- 那么如何將這些設(shè)置與攔截寫在一起呢?
答案很簡單:判斷一下就行了
完整代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始內(nèi)容</div>
<script>
// 聲明一個對象用于進(jìn)行數(shù)據(jù)存儲
let data = {
msg1: 'hello',
msg2: 'world',
arr: [1, 2, 3]
}
// 模擬一個vue實例
let vm = {}
// 添加數(shù)組方法的支持
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 存儲處理結(jié)果的對象,準(zhǔn)備替換到數(shù)組數(shù)組實例的原型指針 _proto_
const customProto = {}
// 為了避免數(shù)組實例無法再使用其他的數(shù)組方法
customProto.__proto__ = Array.prototype
arrMethodName.forEach(method => {
customProto[method] = function () {
// 確保原始功能可以使用,this為數(shù)組實例
const result = Array.prototype[method].apply(this, arguments)
// 進(jìn)行其他自定義功能設(shè)置,比如,更新視圖
document.querySelector('#app').textContent = this
return result
}
})
Object.keys(data).forEach(key => {
// 檢測是否為數(shù)組,是的話單獨處理
if (Array.isArray(data[key])) {
// 將當(dāng)前數(shù)組實例的__proto__更換為customProto就行了
data[key].__proto__ = customProto
}
// 通過數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
// get方法
get () {
console.log('訪問數(shù)據(jù)')
return data[key]
},
// set方法
set (newValue) {
// 更新數(shù)據(jù)
data[key] = newValue
// 數(shù)據(jù)更改,更新視圖中DOM元素內(nèi)容
document.querySelector('#app').textContent = data[key]
}
})
})
</script>
</body>
</html>
改進(jìn):封裝與遞歸
使用立即執(zhí)行函數(shù),全部包裹起來,如果對象內(nèi)部還含有對象的話就進(jìn)行遞歸處理,很簡單的邏輯:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始內(nèi)容</div>
<script>
// 聲明數(shù)據(jù)對象,模擬 Vue 實例的 data 屬性
let data = {
msg1: 'hello',
msg2: 'world',
arr: [1, 2, 3],
obj: {
name: 'jack',
age: 18
}
}
// 模擬 Vue 實例的對象
let vm = {}
// 封裝為函數(shù),用于對數(shù)據(jù)進(jìn)行響應(yīng)式處理
const createReactive = (function () {
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const customProto = {}
customProto.__proto__ = Array.prototype
arrMethodName.forEach(method => {
customProto[method] = function () {
const result = Array.prototype[method].apply(this, arguments)
document.querySelector('#app').textContent = this
return result
}
})
// 需要進(jìn)行數(shù)據(jù)劫持的主體功能,也是遞歸時需要的功能
return function (data, vm) {
// 遍歷被劫持對象的所有屬性
Object.keys(data).forEach(key => {
// 檢測是否為數(shù)組
if (Array.isArray(data[key])) {
// 將當(dāng)前數(shù)組實例的 __proto__ 更換為 customProto 即可
data[key].__proto__ = customProto
} else if (typeof data[key] === 'object' && data[key] !== null) {
// 檢測是否為對象,如果為對象,進(jìn)行遞歸操作
vm[key] = {}
createReactive(data[key], vm[key])
return
}
// 通過數(shù)據(jù)劫持的方式,將 data 的屬性設(shè)置為 getter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('訪問了屬性')
return data[key]
},
set (newValue) {
// 更新數(shù)據(jù)
data[key] = newValue
// 數(shù)據(jù)更改,更新視圖中 DOM 元素的內(nèi)容
document.querySelector('#app').textContent = data[key]
}
})
})
}
})()
createReactive(data, vm)
</script>
</body>
</html>
這就是Vue2版本的響應(yīng)式原理分析
回顧Proxy
ES6提供的一個功能,對一個對象提供代理操作
<script>
const data = {
msg1: '內(nèi)容',
arr: [1, 2, 3],
obj: {
name: 'willam',
age: 19
}
}
const P = new Proxy(data, {
get (target, property, receiver) {
console.log(target, property, receiver)
return target[property]
},
set (target, property, value, receiver) {
console.log(target, property, value, receiver)
target[property] = value
}
})
</script>
通過代理,訪問P也就是訪問了data的代理,同樣的數(shù)據(jù),get方法中,target參數(shù)表示原數(shù)據(jù)data,property表示訪問的哪條屬性,receiver表示通過代理之后的數(shù)據(jù)
set方法中新添了一個value參數(shù),表示當(dāng)前設(shè)置的數(shù)值
我們來通過控制臺打印一探究竟

Vue3響應(yīng)式原理
與2版本的區(qū)別為數(shù)據(jù)響應(yīng)式是Proxy實現(xiàn)的,其他相同,接下來進(jìn)行演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始內(nèi)容</div>
<script>
const data = {
msg1: '內(nèi)容',
arr: [1, 2, 3],
content: 'world',
obj: {
name: 'willam',
age: 19
}
}
const vm = new Proxy(data, {
get (target, key) {
return target[key]
},
set (target, key, newValue) {
// 數(shù)據(jù)更新
target[key] = newValue
// 視圖更新
document.querySelector('#app').textContent = target[key]
}
})
</script>
</body>
</html>
對深層監(jiān)控啊,屬性監(jiān)控啊,遍歷啊都不需要在Vue3進(jìn)行操作了,通過Proxy代理可以輕松解決,但是由于ES6的Proxy方法兼容性不是那么的好,所以市面上Vue3的普及度并不是太高,一切走向都需要根據(jù)市場來確定
相關(guān)設(shè)計模式
設(shè)計模式:針對軟件設(shè)計中普遍存在的各種問題所提出的解決方案
觀察者模式
指的是在對象間定義一個一對多(被觀察者與多個觀察者)的關(guān)聯(lián),當(dāng)一個對象改變了狀態(tài),所有其他相關(guān)的對象會被通知并且自動刷新
就像是超市有一堆顧客,超市出了促銷活動,會通知顧客(觀察者),又因為當(dāng)前是否想要購物,進(jìn)行不同的選擇行動
- 核心概念:
- 觀察者Observer
- 被觀察者(觀察目標(biāo))Subject
設(shè)計的核心點就是設(shè)置一個被觀察者,設(shè)置一個或者多個的觀察者,在被觀察者中設(shè)置一個遍歷進(jìn)行操作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 被觀察者(觀察目標(biāo))
// 1.需要能夠添加觀察者
// 2.通知所有觀察者的功能
class Subject {
constructor () {
// 存儲所有的觀察者
this.observers = []
}
// 添加觀察者功能
addObserver (observer) {
// 檢測傳入的參數(shù)是否為觀察者實例
if (observer && observer.update) {
this.observers.push(observer)
}
}
// 通知所有的觀察者
notify () {
// 調(diào)用觀察者列表中的每個觀察者的更新方法
this.observers.forEach(observer => {
observer.update()
})
}
}
// 觀察者
// 1.被觀察者發(fā)生狀態(tài)變化時,做一些對應(yīng)的操作“更新”
class Observer {
update () {
console.log('事件發(fā)生了,進(jìn)行一個相應(yīng)的處理...')
}
}
// 功能測試
const subject = new Subject()
const ob1 = new Observer()
const ob2 = new Observer()
// 將觀察者添加給要觀察的觀察目標(biāo)
subject.addObserver(ob1)
subject.addObserver(ob2)
// 通知觀察者進(jìn)行操作(某些具體的場景下)
subject.notify()
</script>
</body>
</html>
通過觀察者模式為不同的數(shù)據(jù)設(shè)置不同的觀察者,監(jiān)視被觀察者的情況,通過特定的方法進(jìn)行更新操作等等
發(fā)布-訂閱模式
可以認(rèn)為是為觀察者模式的解耦的進(jìn)階版本,特點是:
- 在發(fā)布者和訂閱者之間添加一個消息中心,所有的消息均通過消息中心管理,而發(fā)布者與訂閱者不會直接聯(lián)系,實現(xiàn)了兩張的解耦
核心概念:
- 消息中心Dep
- 訂閱者Subscriber
-
發(fā)布者Publisher
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script>
// 創(chuàng)建了一個Vue實例(消息中心)
const eventBus = new Vue()
// 注冊事件(設(shè)置訂閱者)
eventBus.$on('dataChange', () => {
console.log('事件處理功能1')
})
eventBus.$on('dataChange', () => {
console.log('事件處理功能2')
})
// 觸發(fā)事件(設(shè)置發(fā)布者)
eventBus.$emit('dataChange')
</script>
</body>
設(shè)計模式小結(jié)
- 觀察者模式是由觀察者和觀察目標(biāo)組成的,適合組件內(nèi)部操作(功能簡單就可以)
- 特性:特殊事件發(fā)生后,觀察目標(biāo)統(tǒng)一通知所有的觀察者
- 發(fā)布/訂閱模式由發(fā)布者與訂閱者以及消息中心組成,更加適合消息類型復(fù)雜的情況
- 特性:特殊事件發(fā)生,消息中心接到發(fā)布指令后,會根據(jù)事件類型給對應(yīng)的訂閱者發(fā)送信息
響應(yīng)式原理模擬
整體分析
要模擬Vue實現(xiàn)響應(yīng)式數(shù)據(jù),首先我們需要觀察一下Vue實例的結(jié)構(gòu),分析要實現(xiàn)哪些屬性和功能

- Vue:
- 目標(biāo):將data數(shù)據(jù)注入到Vue實例,便于方法內(nèi)操作
- Observer(發(fā)布者)
- 目標(biāo):數(shù)據(jù)劫持,監(jiān)聽數(shù)據(jù)變化,并在變化時通知Dep
- Dep(消息中心)
- 目標(biāo):存儲訂閱者以及管理消息的發(fā)送
- Watcher(訂閱者)
- 目標(biāo):當(dāng)訂閱數(shù)據(jù)變化,進(jìn)行視圖更新
- Compiler
- 目標(biāo):解析模板中的指令與插值表達(dá)式,并替換成相應(yīng)的數(shù)據(jù)
Vue類
- 功能:
- 接受配置信息
- 將data的屬性轉(zhuǎn)換為Getter、setter,并且注入到Vue實例中
- *監(jiān)聽data中所有屬性的變化,設(shè)置成響應(yīng)式數(shù)據(jù)
-
*調(diào)用解析功能(解析模板內(nèi)的插值表達(dá)式,指令等等)
1.存儲配置選項,2.掛載元素,3.設(shè)置數(shù)據(jù)屬性,最后通過proxyData將data屬性都設(shè)置到vue實例
n _proxyData (target, data) {
Object.keys(data).forEach(key => {
Object.defineProperty(target, key,{
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
data[key] = newValue
}
})
})
}
Observer類
- 功能:
- 通過數(shù)據(jù)劫持方式監(jiān)視data中的屬性變化,變化時通知消息中心Dep
-
需要考慮data的屬性也可能為對象,也要轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
Dep類
- Dep是dependency的簡寫,含義是“依賴”,指的是Dep用于收集與管理訂閱者與發(fā)布者之間的依賴關(guān)系
- 功能:
- *為每個數(shù)據(jù)收集對應(yīng)的依賴,存儲依賴
- 添加并存儲訂閱者
-
數(shù)據(jù)變化時,通知所有的觀察者
Watcher 類
- 功能:
- 實例化Watch時,往dep對象中添加自己
-
當(dāng)數(shù)據(jù)變化觸發(fā)dep,dep通知所有對應(yīng)的Watcher實例更新視圖
Complier類
- 功能:
- 進(jìn)行編譯模板,并解析內(nèi)部指令與插值表達(dá)式
- 進(jìn)行頁面的首次渲染
-
數(shù)據(jù)變化后,重新渲染視圖
功能回顧與總結(jié)
- Vue類
- 把data的屬性注入到Vue實例
- 調(diào)用Observer實現(xiàn)數(shù)據(jù)響應(yīng)式處理
- 調(diào)用Compiler編譯模板
- Observer
- 將data的屬性轉(zhuǎn)換為Getter/setter
- 為Dep添加訂閱者Watcher
- 數(shù)據(jù)變化發(fā)送時通知Dep
- Dep
- 收集依賴,添加訂閱者(Watcher)
- 通知訂閱者
- Watcher
- 編譯模板時創(chuàng)建訂閱者,訂閱數(shù)據(jù)變化
- 接到Dep通知時,調(diào)用Compiler中的模板功能更新視圖
- Compiler
- 編譯模板,解析指令與插值表達(dá)式
-
負(fù)責(zé)頁面首次渲染與數(shù)據(jù)變化后重新渲染
功能總結(jié)圖








