不可錯(cuò)過的 vue 開發(fā)技巧
原創(chuàng)【51CTO.com原創(chuàng)稿件】
前言
本文主要介紹日常項(xiàng)目開發(fā)過程中的一些技巧,幫助大家規(guī)避錯(cuò)誤的同時(shí)還能提高應(yīng)用的性能。以下是我總結(jié)的一些平時(shí)工作中的經(jīng)驗(yàn)。
在v-if/v-if-else/v-else中使用key
如果一組v-if 與v-else的元素類型相同,最好使用屬性key。這是因?yàn)閂ue2.0引入虛擬DOM,為了避免不必要的DOM操作,虛擬DOM在虛擬節(jié)點(diǎn)映射到視圖過程中,將虛擬節(jié)點(diǎn)與上一次渲染視圖所使用的舊虛擬節(jié)點(diǎn)做對(duì)比,找出真正需要更新的節(jié)點(diǎn)來進(jìn)行DOM操作。但有時(shí)如果兩個(gè)本不相同的節(jié)點(diǎn)被識(shí)別為相同,便會(huì)出現(xiàn)意料之外的問題。我們看下面的一個(gè)例子:
- // 這種寫法會(huì)出bug
- <div v-if="flag">
- <label>浪里行舟</label>
- <input type="text" />
- </div>
- <div v-else>
- <label>前端工匠</label>
- <input type="text" />
- </div>
- <button @click="flag = !flag">切換</button>
如果添加了屬性key,那么在對(duì)比虛擬DOM時(shí),則會(huì)認(rèn)為它們是兩個(gè)不同的節(jié)點(diǎn),于是會(huì)將舊元素移除并在相同位置添加新元素,從而避免漏洞的出現(xiàn)。
- // 最佳寫法
- <div v-if="flag">
- <label>浪里行舟</label>
- <input key="1" type="text" />
- </div>
- <div v-else>
- <label>前端工匠</label>
- <input key="2" type="text" />
- </div>
- <button @click="flag = !flag">切換</button>
v-for循環(huán)中不要使用index作為key
我們會(huì)給列表渲染設(shè)置屬性key,這個(gè)key屬性主要用在虛擬DOM算法上,在對(duì)比新舊虛擬節(jié)點(diǎn)時(shí)辨識(shí)虛擬節(jié)點(diǎn)。但如果key用得不合理,就會(huì)出現(xiàn)bug,比如我們使用index作為key(見下面例子),核心代碼如下:
- <div class="border">
- <Children v-for="(key, index) in list" :key="index">//使用index作為key
- <button @click="() => handleDelete(key)">刪除</button>
- </Children>
- <button @click="handleAdd">添加</button>
- </div>
- ......
- handleAdd() {
- this.list.push(key++);
- },
- handleDelete(key) {
- const index = this.list.findIndex(k => k === key);
- this.list.splice(index, 1);
- }
上例中,我們想刪除第二個(gè)輸入框,卻誤刪了第三個(gè)輸入框,這因?yàn)楫?dāng)使用splice()方法刪除數(shù)組的某個(gè)元素時(shí)數(shù)組的index會(huì)被重新索引,造成數(shù)組的最后一個(gè)index丟失,從而會(huì)使虛擬DOM的最后一個(gè)結(jié)點(diǎn)(key)丟失,造成無論刪除哪個(gè)結(jié)點(diǎn)都會(huì)誤刪除最后一個(gè)結(jié)點(diǎn)的bug。但如果我們使用傳入的key作為key,就可以避免這種問題出現(xiàn)。
- <div class="border">
- <Children v-for="key in list" :key="key">
- <button @click="() => handleDelete(key)">刪除</button>
- </Children>
- <button @click="handleAdd">添加</button>
- </div>
簡單暴力的router key
我們?cè)陧?xiàng)目開發(fā)時(shí),可能會(huì)遇到這樣問題:當(dāng)頁面切換到同一個(gè)路由但不同參數(shù)地址時(shí),比如/detail/1,跳轉(zhuǎn)到/detail/2,頁面跳轉(zhuǎn)后數(shù)據(jù)竟然沒更新?路由配置如下:
- {
- path: "/detail/:id",
- name:"detail",
- component: Detail
- }
這是因?yàn)関ue-router會(huì)識(shí)別出兩個(gè)路由使用的是同一個(gè)組件從而進(jìn)行復(fù)用,并不會(huì)重新創(chuàng)建組件,而且組件的生命周期鉤子自然也不會(huì)被觸發(fā),導(dǎo)致跳轉(zhuǎn)后數(shù)據(jù)沒有更新。那我們?nèi)绾谓鉀Q這個(gè)問題呢? 我們可以為router-view組件添加屬性key,例子如下:
- <router-view :key="$route.fullpath"></router-view>
這種辦法主要是利用虛擬DOM在渲染時(shí)候通過key來對(duì)比兩個(gè)節(jié)點(diǎn)是否相同,如果key不相同,就會(huì)判定router-view組件是一個(gè)新節(jié)點(diǎn),從而先銷毀組件,然后再重新創(chuàng)建新組件,這樣組件內(nèi)的生命周期會(huì)重新觸發(fā)。
路由懶加載
當(dāng)打包構(gòu)建應(yīng)用時(shí),JavaScript 包會(huì)變得非常大,影響頁面加載。如果我們能把不同路由對(duì)應(yīng)的組件分割成不同的代碼塊,然后當(dāng)路由被訪問的時(shí)候才加載對(duì)應(yīng)組件,這樣就更加高效了。 配合webpack支持的路由懶加載方法有:
-
這種方法比較通用,而且支持性好
- resolve => require([./Foo], resolve)
-
這種寫法是官方文檔推薦的,如下:
- const Foo = () => import('./Foo')
接下來我們以官方文檔的寫法為例,對(duì)比這兩種寫法:
- // 非懶加載寫法
- import Vue from 'vue'
- import Router from 'vue-router'
- import Home from 'pages/home'
- ...
-
- Vue.use(Router)
-
- export default new Router({
- routes: [
- {
- path: '/',
- name: 'home',
- component: Home
- }
- ...
- ]
- })
推薦以下寫法,路由懶加載可以幫我們?cè)谶M(jìn)入首屏?xí)r不用加載過多的資源,從而減少首屏加載速度。
- // 路由懶加載寫法
- import Vue from 'vue'
- import Router from 'vue-router'
-
- // 其它都不用變,就是這么簡單
- const Home = () => import('./home')
- ...
-
- Vue.use(Router)
-
- export default new Router({
- routes: [
- {
- path: '/',
- name: 'home',
- component: Home
- }
- ...
- ]
- })
不要在使用v-for的同一元素上使用v-if
Vue官方文檔強(qiáng)烈建議永遠(yuǎn)不要把 v-if 和 v-for 同時(shí)用在同一個(gè)元素上。一般我們?cè)趦煞N常見的情況下會(huì)傾向于這樣做:
-
1)為了過濾一個(gè)列表中的項(xiàng)目 (比如
v-for="user in users" v-if="user.isActive"
)。在這種情形下,請(qǐng)將 users 替換為一個(gè)計(jì)算屬性 (比如 activeUsers),讓其返回過濾后的列表(見下面例子)。
- // 第一種情形 反例
- <ul>
- <li
- v-for="user in users"
- v-if="user.isActive"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
當(dāng) Vue 處理指令時(shí),v-for 比 v-if 具有更高的優(yōu)先級(jí),所以哪怕我們只渲染出一小部分用戶的元素,也得在每次重渲染的時(shí)候遍歷整個(gè)列表,不論活躍用戶是否發(fā)生了變化。我們可通過將其更換為在如下的一個(gè)計(jì)算屬性上遍歷:
- // 好例子
- computed: {
- activeUsers: function () {
- return this.users.filter(function (user) {
- return user.isActive
- })
- }
- }
- <ul>
- <li
- v-for="user in activeUsers"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
-
2)為了避免渲染本應(yīng)該被隱藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。這種情形下,請(qǐng)將 v-if 移動(dòng)至容器元素上 (比如 ul, ol)(見下面例子)。
- // 第二種情形 反例
- <ul>
- <li
- v-for="user in users"
- v-if="shouldShowUsers"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
更新為:
- // 好例子
- <ul v-if="shouldShowUsers">
- <li
- v-for="user in users"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
過濾器讓數(shù)據(jù)處理更便利
Vue.js 允許你自定義過濾器,它的用法其實(shí)是很簡單,但是可能有些朋友沒有用過,接下來我們介紹下:
1.理解過濾器
-
功能:對(duì)要顯示的數(shù)據(jù)進(jìn)行特定格式化后再顯示。
-
注意:過濾器并沒有改變?cè)镜臄?shù)據(jù),需要對(duì)展現(xiàn)的數(shù)據(jù)進(jìn)行包裝。
-
使用場景:雙花括號(hào)插值和 v-bind 表達(dá)式 (后者從 2.1.0+ 開始支持)。
2.定義過濾器
可以在一個(gè)組件的選項(xiàng)中定義本地的過濾器:
- filters: {
- capitalize: function (value) {
- if (!value) return ''
- value = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- }
- }
也可以在創(chuàng)建 Vue 實(shí)例之前全局定義過濾器:
- Vue.filter('capitalize', function (value) {
- if (!value) return ''
- value = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- })
3.使用過濾器
使用方法也簡單,即在雙花括號(hào)中使用管道符(pipeline) |隔開:
- <!-- 在雙花括號(hào)中 -->
- <div>{{ myData| filterName}}</div>
- <div>{{ myData| filterName(arg)}}</div>
- <!-- 在 v-bind 中 -->
- <div v-bind:id="rawId | formatId"></div>
接下來我們看個(gè)如何使用過濾器格式化日期的例子:
- <div>
- <h2>顯示格式化的日期時(shí)間</h2>
- <p>{{ date }}</p>
- <p>{{ date | filterDate }}</p>
- <p>年月日: {{ date | filterDate("YYYY-MM-DD") }}</p>
- </div>
- ......
- filters: {
- filterDate(value, format = "YYYY-MM-DD HH:mm:ss") {
- console.log(this)//undefined 過濾器沒有this指向的
- return moment(value).format(format);
- }
- },
- data() {
- return {
- date: new Date()
- };
- }
能用computed的盡量用computed代替 watch
很多時(shí)候頁面會(huì)出現(xiàn) watch 的濫用而導(dǎo)致一系列問題的產(chǎn)生,而通常更好的辦法是使用 computed 屬性,先從一張圖區(qū)別它們有什么區(qū)別?
從上面流程圖中,我們可以看出它們之間的區(qū)別:
-
watch:監(jiān)測的是屬性值, 只要屬性值發(fā)生變化,其都會(huì)觸發(fā)執(zhí)行回調(diào)函數(shù)來執(zhí)行一系列操作。
-
computed:監(jiān)測的是依賴值,依賴值不變的情況下其會(huì)直接讀取緩存進(jìn)行復(fù)用,變化的情況下才會(huì)重新計(jì)算。
除此之外,有點(diǎn)很重要的區(qū)別是:計(jì)算屬性不能執(zhí)行異步任務(wù),計(jì)算屬性必須同步執(zhí)行。也就是說計(jì)算屬性不能向服務(wù)器請(qǐng)求或者執(zhí)行異步任務(wù)。如果遇到異步任務(wù),就交給偵聽屬性。watch也可以檢測computed屬性。總而言之,兩者的區(qū)別歸納為以下兩句話:
-
computed能做的,watch都能做,反之則不行
-
能用computed的盡量用computed
為啥提倡使用 computed 代替 watch,這是因?yàn)橛袝r(shí)候可以實(shí)現(xiàn)同樣的效果,而 computed 會(huì)更勝一籌,比如在處理多數(shù)據(jù)聯(lián)動(dòng)的情況下,使用 computed 會(huì)更加合理一點(diǎn)。
- <template>
- <div>
- <input type="text" v-model="firstName">
- <input type="text" v-model="lastName">
- <span>{{ fullName }}</span>
- <span>{{ fullName2 }}</span>
- </div>
- </template>
-
- <script>
- export default {
- data() {
- reurn {
- firstName: '',
- lastName: '',
- fullName2: ''
- }
- },
- // 使用 computed
- computed: {
- fullName() {
- return this.firstName + ' ' + this.lastName
- }
- },
- // 使用 watch
- watch: {
- firstName: function(newVal, oldVal) {
- this.fullName2 = newVal + ' ' + this.lastName;
- },
- lastName: function(newVal, oldVal) {
- this.fullName2 = this.firstName + ' ' + newVal;
- },
- }
- }
- </script>
化繁為簡的Watchers
如果我們需要在組件初始化以及偵聽屬性變化時(shí)調(diào)用同一個(gè)方法,通常的做法像下面這樣:
- watch: {
- myProperty() {
- this.doSomething();
- }
- },
- created() {
- this.doSomething();
- },
- methods: {
- doSomething() {
- console.log('doing something...');
- }
- }
盡管上面這段代碼看起來沒什么問題,但created鉤子里面執(zhí)行的方法是多余的。我們可以把所需要執(zhí)行的方法放到watch里面,避免在created鉤子里寫重復(fù)代碼,那將會(huì)在組件實(shí)例化的時(shí)候觸發(fā)多一次。 那如何優(yōu)化呢?代碼如下:
- watch: {
- myProperty: {
- immediate: true,//表示創(chuàng)建組件時(shí)立馬執(zhí)行一次
- handler() {
- console.log('doing something...'); // 只用一次的方法沒必要在methods里面聲明了
- }
- }
- }
參考文章與書籍
作者介紹
浪里行舟,慕課網(wǎng)認(rèn)證作者,前端愛好者,立志往全棧工程師發(fā)展,從事前端一年多,目前技術(shù)棧有vue全家桶、ES6以及l(fā)ess等,樂于分享,最近一年寫了五六十篇原創(chuàng)技術(shù)文章,得到諸多好評(píng)!
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】