一探 Vue 資料回應式原理
本文寫于 2020 年 8 月 5 日
相信在很多新人第一次使用 Vue 這種框架的時候,就會被其修改資料便自動更新視圖的操作所震撼,
Vue 的檔案中也這么寫道:
Vue 最獨特的特性之一,是其非侵入性的回應式系統,資料模型僅僅是普通的 JavaScript 物件,而當你修改它們時,視圖會進行更新,
單看這句話,像我這種菜鳥程式員必然是看不懂的,我只知道,在 new Vue() 時傳入的 data 屬性一旦產生變化,那么在視圖里的變數也會隨之而變,
但這個變化是如何實作的呢?接下來讓我們,一探究竟,
1 偷偷變化的 data
我們先來新建一個變數:let data = https://www.cnblogs.com/xhyccc/p/{ msg:'hello world' },
接著我們將這個 data 傳給 Vue 的 data:
let data = https://www.cnblogs.com/xhyccc/p/{ msg:'hello world' }
/*****留空處*****/
new Vue({
data,
methods: {
showData() {
console.log(data)
}
}
})
這看似是非常平常的操作,但是我們在觸發 showData 的時候,會發現打出來 data 不太對勁:
msg: (...)
__ob__: Observer {value: {…}, dep: Dep, vmCount: 1}
get msg: ? reactiveGetter()
set msg: ? reactiveSetter(newVal)
__proto__: Object
它不僅多了很多沒見過的屬性,還把里面的 msg: hello world 變成了 msg: (...),
接下來我們嘗試在留空處列印出 data,即在定義完 data 之后立即將其列印,
但是很不幸,列印出來依然是上面這個不對勁的值,
可是很明顯,當我們不去 new Vue(),并且傳入 data 的時候,data 的列印結果絕對不是這樣,
所以我們可以嘗試利用 setTimeout() 將 new Vue() 延遲 3 秒執行,
這個時候我們就會驚訝的發現:
- 當我們在 3s 內點開 console 的結果時,data 是普通的形式;
- 當我們在 3s 后點開 console 的結果時,data 又變成了奇怪的形式,
這說明就是 new Vue() 的程序中,Vue 偷偷的對 data 進行了修改!正是這個修改,讓 data 的資料,變成了回應式資料,
2 (...) 的由來
為什么好好的一個 msg 屬性會變成 (...) 呢?
這就涉及到了 ES6 中的 getter 和 setter,(如果理解 getter/setter,可跳至下一節)
一般我們如果需要計算后的值,會定義一個函式,例如:
const obj = {
number: 5,
double() {
return this.number * 2;
}
};
在使用的時候,我們寫上 obj.double(obj.number) 即可,
但是函式是需要加括號的,我太懶了,以至于括號都不想要了,
于是就有了 getter 方法:
const obj = {
number: 5,
get double() {
return this.number * 2;
}
};
const newNumber = obj.double;
這樣一來,就能夠不需要括號,就可以得到 return 的值,
setter 同理:
const obj = {
number: 5,
set double(value) {
if(this.number * 2 != value;)
this.number = value;
}
};
obj.double = obj.number * 2;
由此我們可以看出:通過 setter,我們可以達到給賦值設限的效果,例如這里我就要求新值必須是原值的兩倍才可以,
但經常的,我們會用 getter/setter 來隱藏一個變數,
比如:
const obj = {
_number: 5,
get number() {
return this._number;
},
set number(value) {
this._number = value;
}
};
這個時候我們列印出 obj,就會驚訝的發現 (...) 出現了:
number: (...)
_number: 5
現在我們明白了,Vue 偷偷做的事情,就是把 data 里面的資料全變成了 getter/setter,
3 利用 Object.defineProperty() 實作代理
這個時候我們想一個問題,原來我們可以通過 obj.c = 'c'; 來定義 c 的值——即使 c 本身不在 obj 中,
但如何定義一個 getter/setter 呢?答:使用 Object.defineProperty(),
Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,并回傳此物件,
例如我們上面寫的 obj.c = 'c';,就可以通過
const obj = {
a: 'a',
b: 'b'
}
Object.defineProperty(obj, 'c', {
value: 'c'
})
Object.defineProperty() 接收三個引數:第一個是要定義屬性的物件;第二個是要定義或修改的屬性的名稱或 Symbol;第三個則是要定義或修改的屬性描述符,
在第三個引數中,可以接收多個屬性,value 代表「值」,除此之外還有 configurable, enumerable, writable, get, set 一共六個屬性,
這里我們只看 get 與 set,
之前我們說了,通過 getter/setter 我們可以把不想讓別人直接操作的資料“藏起來”,
可是本質上,我們只是在前面加了一個 _ 而已,直接訪問是可以繞過我們的 getter/setter 的!
那么我們怎么辦呢?
利用代理,這個代理不是 ES6 新增的 Proxy,而是設計模式的一種,
我們剛剛為什么可以去修改我們“藏起來”的屬性值?
因為我們知道它的名字呀!如果我不給他名字,自然別人就不可能修改了,
例如我們寫一個函式,然后把資料傳進去:
proxy({ a: 'a' })
這樣一來我們的 { a: 'a' } 就根本沒有名字了,無從改起!
接下來我們在定義 proxy 函式時,可以新建一個空物件,然后遍歷傳入的值,分別進行 Object.defineProperty() 以將傳入的物件的 keys 作為 getter/setter 賦給新建的空物件,
最后,我們 return 這個物件即可,
let data = https://www.cnblogs.com/xhyccc/p/proxy({
a:'a',
b: 'b'
});
function proxy(data) {
const obj = {};
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(obj, keys[i], {
get() {
return data[keys[i]];
},
set(value) {
if (value < 0) return;
data[keys[i]] = value;
}
});
}
return obj;
}
這樣一來,我們一開始宣告的 data,就是我們 return 的物件了,在這個物件里,沒有原始的資料,別人無法繞過 getter/setter 進行操作!
但是往往并沒有這么簡單,如果我一定需要一個變數名呢?
const sourceData = https://www.cnblogs.com/xhyccc/p/{
a:'a',
b: 'b'
};
let data = https://www.cnblogs.com/xhyccc/p/proxy(sourceData);
如此一來,通過直接操作 sourceData.a,時可以直接繞過我們在 proxy 中設定的 set a 進行賦值的,這個時候我們怎么處理?
很簡單嘛,當我們遍歷傳入的資料時,我們可以對傳入的資料新增 getter/setter,此后原始的資料就會被 getter/setter 所替代,
在剛剛的代碼中,我們在回圈的剛開始添加這樣一段代碼:
for(/*......*/) {
const value = https://www.cnblogs.com/xhyccc/p/data[keys[i]];
Object.defineProperty(data, keys[i], {
get() {
return value;
},
set(newValue) {
if (newValue < 0) return;
value = newValue;
}
});
/*......*/
}
這是什么意思呢?
我們利用了閉包,將原始值單獨拎出來,每一次對原始屬性進行讀寫,其實都是 get 和 set 在讀取閉包時被拎出來的值,
那么不管別人是操作我們的 let data = https://www.cnblogs.com/xhyccc/p/proxy(sourceData); 的 data,還是操作 sourceData,都會被我們的 getter/setter 所攔截,
4 回到 Vue
我們剛剛寫的代碼是這樣的:
let data = https://www.cnblogs.com/xhyccc/p/proxy({
a:'a'
})
function proxy(data) {
}
那如果我改成這樣呢:
let data = proxy({
data: {
a: 'a'
}
})
function proxy({ data }) {
// 結構賦值
}
是不是和 Vue 就非常非常像了!
const vm = new Vue({ data: {} }) 也是讓 vm 成為 data 的代理,并且就算你從外部將資料傳給 data,也會被 Vue 所捕捉,
而在每一次捕獲到你操作資料之后,就會對需要改變的 UI 進行重新渲染,
同理,Vue 對 computed 和 watch 也存在著各種偷偷的處理,
5 Vue 資料回應式的 Bug
如果我們的資料是這樣:
data: {
obj: {
a: 'a'
}
}
我們在 Vue 的 template 里卻寫了 <div>{{ obj.b }}<div> 會怎樣?
Vue 對于不存在或者為 undefined 和 null 的資料是不予以顯示的,但是當我們往 obj 中新增 b 的時候,他會顯示嗎?
寫法一:
const vm = new Vue({
data: {
obj: {
a: 'a'
}
},
methods: {
changeObj() {
this.obj.b = 'b';
}
}
})
我們可以給一個按鈕系結 changeObj 事件,但是很遺憾,這樣并不能使視圖中的 obj.b 顯示出來,
回想一下剛剛我們對于資料的處理,是不是只遍歷了外層?這就是因為 Vue 并沒有對 b 進行監聽,他根本不知道你的 b 是如何變化的,自然也就不會去更新視圖層了,
寫法 2:
const vm = new Vue({
data: {
obj: {
a: 'a'
}
},
methods: {
changeObj() {
this.obj.a = 'a2'
this.obj.b = 'b';
}
}
})
我們僅僅只是新增了一行代碼,在改變 b 之前先改變了 a,居然就讓 b 實作了更新!
這是為什么?
因為視圖更新其實是異步的,
當我們讓 a 從 'a' 變成 'a2' 時,Vue 會監聽到這個變化,但是 Vue 并不能馬上更新視圖,因為 Vue 是使用 Object.defineProperty() 這樣的方式來監聽變化的,監聽到變化后會創建一個視圖更新任務到任務佇列里,
所以在視圖更新之前,要先把余下的代碼運行完才行,也就是會運行 b = 'b',
最后等到視圖更新的時候,由于 Vue 會去做 diff 演算法,于是 Vue 就會發現 a 和 b 都變了,自然會去更新相對應的視圖,
但是這并不是我們解決問題的辦法,寫法 2 充其量只能算是“副作用”,
Vue 其實提供了方法讓我們來新增以前沒有生命的屬性:Vue.set() 或者 this.$set(),
Vue.set(this.obj, 'b', 'b'); 會代替我們進行 obj.b = 'b';,然后監聽 b 的變化,觸發視圖更新,
那陣列怎么回應呢?
每當我們往陣列里新增元素的時候,陣列就在不斷的變長,對于沒有宣告的陣列下標,很明顯 Vue 不會給予監聽呀,
比如 a: [1, 2, 3],當我新增一個元素,讓 a === [1, 2, 3, 4] 的時候,a[3] 是不會被監聽的,
總不能每次 push 陣列,都要手寫剛剛說的 Vue.set 方法吧,
可實際操作中,我們發現并沒有呀,Vue 監聽了新增的資料,
這是因為 Vue 又偷偷的干了一件事兒,它把你原本的陣列方法給改了一些,
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
在 Vue 中的陣列所帶的這七個方法都不是原生的方法了,Vue 考慮到這些操作極為常用,所在中間為我們添加了監聽,
講到這里,相信大家對 Vue 的回應式原理應該有了更深的認識了,
(完)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/18387.html
標籤:JavaScript
上一篇:遍歷陣列,物件和JSON
