by yugasun from https://yugasun.com/post/you-may-not-know-vuejs-13.html
本文可全文轉(zhuǎn)載食棕,但需要保留原作者和出處。
有了前面文章的鋪墊柬讨,相信一路看過來的新手的你開發(fā)一個(gè)中型的 Vuejs 應(yīng)用已經(jīng)不在話下罚舱,包括 Vuejs 生態(tài)核心工具(vue-router继薛,vuex)的使用也不成問題。但是在實(shí)際項(xiàng)目開發(fā)過程中诫隅,我們要做的工作不僅僅是完成我們的業(yè)務(wù)代碼,當(dāng)一個(gè)需求完成后帐偎,我們還需要考慮更多后期優(yōu)化工作逐纬,本篇主要講述代碼層面的優(yōu)化。
被忽視的 setter 之計(jì)算屬性
我們先回到上一篇的狀態(tài)管理案例肮街,使用 vuex
方式共享我們的 msg
屬性风题,先創(chuàng)建 src/store/index.js
:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const types = {
UPDATE_MSG: 'UPDATE_MSG',
};
const mutations = {
[types.UPDATE_MSG](state, payload) {
state.msg = payload.msg;
},
};
const actions = {
[types.UPDATE_MSG]({ commit }, payload) {
commit(types.UPDATE_MSG, payload);
},
};
export default new Vuex.Store({
state: {
msg: 'Hello world',
},
mutations,
actions,
});
然后在組件 comp1
中使用它:
<template>
<div class="comp1">
<h1>Component 1</h1>
<input type="text" v-model="msg">
</div>
</template>
<script>
export default {
name: 'comp1',
data() {
const msg = this.$store.state.msg;
return {
msg,
};
},
watch: {
msg(val) {
this.$store.dispatch('UPDATE_MSG', { msg: val });
},
},
};
</script>
同樣對(duì) comp2
做相同修改。當(dāng)然還得在 src/main.js
中引入:
import Vue from 'vue';
import App from './App';
import store from './store';
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
store,
el: '#app',
template: '<App/>',
components: { App },
});
如果還不知道 vuex 基本使用嫉父,建議先閱讀官方文檔沛硅。
好了,我們已經(jīng)實(shí)現(xiàn) msg
的共享了绕辖,并且對(duì)其變化進(jìn)行了 watch
摇肌,在輸入框發(fā)生改變時(shí),通過 $store.dispatch
來觸發(fā)相應(yīng) UPDATE_MSG
actions 操作仪际,實(shí)現(xiàn)狀態(tài)修改围小。但是你會(huì)發(fā)現(xiàn)修改 comp1
中的輸入框,通過 vue-devtools
也可查看到 Vuex 中的的 state.msg
的確也跟著變了树碱,但是 comp2
中輸入框并沒有發(fā)生改變肯适,當(dāng)然這因?yàn)槲覀兂跏蓟?msg
時(shí),是直接變量賦值成榜,并未監(jiān)聽 $store.state.msg
的變化框舔,所以兩個(gè)組件沒法實(shí)現(xiàn)同步。
有人又會(huì)說了赎婚,再添加個(gè) watch
屬性刘绣,監(jiān)聽 $store.state.msg
改變,重新賦值組件中的 msg
不就行了挣输,確實(shí)可以實(shí)現(xiàn)纬凤,但是這樣代碼是不是不太優(yōu)雅,為了一個(gè)簡(jiǎn)單的 msg
同步撩嚼,我們需要給 data
添加屬性停士,外加兩個(gè)監(jiān)聽器,是不是太不劃算完丽?
其實(shí)這里是可以通過計(jì)算屬性很好地解決的向瓷,因?yàn)榻M件中的 msg
就是依賴 $store.state.msg
的,我們直接定義計(jì)算屬性 msg
舰涌,然后返回不就可以了。
ok你稚,修改 comp1
如下:
<template>
<div class="comp1">
<h1>Component 1</h1>
<input type="text" v-model="msg">
</div>
</template>
<script>
export default {
name: 'comp1',
computed: {
msg() {
return this.$store.state.msg;
},
},
};
</script>
我們?cè)俅涡薷?comp1
中的輸入框瓷耙,打開控制臺(tái)朱躺,會(huì)報(bào)如下錯(cuò)誤:
vue.esm.js?efeb:591 [Vue warn]: Computed property "msg" was assigned to but it has no setter.
...
因?yàn)槲覀兪褂玫氖?v-model
來綁定 msg
到 input 上的,當(dāng)輸入框改變搁痛,必然觸發(fā) msg
的 setter(賦值)
操作长搀,但是計(jì)算屬性默認(rèn)會(huì)幫我定義好 getter
,并未定義 setter
鸡典,這就是為什么會(huì)出現(xiàn)上面錯(cuò)誤提示的原因源请,那么我們?cè)僮远x下 setter
吧:
<template>
<div class="comp1">
<h1>Component 1</h1>
<input type="text" v-model="msg">
</div>
</template>
<script>
export default {
name: 'comp1',
computed: {
msg: {
get() {
return this.$store.state.msg;
},
set(val) {
this.$store.dispatch('UPDATE_MSG', { msg: val });
},
},
},
};
</script>
可以看到,我們正好可以在 setter
中彻况,也就是修改 msg
值得時(shí)候谁尸,將其新值傳遞到我們的 vuex
中,這樣豈不是一舉兩得了纽甘。同樣的對(duì) comp2
做相同修改良蛮。運(yùn)行項(xiàng)目,你會(huì)發(fā)祥悍赢,comp1 輸入框的值
决瞳、comp2 輸入框的值
和 store 中的值
實(shí)現(xiàn)同步更新了。而且相對(duì)與上面的方案左权,代碼量也精簡(jiǎn)了很多~
可配置的 watch
先來看段代碼:
// ...
watch: {
username() {
this.getUserInfo();
},
},
methods: {
getUserInfo() {
const info = {
username: 'yugasun',
site: 'yugasun.com',
};
/* eslint-disable no-console */
console.log(info);
},
},
created() {
this.getUserInfo();
},
// ...
這里很好理解皮胡,組件創(chuàng)建的時(shí)候,獲取用戶信息赏迟,然后監(jiān)聽用戶名屡贺,一旦發(fā)生變化就重新獲取用戶信息,這個(gè)場(chǎng)景在實(shí)際開發(fā)中非常常見瀑梗。那么能不能再優(yōu)化下呢烹笔?
答案是肯定的。其實(shí)抛丽,我們?cè)?Vue 實(shí)例中定義 watcher
的時(shí)候谤职,監(jiān)聽屬性可以是個(gè)對(duì)象的,它含有三個(gè)屬性: deep
亿鲜、immediate
允蜈、handler
,我們通常直接以函數(shù)的形式定義時(shí)蒿柳,Vue 內(nèi)部會(huì)自動(dòng)將該回調(diào)函數(shù)賦值給 handler
饶套,而剩下的兩個(gè)屬性值會(huì)默認(rèn)設(shè)置為 false
。這里的場(chǎng)景就可以用到 immediate
屬性垒探,將其設(shè)置為 true
時(shí)妓蛮,表示創(chuàng)建組件時(shí) handler
回調(diào)會(huì)立即執(zhí)行,這樣我們就可以省去在 created
函數(shù)中再次調(diào)用了圾叼,實(shí)現(xiàn)如下:
watch: {
username: {
immediate: true,
handler: 'getUserInfo',
},
},
methods: {
getUserInfo() {
const info = {
username: 'yugasun',
site: 'yugasun.com',
};
/* eslint-disable no-console */
console.log(info);
},
},
Url改變但組件未變時(shí)蛤克,created 無法觸發(fā)的問題
首先默認(rèn)項(xiàng)目路由是通過 vue-router 實(shí)現(xiàn)的捺癞,其次我們的路由是類似下面這樣的:
// ...
const routes = [
{
path: '/',
component: Index,
},
{
path: '/:id',
component: Index,
},
];
公用的組件 src/views/index.vue
代碼如下:
<template>
<div class="index">
<router-link :to="{path: '/1'}">挑戰(zhàn)到第二頁(yè)</router-link><br/>
<router-link v-if="$route.path === '/1'" :to="{path: '/'}">返回</router-link>
<h3>{{ username }} </h3>
</div>
</template>
<script>
export default {
name: 'Index',
data() {
return {
username: 'Loading...',
};
},
methods: {
getName() {
const id = this.$route.params.id;
// 模擬請(qǐng)求
setTimeout(() => {
if (id) {
this.username = 'Yuga Sun';
} else {
this.username = 'yugasun';
}
}, 300);
},
},
created() {
this.getName();
},
};
</script>
兩個(gè)不同路徑使用的是同一個(gè)組件 Index
,然后 Index 組件中的 getName
函數(shù)會(huì)在 created
的時(shí)候執(zhí)行构挤,你會(huì)發(fā)現(xiàn)髓介,讓我們切換路由到 /1
時(shí),我們的頁(yè)面并未改變筋现,created
也并未重新觸發(fā)唐础。
這是因?yàn)?
vue-router
會(huì)識(shí)別出這兩個(gè)路由使用的是同一個(gè)組件,然后會(huì)進(jìn)行復(fù)用矾飞,所以并不會(huì)重新創(chuàng)建組件一膨,那么created
周期函數(shù)自然也不會(huì)觸發(fā)。
通常解決辦法就是添加 watcher
監(jiān)聽 $route
的變化凰慈,然后重新執(zhí)行 getName
函數(shù)汞幢。代碼如下:
watch: {
$route: {
immediate: true,
handler: 'getName',
},
},
methods: {
getName() {
const id = this.$route.params.id;
// 模擬請(qǐng)求
setTimeout(() => {
if (id) {
this.username = 'Yuga Sun';
} else {
this.username = 'yugasun';
}
}, 300);
},
},
ok,問題是解決了微谓,但是有沒有其他不用改動(dòng) index.vue
的偷懶方式呢森篷?
就是給 router-view
添加一個(gè) key
屬性,這樣即使是相同組件豺型,但是如果 url
變化了仲智,Vuejs就會(huì)重新創(chuàng)建這個(gè)組件。我們直接修改 src/App.vue
中的 router-view
如下:
<router-view :key="$route.fullPath"></router-view>
被遺忘的 $attrs
大多數(shù)情況下姻氨,從父組件向子組件傳遞數(shù)據(jù)的時(shí)候钓辆,我們都是通過 props
實(shí)現(xiàn)的,比如下面這個(gè)例子:
<!-- 父組件中 -->
<Comp3
:value="value"
label="用戶名"
id="username"
placeholder="請(qǐng)輸入用戶名"
@input="handleInput"
>
<!-- 子組件中 -->
<template>
<label>
{{ label }}
<input
:id="id"
:value="value"
:placeholder="placeholder"
@input="$emit('input', $event.target.value)"
/>
</label>
</template>
<script>
export default {
props: {
id: {
type: String,
default: 'username',
},
value: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
},
}
</script>
這樣一階組件肴焊,實(shí)現(xiàn)起來很簡(jiǎn)單前联,也沒什么問題,我們只需要在子組件的 props
中寫一遍 id, value, placeholder...
這樣的屬性定義就可以了娶眷。但是如果子組件又包含了子組件似嗤,而且同樣需要傳遞 id, value, placeholder...
呢?甚至三階届宠、四階...呢烁落?那么就需要我們?cè)?props
中重復(fù)定義很多遍了,這怎么能忍呢豌注?
于是 vm.$attrs 可以閃亮登場(chǎng)了伤塌,先來看官方解釋:
包含了父作用域中不作為 prop 被識(shí)別 (且獲取) 的特性綁定 (class 和 style 除外)。當(dāng)一個(gè)組件沒有聲明任何 prop 時(shí)轧铁,這里會(huì)包含所有父作用域的綁定 (class 和 style 除外)每聪,并且可以通過 v-bind="$attrs" 傳入內(nèi)部組件—— 在創(chuàng)建高級(jí)別的組件時(shí)非常有用。
作者還特別強(qiáng)調(diào)了 在創(chuàng)建高級(jí)別的組件時(shí)非常有用
,他就是為了解決剛才我提到的問題的熊痴。它也沒什么難度他爸,那么趕緊用起來吧,代碼修改如下:
<!-- 父組件中 -->
<Comp3
:value="value"
label="用戶名"
id="username"
placeholder="請(qǐng)輸入用戶名"
@input="handleInput"
>
<!-- 子組件中 -->
<template>
<label>
{{ $attrs.label }}
<input
v-bind="$attrs"
@input="$emit('input', $event.target.value)"
/>
</label>
</template>
<script>
export default {
}
</script>
這樣看起來是不是清爽多了果善,而且就算子組件中再次引用類似的子組件,我們也不怕了系谐。因?yàn)橛辛?$attrs
巾陕,哪里不會(huì)點(diǎn)哪里......
總結(jié)
當(dāng)然 Vuejs 的實(shí)踐技巧遠(yuǎn)不止如此,這里只是總結(jié)了個(gè)人在實(shí)際開發(fā)中遇到的纪他,而且正好是很多朋友容易忽視的地方鄙煤。如果你有更好的實(shí)踐方法,歡迎評(píng)論或者發(fā)郵件給我茶袒,一起交流學(xué)習(xí)梯刚。