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