1. 程式人生 > >Vue中用props給data賦初始值遇到的問題

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>

程式碼解讀:

  1. 根元件data中有一個物件:user,包含三個屬性:name、gender、birthday,初始值都為空字串
  2. 模擬api非同步請求,500毫秒後對user的重新賦值,三個屬性都不再為空
  3. 宣告一個子元件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的文件,其中:

clipboard.png

當看到"遞迴地"那個詞,基本上就能斷定上面的推論是正確的,因為深拷貝的核心原理就是遞迴

原來,Vue初始化時會遞迴地遍歷data所有的屬性,並使用Object.defineProperty把這些屬性全部轉為getter/setter,用於實現雙向繫結。官方文件在Reactivity in Depth一章明確有說:

clipboard.png

還順便解釋了一下為什麼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>

執行結果

clipboard.png

完美!!!

四、總結:關於Vue中props的要點

事後又仔細翻了一下關於props的文件:

clipboard.png

大概梳理一下:

1.props是單向資料流:父元件的資料變化,通過props實時反應在子元件中,反之不然

2.不允許在子元件中直接操作props

3.可以變相操作props

(1)在data中宣告區域性變數,並用props初始化,弊端:區域性變數不隨著props更新而更新

(2)在computed中對props值轉換後輸出