數(shù)據(jù)驅(qū)動(dòng)
在我們學(xué)習(xí)Vue.js的過(guò)程中蒜绽,我們經(jīng)橙焓担看到三個(gè)概念
- 數(shù)據(jù)驅(qū)動(dòng)
- 數(shù)據(jù)響應(yīng)式
- 雙向數(shù)據(jù)綁定
核心原理分析
- Vue 2.x版本與Vue 3.x版本的響應(yīng)式實(shí)現(xiàn)有所不同郊丛,我們可以進(jìn)行分別講解
- Vue 2.x響應(yīng)式基于ES5的Object.defineProperty實(shí)現(xiàn)
- Vue 3.x響應(yīng)式基于ES6的Proxy實(shí)現(xiàn)
回顧defineProperty
我們先定義一個(gè)對(duì)象
var obj = {
name: 'willam',
age: 18
}
在defineProperty中念赶,第一個(gè)參數(shù)為需要進(jìn)行操作的對(duì)象忆绰,第二個(gè)參數(shù)為屬性闸与,第三個(gè)為對(duì)應(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時(shí)可以進(jìn)行再次的配置
進(jìn)行屬性操作時(shí)忽洛,可以通過(guò)getter,setter實(shí)現(xiàn)环肘,訪問(wèn)器和設(shè)置器欲虚,在訪問(wèn)和設(shè)置時(shí)進(jìn)行相應(yīng)的功能設(shè)置
value,writable和get悔雹,set無(wú)法共存复哆,邏輯沖突
getter指的是:
當(dāng)我們?cè)L問(wèn)對(duì)象的屬性時(shí),會(huì)執(zhí)行這個(gè)函數(shù)
Object.defineProperty(obj, 'gender', {
get () {
// 甚至可以進(jìn)行額外的操作
console.log('任意需要的自定義操作')
return '男'
},
setter指的是:
當(dāng)我們?cè)O(shè)置某個(gè)屬性時(shí)觸發(fā)的函數(shù)
set (newValue) {
console.log('新的值是',newValue)
this.gender = newValue
}
這樣寫是一個(gè)誤區(qū)腌零,設(shè)置時(shí)觸發(fā)setter梯找,就會(huì)造成遞歸
解決辦法:
通過(guò)第三方數(shù)據(jù),來(lái)存取數(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()實(shí)現(xiàn)的
- 設(shè)置data之后益涧,遍歷所有的屬性锈锤,轉(zhuǎn)換為getter和setter,從而在數(shù)據(jù)變化時(shí)進(jìn)行視圖更新操作
我們來(lái)寫寫模擬代碼:
<!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>
// 聲明一個(gè)對(duì)象用于進(jìn)行數(shù)據(jù)存儲(chǔ)
let data = {
msg: 'hello'
}
// 模擬一個(gè)vue實(shí)例
let vm = {}
// 通過(guò)數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter久免,并且設(shè)置給vm
Object.defineProperty(vm, 'msg', {
// 可遍歷
enumerable: true,
// 可配置
configurable: true,
// get方法
get () {
console.log('訪問(wèn)數(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的作用就是通過(guò)數(shù)據(jù)劫持將data中的數(shù)據(jù)設(shè)置給get與set妄壶,并且設(shè)置給vm摔握,最后更改的還是data
改進(jìn)
- 操作中只監(jiān)聽(tīng)了一個(gè)屬性寄狼,多個(gè)屬性無(wú)法處理
- 無(wú)法監(jiān)聽(tīng)數(shù)組變化(Vue里也是同樣存在這個(gè)問(wèn)題)
- 無(wú)法處理屬性也為對(duì)象的情況
處理多個(gè)屬性的情況
<!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>
// 聲明一個(gè)對(duì)象用于進(jìn)行數(shù)據(jù)存儲(chǔ)
let data = {
msg1: 'hello',
msg2: 'world'
}
// 模擬一個(gè)vue實(shí)例
let vm = {}
Object.keys(data).forEach(key => {
// 通過(guò)數(shù)據(jù)劫持的方式丁寄,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
// get方法
get () {
console.log('訪問(wèn)數(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()
方法伊磺,該方法可以返回一個(gè)由內(nèi)部參數(shù)對(duì)象的自身可枚舉屬性構(gòu)成的一個(gè)數(shù)組,然后我們?cè)賹⑵溥M(jìn)行forEach遍歷删咱,得到每一個(gè)屬性屑埋,然后進(jìn)行多個(gè)屬性的處理,詳細(xì)邏輯可以通過(guò)代碼看的一清二楚
檢測(cè)數(shù)組的方法
對(duì)數(shù)組的操作是無(wú)法實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)實(shí)現(xiàn)的
Vue通過(guò)特定的方法處理可以解決這種問(wèn)題
- 添加數(shù)組方法支持:
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
- 準(zhǔn)備一個(gè)用于存儲(chǔ)處理結(jié)果的對(duì)象痰滋,準(zhǔn)備替換掉數(shù)組屬性的原型指針
// 存儲(chǔ)處理結(jié)果的對(duì)象摘能,準(zhǔn)備替換到數(shù)組數(shù)組實(shí)例的原型指針 _proto_
const customProto = {}
- 為了確保原始功能能夠被使用
// 確保原始功能可以使用,this為數(shù)組實(shí)例
const result = Array.prototype[method].apply(this, arguments)
- 進(jìn)行其他自定義設(shè)置,比如更新視圖
// 進(jìn)行其他自定義功能設(shè)置敲街,比如团搞,更新視圖
document.querySelector('#app').textContent = this
return result
- 為了避免數(shù)組實(shí)例無(wú)法再使用我們處理的方法以外的方法:
// 為了避免數(shù)組實(shí)例無(wú)法再使用其他的數(shù)組方法
customProto.__proto__ = Array.prototype
- 那么如何將這些設(shè)置與攔截寫在一起呢?
答案很簡(jiǎ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>
// 聲明一個(gè)對(duì)象用于進(jìn)行數(shù)據(jù)存儲(chǔ)
let data = {
msg1: 'hello',
msg2: 'world',
arr: [1, 2, 3]
}
// 模擬一個(gè)vue實(shí)例
let vm = {}
// 添加數(shù)組方法的支持
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 存儲(chǔ)處理結(jié)果的對(duì)象多艇,準(zhǔn)備替換到數(shù)組數(shù)組實(shí)例的原型指針 _proto_
const customProto = {}
// 為了避免數(shù)組實(shí)例無(wú)法再使用其他的數(shù)組方法
customProto.__proto__ = Array.prototype
arrMethodName.forEach(method => {
customProto[method] = function () {
// 確保原始功能可以使用,this為數(shù)組實(shí)例
const result = Array.prototype[method].apply(this, arguments)
// 進(jìn)行其他自定義功能設(shè)置逻恐,比如,更新視圖
document.querySelector('#app').textContent = this
return result
}
})
Object.keys(data).forEach(key => {
// 檢測(cè)是否為數(shù)組峻黍,是的話單獨(dú)處理
if (Array.isArray(data[key])) {
// 將當(dāng)前數(shù)組實(shí)例的__proto__更換為customProto就行了
data[key].__proto__ = customProto
}
// 通過(guò)數(shù)據(jù)劫持的方式复隆,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
// get方法
get () {
console.log('訪問(wèn)數(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ù)挽拂,全部包裹起來(lái),如果對(duì)象內(nèi)部還含有對(duì)象的話就進(jìn)行遞歸處理骨饿,很簡(jiǎ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ù)對(duì)象亏栈,模擬 Vue 實(shí)例的 data 屬性
let data = {
msg1: 'hello',
msg2: 'world',
arr: [1, 2, 3],
obj: {
name: 'jack',
age: 18
}
}
// 模擬 Vue 實(shí)例的對(duì)象
let vm = {}
// 封裝為函數(shù),用于對(duì)數(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ù)劫持的主體功能样刷,也是遞歸時(shí)需要的功能
return function (data, vm) {
// 遍歷被劫持對(duì)象的所有屬性
Object.keys(data).forEach(key => {
// 檢測(cè)是否為數(shù)組
if (Array.isArray(data[key])) {
// 將當(dāng)前數(shù)組實(shí)例的 __proto__ 更換為 customProto 即可
data[key].__proto__ = customProto
} else if (typeof data[key] === 'object' && data[key] !== null) {
// 檢測(cè)是否為對(duì)象仑扑,如果為對(duì)象,進(jìn)行遞歸操作
vm[key] = {}
createReactive(data[key], vm[key])
return
}
// 通過(guò)數(shù)據(jù)劫持的方式置鼻,將 data 的屬性設(shè)置為 getter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('訪問(wèn)了屬性')
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提供的一個(gè)功能,對(duì)一個(gè)對(duì)象提供代理操作
<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>
通過(guò)代理箕母,訪問(wèn)P也就是訪問(wèn)了data的代理储藐,同樣的數(shù)據(jù)俱济,get方法中,target參數(shù)表示原數(shù)據(jù)data钙勃,property表示訪問(wèn)的哪條屬性蛛碌,receiver表示通過(guò)代理之后的數(shù)據(jù)
set方法中新添了一個(gè)value參數(shù),表示當(dāng)前設(shè)置的數(shù)值
我們來(lái)通過(guò)控制臺(tái)打印一探究竟
Vue3響應(yīng)式原理
與2版本的區(qū)別為數(shù)據(jù)響應(yīng)式是Proxy實(shí)現(xiàn)的辖源,其他相同蔚携,接下來(lá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>
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>
對(duì)深層監(jiān)控啊,屬性監(jiān)控啊克饶,遍歷啊都不需要在Vue3進(jìn)行操作了酝蜒,通過(guò)Proxy代理可以輕松解決,但是由于ES6的Proxy方法兼容性不是那么的好矾湃,所以市面上Vue3的普及度并不是太高亡脑,一切走向都需要根據(jù)市場(chǎng)來(lái)確定
相關(guān)設(shè)計(jì)模式
設(shè)計(jì)模式:針對(duì)軟件設(shè)計(jì)中普遍存在的各種問(wèn)題所提出的解決方案
觀察者模式
指的是在對(duì)象間定義一個(gè)一對(duì)多(被觀察者與多個(gè)觀察者)的關(guān)聯(lián),當(dāng)一個(gè)對(duì)象改變了狀態(tài)邀跃,所有其他相關(guān)的對(duì)象會(huì)被通知并且自動(dòng)刷新
就像是超市有一堆顧客霉咨,超市出了促銷活動(dòng),會(huì)通知顧客(觀察者)拍屑,又因?yàn)楫?dāng)前是否想要購(gòu)物途戒,進(jìn)行不同的選擇行動(dòng)
- 核心概念:
- 觀察者Observer
- 被觀察者(觀察目標(biāo))Subject
設(shè)計(jì)的核心點(diǎn)就是設(shè)置一個(gè)被觀察者,設(shè)置一個(gè)或者多個(gè)的觀察者丽涩,在被觀察者中設(shè)置一個(gè)遍歷進(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 () {
// 存儲(chǔ)所有的觀察者
this.observers = []
}
// 添加觀察者功能
addObserver (observer) {
// 檢測(cè)傳入的參數(shù)是否為觀察者實(shí)例
if (observer && observer.update) {
this.observers.push(observer)
}
}
// 通知所有的觀察者
notify () {
// 調(diào)用觀察者列表中的每個(gè)觀察者的更新方法
this.observers.forEach(observer => {
observer.update()
})
}
}
// 觀察者
// 1.被觀察者發(fā)生狀態(tài)變化時(shí)棺滞,做一些對(duì)應(yīng)的操作“更新”
class Observer {
update () {
console.log('事件發(fā)生了,進(jìn)行一個(gè)相應(yīng)的處理...')
}
}
// 功能測(cè)試
const subject = new Subject()
const ob1 = new Observer()
const ob2 = new Observer()
// 將觀察者添加給要觀察的觀察目標(biāo)
subject.addObserver(ob1)
subject.addObserver(ob2)
// 通知觀察者進(jìn)行操作(某些具體的場(chǎng)景下)
subject.notify()
</script>
</body>
</html>
通過(guò)觀察者模式為不同的數(shù)據(jù)設(shè)置不同的觀察者,監(jiān)視被觀察者的情況矢渊,通過(guò)特定的方法進(jìn)行更新操作等等
發(fā)布-訂閱模式
可以認(rèn)為是為觀察者模式的解耦的進(jìn)階版本继准,特點(diǎn)是:
- 在發(fā)布者和訂閱者之間添加一個(gè)消息中心,所有的消息均通過(guò)消息中心管理矮男,而發(fā)布者與訂閱者不會(huì)直接聯(lián)系移必,實(shí)現(xiàn)了兩張的解耦
核心概念:
- 消息中心Dep
- 訂閱者Subscriber
-
發(fā)布者Publisher
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script>
// 創(chuàng)建了一個(gè)Vue實(shí)例(消息中心)
const eventBus = new Vue()
// 注冊(cè)事件(設(shè)置訂閱者)
eventBus.$on('dataChange', () => {
console.log('事件處理功能1')
})
eventBus.$on('dataChange', () => {
console.log('事件處理功能2')
})
// 觸發(fā)事件(設(shè)置發(fā)布者)
eventBus.$emit('dataChange')
</script>
</body>
設(shè)計(jì)模式小結(jié)
- 觀察者模式是由觀察者和觀察目標(biāo)組成的,適合組件內(nèi)部操作(功能簡(jiǎn)單就可以)
- 特性:特殊事件發(fā)生后毡鉴,觀察目標(biāo)統(tǒng)一通知所有的觀察者
- 發(fā)布/訂閱模式由發(fā)布者與訂閱者以及消息中心組成崔泵,更加適合消息類型復(fù)雜的情況
- 特性:特殊事件發(fā)生,消息中心接到發(fā)布指令后猪瞬,會(huì)根據(jù)事件類型給對(duì)應(yīng)的訂閱者發(fā)送信息
響應(yīng)式原理模擬
整體分析
要模擬Vue實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)憎瘸,首先我們需要觀察一下Vue實(shí)例的結(jié)構(gòu),分析要實(shí)現(xiàn)哪些屬性和功能
- Vue:
- 目標(biāo):將data數(shù)據(jù)注入到Vue實(shí)例陈瘦,便于方法內(nèi)操作
- Observer(發(fā)布者)
- 目標(biāo):數(shù)據(jù)劫持幌甘,監(jiān)聽(tīng)數(shù)據(jù)變化,并在變化時(shí)通知Dep
- Dep(消息中心)
- 目標(biāo):存儲(chǔ)訂閱者以及管理消息的發(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實(shí)例中
- *監(jiān)聽(tīng)data中所有屬性的變化皱埠,設(shè)置成響應(yīng)式數(shù)據(jù)
-
*調(diào)用解析功能(解析模板內(nèi)的插值表達(dá)式肮帐,指令等等)
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類
- 功能:
- 通過(guò)數(shù)據(jù)劫持方式監(jiān)視data中的屬性變化,變化時(shí)通知消息中心Dep
-
需要考慮data的屬性也可能為對(duì)象饰抒,也要轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
Dep類
- Dep是dependency的簡(jiǎn)寫肮砾,含義是“依賴”诀黍,指的是Dep用于收集與管理訂閱者與發(fā)布者之間的依賴關(guān)系
- 功能:
- *為每個(gè)數(shù)據(jù)收集對(duì)應(yīng)的依賴袋坑,存儲(chǔ)依賴
- 添加并存儲(chǔ)訂閱者
-
數(shù)據(jù)變化時(shí),通知所有的觀察者
Watcher 類
- 功能:
- 實(shí)例化Watch時(shí)眯勾,往dep對(duì)象中添加自己
-
當(dāng)數(shù)據(jù)變化觸發(fā)dep枣宫,dep通知所有對(duì)應(yīng)的Watcher實(shí)例更新視圖
Complier類
- 功能:
- 進(jìn)行編譯模板,并解析內(nèi)部指令與插值表達(dá)式
- 進(jìn)行頁(yè)面的首次渲染
-
數(shù)據(jù)變化后吃环,重新渲染視圖
功能回顧與總結(jié)
- Vue類
- 把data的屬性注入到Vue實(shí)例
- 調(diào)用Observer實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式處理
- 調(diào)用Compiler編譯模板
- Observer
- 將data的屬性轉(zhuǎn)換為Getter/setter
- 為Dep添加訂閱者Watcher
- 數(shù)據(jù)變化發(fā)送時(shí)通知Dep
- Dep
- 收集依賴也颤,添加訂閱者(Watcher)
- 通知訂閱者
- Watcher
- 編譯模板時(shí)創(chuàng)建訂閱者,訂閱數(shù)據(jù)變化
- 接到Dep通知時(shí)郁轻,調(diào)用Compiler中的模板功能更新視圖
- Compiler
- 編譯模板翅娶,解析指令與插值表達(dá)式
-
負(fù)責(zé)頁(yè)面首次渲染與數(shù)據(jù)變化后重新渲染