
Vue數(shù)據(jù)劫持的實(shí)現(xiàn),做一個(gè)自己的理解&簡(jiǎn)單總結(jié)。雖然Vue3.0即將到來,我想Vue2.x也不至于馬上過時(shí)。
今天就從Vue2.x 與 Vue.3.0 數(shù)據(jù)劫持如何實(shí)現(xiàn)數(shù)據(jù)雙向綁定。
數(shù)據(jù)劫持: 指的是在訪問或者修改對(duì)象的某個(gè)屬性時(shí),通過一段代碼攔截這個(gè)行為,進(jìn)行額外的操作或者修改返回結(jié)果。
Vue2.x 選擇的 Object.defineProperty
Object.defineProperty 對(duì)大家都來說應(yīng)該不陌生了。算是面試的一道必考題?(細(xì)品:那掌握好了是不是就是一道送分題呢?)可以點(diǎn)擊這里回顧一下 Object.defineProperty的文檔
我們來認(rèn)清Object.defineProperty的幾個(gè)局限性
- 兼容性是IE8+,這也就是為什么Vue不支持IE8及以下版本的原因。
- 不能監(jiān)聽數(shù)組的變化,Vue通過重寫數(shù)組原型的方法來實(shí)現(xiàn)數(shù)據(jù)劫持。
- 對(duì)于深層次嵌套對(duì)象需要做遞歸遍歷。
- 必須遍歷對(duì)象的每個(gè)屬性。如果要擴(kuò)展該對(duì)象,就必須手動(dòng)去為新的屬性設(shè)置setter、getter方法。 這也就是為什么Vue開發(fā)中的不在 data 中聲明的屬性無法自動(dòng)擁有雙向綁定效果的原因。需要我們手動(dòng)去調(diào)用Vue.set()
我們做個(gè)類似Vue簡(jiǎn)易的數(shù)據(jù)劫持
- 視圖更新觸發(fā)的函數(shù)
// 當(dāng)我們監(jiān)聽的數(shù)據(jù)發(fā)生變化后調(diào)用改函數(shù)
function update() {
console.log('數(shù)據(jù)變化啦,更新視圖')
}
- 通過 Object.defineProperty 處理 data 中的每個(gè)屬性
// 通過 Object.defineProperty 處理 target 中的每個(gè)屬性 key
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
get() {
return value;
},
set(val) {
// 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
if (val !== value) {
// 數(shù)據(jù)更新了,調(diào)用update
update();
value = val;
}
}
})
}
- 監(jiān)聽data的函數(shù)
function observer(target) {
// 如果不是對(duì)象,直接返回;如果是null也直接返回
if (typeof target !== 'object' || !target) return target;
// 遍歷對(duì)象obj的所有key,完成屬性配置
Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}
- 測(cè)試步驟1、2、3
// 需要監(jiān)聽的data對(duì)象
const data = {
level: 1,
info: {
name: 'cc'
}
}
// 調(diào)用監(jiān)聽函數(shù)監(jiān)聽 data
observer(data)
// 修改data的值 視圖更新
data.level = 2
// 看到視圖確實(shí)更新了
// 我們不妨嘗試了一下data深層次對(duì)象的修改
data.info.name = 'yy'
// 控制臺(tái)什么都是沒有
- 想必你也發(fā)現(xiàn)了,監(jiān)聽data只到了對(duì)象的第一層。data深層次的數(shù)據(jù),并沒有被監(jiān)聽。所以我們需要對(duì)data做一個(gè)逐層遍歷(遞歸),直到把每個(gè)對(duì)象的每個(gè)屬性都調(diào)用
Object.defineProperty()為止。
// 改改步驟二的代碼
function defineReactive(target, key, value) {
// 在這里新增代碼
// 當(dāng)value為object我們?cè)僮鲆淮螖?shù)據(jù)監(jiān)聽,直到value不是object為止
if (typeof value === 'object') {
observer(value)
}
// 以下代碼和步驟2沒有區(qū)別
Object.defineProperty(target, key, {
get() {
return value;
},
set(val) {
// 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
if (val !== value) {
// 數(shù)據(jù)更新了,調(diào)用update
update();
value = val;
}
}
})
}
- 再對(duì)步驟5的修改做一次測(cè)試
const data = {
level: 1,
info: {
name: 'cc'
},
a: {
a: {
a: {
a: 1
}
}
}
}
// 我們嘗試改變data.info.name的值
data.info.name = 'xy' // 視圖更新了!
// 我們嘗試跟深層次的修改
data.a.a.a.a = 2 // ok 視圖也更新了
// 那么我再試試其他方式
// 先修改data.info的值
data.info = { name: 'cc' } // 沒毛病,視圖更新了,但此時(shí)data.info的指向已經(jīng)發(fā)生了變化
// 然后再修改data.info.name
data.info.name = 'xy' // emmmmmm... 又是什么都沒有
- 我們針對(duì)步驟5再做一次修改
// 修改步驟5的代碼
function defineReactive(target, key, value) {
if (typeof value === 'object') {
observer(value)
}
Object.defineProperty(target, key, {
get() {
return value;
},
set(newVal) {
// 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
if (newVal !== value) {
// 在這里新增代碼
// 如果設(shè)置newVal是object,對(duì)newVal做監(jiān)聽
if (typeof newVal === 'object') {
observer(newVal)
}
// 數(shù)據(jù)更新了,調(diào)用update
update();
value = newVal;
}
}
})
}
- 再對(duì)步驟7的修改做一次測(cè)試
const data = {
level: 1,
info: {
name: 'cc'
}
}
// 先修改data.info的值
data.info = { name: 'cc' } // 沒毛病,視圖更新了
// 然后再修改data.info.name
data.info.name = 'xy' // 也沒毛病,視圖更新了
- 我們都知道
typeof數(shù)據(jù)返回的也是object
const data = {
arr: []
}
// 嘗試對(duì)數(shù)組做更改
arr.push(1); // 然鵝,并沒有任何輸出
- 前面有說明Object.defineProperty 對(duì)數(shù)組是起不到任何作用的。那Vue如何實(shí)現(xiàn)的呢? Vue是通過修改數(shù)組的原型方法來實(shí)現(xiàn)數(shù)據(jù)劫持(做一些視圖更新、渲染的操作)。
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 遍歷methods數(shù)組
methods.forEach(method => {
// 原生Array的原型方法
const originalArray = Array.prototype[method]
// 重寫Array原型上對(duì)應(yīng)的方式
Array.prototype[method] = function() {
// 做視圖更新或者渲染操作
update();
// 視圖更新了,調(diào)用對(duì)應(yīng)的原生方法
// arguments 將該有的參數(shù)也傳進(jìn)來
originalArray.call(this, ...arguments);
}
})
- 又到了驗(yàn)證一下步驟10的時(shí)候啦!
const data = {
arr: []
}
data.arr.push(1) // 視圖更新了
- 看了上面的代碼,可能就有疑問了。我們明顯直接修改的是 Array.prototype的方法。這樣會(huì)導(dǎo)致一個(gè)問題。沒有被監(jiān)聽的數(shù)組,也會(huì)觸發(fā)update()。如下:
var normalArray = [];
normalArray.push(1); // wtf 竟然也觸發(fā)了視圖更新
結(jié)果明顯不是我們想要的。我們希望的是:Array原有的方法保持不變,但是又要引用到原來的方法的實(shí)現(xiàn)。
我們可以簡(jiǎn)單地處理下啦。
①先修改步驟10的代碼
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []
// 遍歷methods數(shù)組
methods.forEach(method => {
// 原生Array的原型方法
const originalArray = Array.prototype[method]
// 重寫Array原型上對(duì)應(yīng)的方式
arrayList[method] = function() {
// 做視圖更新或者渲染操作
update();
// 視圖更新了,調(diào)用對(duì)應(yīng)的原生方法
// arguments 將該有的參數(shù)也傳進(jìn)來
originalArray.call(this, ...arguments);
}
})
②再修改步驟7的代碼
function defineReactive(target, key, value) {
if (typeof value === 'object') {
// 通過鏈去找我們定義好的方法
if (Array.isArray(value)) {
value.__proto__ = arrayList
}
observer(value)
}
Object.defineProperty(target, key, {
get() {
return value;
},
set(val) {
// 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
if (val !== value) {
// 在這里新增代碼,如果設(shè)置val是object,對(duì)val做監(jiān)聽
if (typeof val === 'object') {
// 通過鏈去找我們定義好的方法
if (Array.isArray(val)) {
val.__proto__ = arrayList
}
observer(val)
}
// 數(shù)據(jù)更新了,調(diào)用update
update();
value = val;
}
}
})
}
- 完整代碼
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []
// 遍歷methods數(shù)組
methods.forEach(method => {
// 原生Array的原型方法
const originalArray = Array.prototype[method]
// 重寫Array原型上對(duì)應(yīng)的方式
arrayList[method] = function() {
// 做視圖更新或者渲染操作
update();
// 視圖更新了,調(diào)用對(duì)應(yīng)的原生方法
// arguments 將該有的參數(shù)也傳進(jìn)來
originalArray.call(this, ...arguments);
}
})
// 當(dāng)我們監(jiān)聽的數(shù)據(jù)發(fā)生變化后調(diào)用改函數(shù)
function update() {
console.log('數(shù)據(jù)變化啦,更新視圖')
}
function observer(target) {
// 如果不是對(duì)象,直接返回
if (typeof target !== 'object' || !target) return target;
// 遍歷對(duì)象obj的所有key,完成屬性配置
Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}
function defineReactive(target, key, value) {
if (typeof value === 'object') {
if (Array.isArray(value)) {
value.__proto__ = arrayList
}
observer(value)
}
Object.defineProperty(target, key, {
get() {
return value;
},
set(newVal) {
// 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
if (newVal !== value) {
// 在這里新增代碼,如果設(shè)置newVal是object,對(duì)newVal做監(jiān)聽
if (typeof newVal === 'object') {
if (Array.isArray(newVal)) {
newVal.__proto__ = arrayList
}
observer(newVal)
}
// 數(shù)據(jù)更新了,調(diào)用update
update();
value = newVal;
}
}
})
}
const data = {
level: 1,
info: {
name: 'cc'
},
arr: []
}
observer(data)
// 自行打開注釋行測(cè)試即可
// ①
// data.level = 2
// ②
// data.info.name = 'xy'
// ③
/*
data.info = {name: 'cc'}
data.info.name = 'xy'
*/
// ④
// data.arr.push(1)
// ⑤
/*
data.arr = []
data.arr.push(1)
*/
值得注意的是:數(shù)組不支持長度的修改,也不支持通過數(shù)組的索引進(jìn)行更改。例如以下方式是不會(huì)觸發(fā)視圖更新,只有上面列舉的7個(gè)方式或者直接替換一個(gè)新的數(shù)組才會(huì)觸發(fā)視圖更新。數(shù)組更新檢測(cè)
data.arr.length = 3
data.arr[1] = 1
Vue3.0 選擇的 Proxy
Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問進(jìn)行過濾和改寫。
function update() {
console.log('數(shù)據(jù)變化啦,更新視圖')
}
const data = {
level: 1,
info: {
name: 'cc'
},
arr: []
}
const handler = {
get(target, property) {
// 如果值為對(duì)象,在對(duì)該值進(jìn)行數(shù)據(jù)劫持
if (typeof target[property] === 'object' && target[property] !== null) {
return new Proxy(target[property], handler)
}
return Reflect.get(target, property)
},
set(target, property, value) {
if (property === 'length') {
return true
}
update()
return Reflect.set(target, property, value)
}
}
const proxy = new Proxy(data, handler)
proxy.level = 2
proxy.info.name = 'yy'
proxy.arr.push(1)
proxy.arr[1] = 1
Proxy最大的問題應(yīng)該就是兼容性了,但是3.0都準(zhǔn)備發(fā)布了,我們值得簡(jiǎn)單一試~