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