Vue中用props給data賦初始值遇到的問題
前言
前段時間做一個運營活動的專案,上線後產品反饋頁面埋點不對,在排查過程中發現,問題竟然是由於Vue中的data初始值導致,而data的初始值來自於props。為方便描述,現將問題抽象如下:
一、現象
程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用props初始化data中變數</title> <script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script> </head> <body> <div id="app"> <user-info :user-data="user"></user-info> </div> <script> //全域性元件 let userInfo = Vue.component('userInfo' ,{ name: 'user-info', props: { userData: Object }, data() { return { userName: this.userData.name } }, template: ` <div> <div>姓名:{{userName}}</div> <div>性別:{{userData.gender}}</div> <div>生日:{{userData.birthday}}</div> </div> ` }); //Vue例項 new Vue({ el: '#app', data: { user: { name: '', gender: '', birthday: '' } }, created(){ this.getUserData(); }, methods:{ getUserData(){ setTimeout(()=>{ this.user = { name: '於永雨', gender: '男', birthday: '1991-7' } }, 500) } }, components: { userInfo } }); </script> </body> </html>
程式碼解讀:
- 根元件data中有一個物件:user,包含三個屬性:name、gender、birthday,初始值都為空字串
- 模擬api非同步請求,500毫秒後對user的重新賦值,三個屬性都不再為空
- 宣告一個子元件userInfo,props中有一個物件userData,用於接收父元件的user;data中有一個變數userName,初始值來自於userData.name
結果:
頁面初始化後,姓名、性別、生日都顯示為空,500毫秒後性別和生日顯示正常結果,僅姓名沒有變化。
為什麼會這樣呢?
我最初的想法:user.name是String,屬於基本資料型別,用它給子元件data中userName賦值,屬於基本資料型別賦值,所以當父元件中user.name變化時,子元件中userName並不會隨之變化。
是這樣的嗎?於是我決定將user.name改為物件,通過引用資料型別賦值,然後觀察是否符合預期。程式碼如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用props初始化data中變數-物件形式</title> <script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script> </head> <body> <div id="app"> <user-info :user-data="user"></user-info> </div> <script> //全域性元件 let userInfo = Vue.component('userInfo' ,{ name: 'user-info', props: { userData: Object }, data() { return { userName: this.userData.name } }, template: ` <div> <div>姓名:{{userName.text}}</div> <div>性別:{{userData.gender}}</div> <div>生日:{{userData.birthday}}</div> </div> ` }); //Vue例項 new Vue({ el: '#app', data: { user: { name: {text: ''}, gender: '', birthday: '' } }, created(){ this.getUserData(); }, methods:{ getUserData(){ setTimeout(()=>{ this.user = { name: {text: '於永雨'}, gender: '男', birthday: '1991-7' } }, 500) } }, components: { userInfo } }); </script> </body> </html>
執行結果:姓名仍然沒有值,和第一次結果一樣!!!
二、原因
那麼,原因到底是什麼呢?百思不得解,後來和小夥伴們討論時,有人提出:會不會因為data在初始化時深拷貝?
我覺得這種解釋比較靠譜,於是去收集證據,首先去Vue官網翻了一下關於data的文件,其中:
當看到"遞迴地"那個詞,基本上就能斷定上面的推論是正確的,因為深拷貝的核心原理就是遞迴。
原來,Vue初始化時會遞迴地遍歷data所有的屬性,並使用Object.defineProperty把這些屬性全部轉為getter/setter,用於實現雙向繫結。官方文件在Reactivity in Depth一章明確有說:
還順便解釋了一下為什麼Vue不支援IE8的原因:IE8不支援Object.defineProperty。
三、解決辦法
既然因為data深拷貝的原因,data無法隨著props的變化而更新,我們很自然的就想到Vue中有監聽作用的兩個功能:watch、computed。
修改程式碼如下,觀察結果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>解決方案:watch、computed</title>
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script>
</head>
<body>
<div id="app">
<user-info :user-data="user"></user-info>
</div>
<script>
//全域性元件
let userInfo = Vue.component('userInfo' ,{
name: 'user-info',
props: {
userData: Object
},
data() {
return {
userName: this.userData.name
}
},
computed: {
computedUserName(){
return this.userData.name
}
},
watch: {
'userData.name': function (val) {//監聽props中的屬性
this.userName = val;
}
},
template: `
<div>
<div>姓名(watch):{{ userName }}</div>
<div>姓名(computed):{{ computedUserName }}</div>
<div>性別:{{ userData.gender }}</div>
<div>生日:{{ userData.birthday }}</div>
</div>
`
});
//Vue例項
new Vue({
el: '#app',
data: {
user: {
name: '',
gender: '',
birthday: ''
}
},
created(){
this.getUserData();
},
methods:{
getUserData(){
setTimeout(()=>{
this.user = {
name: '於永雨',
gender: '男',
birthday: '1991-7'
}
}, 500)
}
},
components: {
userInfo
}
});
</script>
</body>
</html>
執行結果
完美!!!
四、總結:關於Vue中props的要點
事後又仔細翻了一下關於props的文件:
大概梳理一下: