vue中需要注意的問題總結(上)

木子星兮發表於2018-04-17

文章首發於個人部落格

前言

使用vue的時候經常會遇到一些問題,其實仔細閱讀查閱官方文件,就會發現文件中已提到一些格外需要注意的點; 為了深入的理解官方文件中對這些問題的解釋,查閱了一些資料,再加上自己的理解,整理了一些常見的問題;如果哪方面解釋的不太合理希望各路大神指出;

文章篇幅較長,但是很實用;

目錄

  • 元件裡面, data必須是一個函式
  • vue中$set的使用場景
  • vue生命週期詳解
  • vue元件通訊
  • vue元件之keep-alive
  • 生命週期函式/methods/watch裡面不應該使用箭頭函式
  • methods/computed/watch

1.元件裡面, data必須是一個函式

類比引用資料型別 Object是引用資料型別, 每個元件的data 都是記憶體的同一個地址,一個資料改變了其他也改變了;

那麼用什麼方法可以使每個元件的data相互獨立,不受影響呢?

當一個元件被定義,data 必須宣告為返回一個初始資料物件的函式,因為元件可能被用來建立多個例項。如果 data 仍然是一個純粹的物件,則所有的例項將共享引用同一個資料物件!通過提供 data 函式,每次建立一個新例項後,我們能夠呼叫 data 函式,從而返回初始資料的一個全新副本資料物件。

2.vue中$set的使用場景

場景1:

通過陣列的下標去修改陣列的值,資料已經被修改了,但是不觸發updated函式,檢視不更新,

export default {
    data () {
        return {
            items: ['a', 'b', 'c']
        };
    },
    updated () {
        console.log('資料更新', this.items[0]);
    },
    methods: {
        changeItem1 () {
            this.items[0] = 'x';
            console.log(111, this.items[0]);
        },
        changeItem2 () {
            this.$set(this.items, 0, 'x');
            console.log(222, this.items[0]);
        },
    }
};
複製程式碼

執行changeItem1, 控制檯列印 111 'x', 沒有觸發updated,檢視不更新 執行changeItem2, 控制檯列印 222 'x', 資料更新 'x'; 觸發updated,檢視更新

場景2: vue中檢測不到物件屬性的新增和刪除

data() {
     userProfile: {
        name: '小明',
    }
}
複製程式碼

想要給userProfile加一個age屬性

addProperty () {
     this.userProfile.age = '12';
     console.log(555, this.userProfile);
}
複製程式碼

執行addProperty函式時,列印如下

555 { name: '小明', age: '12'}
複製程式碼

但是沒有觸發updated, 檢視未更新 改成下面這種

addProperty () {
      this.$set(this.userProfile, 'age', '12');
      console.log(666, this.userProfile);
 }
複製程式碼

再次執行, 資料發生變化, 觸發updated, 檢視更新;

有時你想向已有物件上新增多個屬性,例如使用 Object.assign() 或 _.extend() 方法來新增屬性。但是,新增到物件上的新屬性不會觸發更新。在這種情況下可以建立一個新的物件,讓它包含原物件的屬性和新的屬性:

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
複製程式碼

這是vue中很典型的一個問題,使用的時候一定要注意!

簡單的解釋一下原理:

vue在建立例項的時候把data深度遍歷所有屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。所以屬性必須在 data 物件上存在才能讓 Vue 轉換它,這樣才能讓它是響應的。

當你在物件上新加了一個屬性newProperty,當前新加的這個屬性並沒有加入vue檢測資料更新的機制(因為是在初始化之後新增的),vue.$set是能讓vue知道你新增了屬性, 它會給你做處理

3.vue生命週期詳解

1. vue的生命週期

  • beforeCreate: 元件例項剛剛被建立,元件屬性計算之前,如data屬性
  • created: 元件例項建立完成,屬性已繫結,但是DOM還未完成,$el屬性還不存在
  • beforeMount:模板編譯/掛載之前
  • mounted: 模板編譯/掛載之後
  • beforeUpdate: 元件更新之前
  • updated: 元件更新之後
  • activated: for keep-alive,元件被啟用時呼叫
  • deactivated: for keep-alive,元件被移除時呼叫
  • beforeDestroy: 元件銷燬前被呼叫
  • destoryed: 元件銷燬後呼叫

ps:下面程式碼可以直接複製出去執行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<script type="text/javascript" src="https://cdn.jsdelivr.net/vue/2.1.3/vue.js"></script>
<body>
    <div id="app">{{a}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                a: 'vuejs',
            },
            beforeCreate: function() {
                console.log('建立前');
                console.log(this.a);
                console.log(this.$el);
            },
            created: function() {
                console.log('建立之後');
                console.log(this.a);
                console.log(this.$el);
            },
            beforeMount: function() {
                console.log('mount之前');
                console.log(this.a);
                console.log(this.$el);
            },
            mounted: function() {
                console.log('mount之後');
                console.log(this.a);
                console.log(this.$el);
            },
            beforeUpdate: function() {
                console.log('更新之前');
                console.log(this.a);
                console.log(this.$el);
            },
            updated: function() {
                console.log('更新完成');
                console.log(this.a);
                console.log(this.$el);
            },
            beforeDestroy: function() {
                console.log('元件銷燬之前');
                console.log(this.a);
                console.log(this.$el);
            },
            destroyed: function() {
                console.log('元件銷燬之後');
                console.log(this.a);
                console.log(this.$el);
            },
        })
    </script>
</body>
</html>
複製程式碼

beforeCreated: el和data並未初始化 created: 完成data資料的初始化,el沒有 beforeMount: 完成了el和data初始化 mounted: 完成掛載

title

開啟命令列在命令列中輸入vm.a = 'change';檢視效果
複製程式碼

title

4.vue元件通訊

1.父元件給子元件傳遞資料

vue中使用props向子元件傳遞資料 1): 子元件在props中建立一個屬性,用於接收父元件傳過來的值 2): 父元件中註冊子元件 3): 在子元件標籤中新增子元件props中建立的屬性 4): 把需要傳給子元件的值賦給該屬性

2.子元件向父元件傳遞資料

子元件主要通過事件傳遞資料給父元件 1), 子元件中需要以某種方式,例如點選事件的方法來觸發一個自定義事件 2),將需要傳的值作為$emit的第二個引數,該值將作為實引數傳給相應自定義事件的方法 3),在父元件中註冊子元件並在子元件標籤上繫結自定義事件的監聽

3.子元件向子元件傳遞資料

vue找那個沒有直接子元件對子元件傳參的方法,建議將需要傳遞資料的在元件,都合併為一個元件,如果一定需要子元件對子元件傳參,可以先傳到父元件,再傳到子元件,為了方便開發,vue推出了一個狀態管理工具vuex,可以啃方便的實現元件之間的引數傳遞

具體的例項程式碼如下:可以自行參考相關程式碼在編輯器中嘗試 父元件向子元件傳遞資料

// 父元件向子元件傳遞資料
<!--
msg 是在data中(父元件)定義的變數
如果需要從父元件中獲取logo的值,就需要使用props['msg'], 如30行
在props中新增了元素以後,就不需要在data中(子元件)中再新增變數了
-->
<template>
  <div>
    <child  @transferuser="getUser" :msg="msg"></child>  
    <p>使用者名稱為:{{user}}(我是子元件傳遞給父元件的資料)</p>  
  </div>
</template>

<script>
    import child from './child.vue';
    export default {
        components: {
            child,
        },
        data() {
            return {
                user: '',
                msg: '我是父元件傳給子元件的資訊',
            };
        },
        methods: {
            getUser(msg) {
                this.user = msg;
                console.log(msg);
            },
        },
    };
</script>
複製程式碼

子元件向父元件傳遞資料

// 子元件向父元件傳遞資料
<!--
1.@ : 是  v-on的簡寫
2.子元件主要通過事件傳遞資料給父元件
3.當input的值發生變化時,將username傳遞給parent.vue,首先宣告瞭一個setUser,用change事件來呼叫setUser
4.在setUser中,使用了$emit來遍歷transferUser事件,並返回this.username,其中transferuser是一個自定義事件,功能類似一箇中轉,this.username通過這個事件傳遞給父元件
-->
<template>
  <div>
      <div>{{msg}}</div>
      <span>使用者名稱</span>
      <input v-model="username" @change='setUser'>向父元件傳值</button>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                username: '測試',
            };
        },
        props: {
            msg: {
                type: String,
            },
        },
        methods: {
            setUser() {
                this.$emit('transferuser', this.username);
            },
        },
    };
</script>
複製程式碼

5.vue元件之keep-alive

專案中寫vue也沒注意到<keep-alive></keep-alive>這個元件,最近在深入的研究vue元件的生命週期函式,每一個函式都是幹嘛的,然後其中有activateddeactivated這兩個函式與<keep-alive></keep-alive>這個元件有關

  • activated: keep-alive元件啟用時呼叫
  • deactivated: keep-alive元件停用時呼叫

keep-alive用法

  • <keep-alive>包裹動態元件時,會快取不活動的元件例項,而不是銷燬它們
  • <keep-alive>是一個抽象元件:它自身不會渲染一個DOM元素,也不會出現在父元件鏈中
  • 當元件在<keep-alive>內被切換,它的activateddeactivated這兩個生命週期鉤子函式將會被對應執行

具體的例項如下

  • 是一個簡單的tab切換,可以嘗試把<keep-alive>去掉之後,對比一下,然後就會發現它的好處

test.vue

<template>
    <div class="test">
        <div class="testNav">
            <div :class="{'selected':tab === 1,'testTitle':true}" @click="toTab(1)">標題一</div>
            <div :class="{'selected':tab === 2,'testTitle':true}"  @click="toTab(2)">標題二</div>
        </div>
        <div class="container">
            <keep-alive>
                <Test1 v-if="tab === 1">
                </Test1>
                <Test2 v-else>
                </Test2>
            </keep-alive>
        </div>
    </div>
</template>

<script>
    import Test1 from './test1.vue';
    import Test2 from './test2.vue';
    export default {
        data() {
            return {
                tab: 1,
            };
        },
        components: {
            Test1,
            Test2,
        },
        methods: {
            toTab(index) {
                this.tab = index;
            },
        },
    }
</script>

<style lang="less">
.test {
    width: 100%;
    .testNav {
        height: 60px;
        line-height: 60px;
        display: flex;
        border-bottom: 1px solid #e5e5e5;
        .testTitle {
            flex: 1;
            text-align: center;
        }
        .selected {
            color: red;
        }
    }
}
</style>
複製程式碼

測試結果如下: 注意看一下頁面和控制檯輸出的資訊,可以更加直觀的注意到<keep-alive>的作用及activateddeactivated這兩個函式什麼時候會被觸發

  • 開啟頁面,會出現下面這樣
    1

用setTimeout模擬請求後端介面的場景

  • 點選title2,出現下面的情況
    2
  • 再次點選title1,出現下面的情況,你會發現從後端請求的資料會快速顯示出來,但是如果你此時不用
    3

test1.vuetest2.vue的相關程式碼如下:

test1.vue

<template>
  <div class="test1">
      test1
      {{testInfo1}}
  </div>
</template>

<script>
    export default {
        data() {
            return {
                testInfo1: '',
            };
        },
        activated() {
            console.log('測試1被啟用');
        },
        deactivated() {
            console.log('測試1被快取');
        },
        created() {
            setTimeout(() => {
                this.testInfo1 = '這是測試一的資料';
            }, 2000);
        },
    }
</script>

複製程式碼

test2.vue

<template>
  <div>
      test2
      {{testInfo2}}
  </div>
</template>

<script>
    export default {
        data() {
            return {
                testInfo2: '',
            }
        },  
        activated() {
            console.log('測試2被啟用');
        },
        deactivated() {
            console.log('測試2被快取');
        },
        created() {
            setTimeout(() => {
                this.testInfo2 = '這是測試二的資料';
            }, 2000);
        },
    }
</script>
複製程式碼

6. 生命週期函式/methods/watch裡面不應該使用箭頭函式

es6的箭頭函式的出現,是我們可以用更少的程式碼實現功能,但是應該注意箭頭函式和普通函式的最大區別是this的指向問題: 箭頭函式的this指向函式所在的所用域,普通函式的this指向函式的呼叫者;

官方文件中特別提醒中已經指出這一點:

vue中生命週期函式, methods, watch 自動繫結 this 上下文到例項中,因此你可以訪問資料,對屬性和方法進行運算。這意味著 你不能使用箭頭函式來定義一個生命週期方法, 這是因為箭頭函式繫結了父上下文,因此 this 與你期待的 Vue 例項不同

7.methods/computed/watch

methods VS computed

我們可以將同一個函式定義為methods或者computed,用這兩種方式,得到的結果是相同的,不同的是computed是基於它們的依賴進行快取的,計算屬性只有在它相關的依賴發生改變時才重新求值;

適用場景:

重新計算開銷很大的話,選computed; 不希望有快取的選methods

computed vs watch

watch 有新舊值兩個引數, 計算屬性沒有,但是計算屬性可以從setter獲得新值

關於computed

對於計算屬性要特別說明一點: vue的計算屬性computed預設只有getter,需要使用getter的時候需要自己加一個setter

export default {
    data () {
        return {
            firstName: '張',
            lastName: '三',
        };
    },
    computed: {
        fullName() {
              return this.firstName + ' ' + this.lastName
        },
    },
    methods: {
        changeFullName () {
            this.fullName = '李 四';
        }
    },
};

其中computed裡的程式碼完整寫法是  

computed: {
   fullName: {
        // getter
        get: function () {
          return this.firstName + ' ' + this.lastName
        },
   }    
},
複製程式碼

執行 changeFullName 發現報錯[Vue warn]: Computed property "fullame" was assigned to but it has no setter.

我們需要給計算屬性fullName新增一個setter

computed: {
   fullName: {
        // getter
        get: function () {
          return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
          var names = newValue.split(' ')
          this.firstName = names[0]
          this.lastName = names[names.length - 1]
        }
  }    
},
複製程式碼

總結

上述這些問題從vue官方文件中均能找到答案,當然想要更深入的理解為什麼,還需要從vue原始碼分析入手;

下一篇文章打算從原始碼入手去解釋這些問題,理解vue整體的程式設計;

vue系列文章

相關文章