追憶 上一節:零基礎入門Vue之影分身之術——列表渲染&渲染原理淺析 雖然我深知,大佬告訴我”先學應用層在瞭解底層,以應用層去理解底層“,但Vue的數據如何檢測的我不得不去學 否則,在寫代碼的時候,可能會出現我難以解釋的bug 對此,本篇文章,將記錄我對Vue檢測數據的理解 對於Vue檢測數據的實現 ...
追憶
上一節:零基礎入門Vue之影分身之術——列表渲染&渲染原理淺析
雖然我深知,大佬告訴我”先學應用層在瞭解底層,以應用層去理解底層“,但Vue的數據如何檢測的我不得不去學
否則,在寫代碼的時候,可能會出現我難以解釋的bug
對此,本篇文章,將記錄我對Vue檢測數據的理解
對於Vue檢測數據的實現,我打算由淺入深的去記錄
- JavaScript實現數據監控
- 實現簡單的數據監測(淺淺的響應式)
- Vue對哪些數據做了監測,哪些沒有?
JavaScript的數據檢測
Object.defineProperty() 靜態方法會直接在一個對象上定義一個新屬性,或修改其現有屬性,並返回此對象。
熟悉JavaScript的人,應該在一個月黑風高的夜晚都瞭解過Object上的一個方法: Object.defineProperty()
而Vue大部分的數據監測都是依賴於這個方法來實現的
ps:本篇不會深度探討這個靜態方法的用法,僅僅對其get和set方法的用法講解,為下一章節做鋪墊
Object.defineProperty(obj, prop, descriptor)
- obj:要添加新屬性的對象
- prop:要添加屬性的名稱(一般為字元串)
- descriptor:這個屬性的相關描述(配置)
假設現在有一個對象如下:
let person = {
name:"張三"
};
現在,我試圖這個person對象新增一個age屬性,那麼我可以這麼乾
Object.defineProperty(person, "age", {
value:18 //設置預設的值
});
此時輸出person時可以看到
{name: '張三', age: 18}
題外話:關於為什麼是點開後為什麼age屬性與其他屬性顏色不一樣,可以將enumerable:true,使它可迭代
但這真的是一個普通屬性了嗎?並不會,當我試圖
person.age = 20
console.log(person.age); //18
似乎修改不了數據,因為少了一個配置項
Object.defineProperty(person, "age", {
value:18,
writable:true //配置為可修改
});
此時上述代碼差不多等同於
person.age = 18; //假設age沒定義過,重新給person追加一個屬性
get
用作屬性 getter 的函數,如果沒有 getter 則為 undefined。當訪問該屬性時,將不帶參地調用此函數,並將 this 設置為通過該屬性訪問的對象(因為可能存在繼承關係,這可能不是定義該屬性的對象)。返回值將被用作該屬性的值。預設值為 undefined。
從上面MDN上的話來說,當我們要用到對象上的某個屬性時會調用 getter,如果對象沒有設置,則預設是undefined,當訪問這個屬性時,訪問得到的結果將是getter返回的結果
我現在有一個需求:當age屬性被讀取時,就加1歲,並且輸出變化後的值
在上述代碼裡面是完不成這個需求的,此時就需要用到get方法了,當讀取時會自動調用get方法,我可以在那個裡面進行數據的遞增
註意:官網描述中,get不能與 value 或 writable 同時使用
- 定義實際年齡數據:_age
- 當讀取person.age,get選擇器返回_age,並且把_age遞增
具體實現代碼如下:
let age = 18; //定義一個實際的年齡數據
let person = { //準備好目標對象
name:"張三"
};
Object.defineProperty(person,"age",{
get(){
console.log("年齡即將遞增一歲後:",age + 1);
return age,age++;
}
});
> person.age
年齡即將遞增一歲後: 19
18
> person.age
年齡即將遞增一歲後: 20
19
綜上所述,當有人要讀取某個屬性的時候,可以對這個屬性值做了處理在返回,或者是調用函數通知其他的事件觸發等等
set
用作屬性 setter 的函數,如果沒有 setter 則為 undefined。當該屬性被賦值時,將調用此函數,並帶有一個參數(要賦給該屬性的值),並將 this 設置為通過該屬性分配的對象。預設值為 undefined。
set的用法正好和get方法對應,一個是讀一個是寫,set方法當要給屬性賦值時會被調用,並且可以接收一個參數作為新的值
同樣以這個對象為例,還是把get賦值寫好,每次都返回當前age的值
let age = 18; //定義一個實際的年齡數據
let person = { //準備好目標對象
name:"張三"
};
Object.defineProperty(person,"age",{
get(){
return age;
}
});
現在呢,如果我去修改他得值,他還是出現改不掉的情況
這是因為set沒配置,我先配置個最基本的set看看,能否修改age的值
let age = 18; //定義一個實際的年齡數據
let person = { //準備好目標對象
name:"張三"
};
Object.defineProperty(person,"age",{
get(){
return age; //返回實際年齡
},
set(newVal){
age = newVal //修改實際年齡
}
});
> person.age
18
> person.age = 19
19
> person.age
19
很顯然是可以修改的
現在呢,我希望當我對年齡做出了修改,如果不是遞增1的話就彈出警告
那麼我可以這麼乾
let age = 18; //定義一個實際的年齡數據
let person = { //準備好目標對象
name:"張三"
};
Object.defineProperty(person,"age",{
get(){
return age;
},
set(newVal){
if(newVal !== age+1){
console.warn("註意:你這個年齡增加的有點快啊!!!");
}
age = newVal
}
});
此時這段代碼,能完美的完成需求
畫龍點睛
上面的例子,還是不怎麼完善,萬一有人給年齡隨意賦值呢?那我是不是要彈出報錯?
所以,當調用set的時候,可以進行一系列的數據類型判斷,這裡僅需判斷是否為數值即可,區別不能為負值不然就拋出錯誤
代碼如下:
let age = 18; //定義一個實際的年齡數據
let person = { //準備好目標對象
name:"張三"
};
Object.defineProperty(person,"age",{
get(){
return age;
},
set(newVal){
if(typeof newVal !== 'number'){
throw "你在想什麼呢?";
}else if(newVal < 0){
throw "你跟閻王溝通過?";
}else if(newVal !== age+1){
console.warn("註意:你這個年齡增加的有點快啊!!!");
}
age = newVal;
}
});
實現簡單的數據檢測
在第一篇:零基礎入門Vue之夢開始的地方——插值語法 中我提到如下的說明
"{{}}"在這個表達式裡面可以寫js的表達式,並且它裡面的執行語句的this是vue實例,同時vue官方文檔指出,在data中配置的東西最後都會通過數據代理的方式掛在到vue實例上。
data配置項裡面的所有數據,都會以數據代理的方式掛在到vm實例上,並且Vue也會提供一個純凈版的Vue._data,此時這個Vue._data等同於我們配置的data
(即:vm._data === data is true)
而這個數據代理就是依賴於上一節說的 Object.defineProperty() 來實現
實現原理簡單分析&實現
在Vue中,Vue對實際傳入的data並沒有直接掛在到vm對象及vm._data上,而是重新通過get和set去做一系列的數據代理和數據監測
這個過程中有許多細節要處理,本篇不可能以這一千不到的字數去說明白Vue的數據檢測和數據代理
僅僅只是做一個基本的樣例,供我自己學習
首先,我得準備一個方法用來刷新dom意思意思一下
function flashVirtualDom(){
//此處省略新老虛擬dom之間的比較演算法
console.log("檢測到數據更改,準備刷新虛擬dom");
}
然後呢,我要準備好一個數據
let data = {
name:"張三",
age:18,
friends:["李四","王五","趙高"],
school:{
name:"北京大學",
local:"北京",
totalYears:4
}
};
目標:接下來我希望不直接操作data的數據,而是用另外的方法去操作data的數據,並且當data數據發送改變時能被我寫的代碼檢測到
既然不是直接操作,那麼用戶和真實數據應該有一個 中間層,所以我把它明明為Middle
這個中間層呢,使用Object.defineProperty() 把data的數據掛載到自己實例上,可以操作它的實例間接更改data
(換個說法:在上一節age就是真實數據,而person.age實際上是真實數據的代理,我並沒有直接操作age,只不過這次數據交給data對象,更加的密集,集中保管和內部維護)
function Middle(obj){
let keys = Object.keys(obj); //拿到所有的key
for(let key of keys){
//如果是對象
if(typeof obj[key] === "object" && !(obj[key] instanceof Array)){
this[key] = new Middle(obj[key]); //如果是對象嵌套那麼遞歸調用
continue;
}else{
//過濾undefined
if(!obj[key]){
continue;
}
//設置數據代理
Object.defineProperty(this,key,{
get(){
//讀取就返回原模原樣的值
return obj[key];
},
set(newVal){
//賦值就修改原始數據
flashVirtualDom();
obj[key] = newVal;
}
})
}
}
}
此時,在console執行如下代碼
> let m = new Middle(data);
undefined
> m.age = 30
檢測到數據更改,準備刷新虛擬dom
30
> m.age
30
展開m,與Vue實例的_data進行對比,相差不大,當我修改其中一個數據時會調用flashVirtualDom();方法用於刷新dom,同樣還可以寫其他方法做其他操作
數據劫持
在尚矽谷的課程中,有提到”數據劫持“這一概念,對此,本篇也相應的記錄下
個人理解的數據劫持,實際上是當數據發生變動時,率先攔截變動,做出處理後,在決定是否要變動數據
或者更改變動的結果,其在我理解更類似於hook的操作,當某個函數或者變數被賦值,然後我對他進行一次攔截
攔截之後做出我想要的操作,操作做完再讓他回歸正常運行
Vue中的數據監測
在做Vue的開發中,Vue並非對任何數據都做了監測,因此我認為我作為Vue的學習者,應當去瞭解具體有哪些情況不會被監測到,從而避免日後開發的各種奇奇怪怪的問題。
對於常見數據,有兩種比較容易出錯
- 對象
- 數組
對象相關的數據監測問題&解決方案
假設,在初始的data裡面僅有如下數據
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./vue.js"></script>
<title>Document</title>
</head>
<body>
<div id="root">
<div>姓名:{{person.name}}</div>
<div>性別:{{person.sex}}</div>
<div>年齡:{{person.age}}</div>
</div>
</body>
<script>
let vm = new Vue({
el:"#root",
data:{
person:{
name:"張三",
sex:"男"
}
}
})
</script>
</html>
這都很正常,但如果我想不修改這個代碼,在項目上線後根據後端給的數據動態的增加
假設在某處代碼上後端返回的數據有年齡,我想data的數據增加一條年齡並且展示到應該展示的位置
那麼我試圖這麼做
vm.data.age = 19; //假設後端給出的數據是19
此時頁面無任何變化
(實際操作,可以先讓頁面運行起來,然後再console上去追加一個年齡)
這個後期追加的數據在Vue中並沒有被監測,導致他沒有顯示(簡單說就是沒有get和set方法)
那我該如何解決呢?
Vue非常人性化的提供另外的方法:Vue.set( target, propertyName/index, value )
(註:用vm.$set也是一樣的,詳細區別可以去翻官方文檔)
在這個方法允許給某個對象添加屬性並且監測
- target:目標對象
- propertyName/index:屬性名稱/索引值(可用於數組)
- value:值
所以當我接收到後端數據時,我可以用這個方法追加數據並且被Vue所監測
Vue.set(vm.person,'age',19); //假設後端給出的數據是19
此時age即可直接顯示到頁面上了
數組相關的數據監測問題&解決辦法
假設,這次問題代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./vue.js"></script>
<title>Document</title>
</head>
<body>
<div id="root">
<ul>
<li v-for="per in friends" :key="per.id">
{{per}}
</li>
</ul>
</div>
</body>
<script>
let vm = new Vue({
el:"#root",
data:{
friends:["張三","李四","王五","趙高"]
}
})
</script>
</html>
現在呢,我想要修改friends第一個元素,修改為”張四“
正常做法如下:
vm.friends[0] = "張四";
但頁面數據無變化,實際數據已經更改了
這是為什麼呢?展開friends數組,發現這並沒有數組成員的get和set方法,Vue並未對數組成員做監測,因此改了之後,數據並未刷新
那麼,我該如何做呢?官網其實早就給出了答案:變更方法。 官方對以下七個方法進行了包裹(我感覺hook更好理解)
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
依次,但我通過這些方法去做數組進行”增刪改查“時,Vue會檢測到,所以我想修改第一個元素為李四可以這麼乾
vm.friends.splice(0,1,"張四")
執行完後頁面變了,說明變動被監測到了,除此之外還可以使用set方法
Vue.set(vm.friends,0,"張四");
The End
唔~(一口濁氣)
這一篇可真長啊
本篇完~~~~