by yugasun from yugasun.com/post/you-ma… 本文可全文轉載,但需要保留原作者和出處。
有了前面文章的鋪墊,相信一路看過來的新手的你開發一箇中型的 Vuejs 應用已經不在話下,包括 Vuejs 生態核心工具(vue-router,vuex)的使用也不成問題。但是在實際專案開發過程中,我們要做的工作不僅僅是完成我們的業務程式碼,當一個需求完成後,我們還需要考慮更多後期優化工作,本篇主要講述程式碼層面的優化。
被忽視的 setter 之計算屬性
我們先回到上一篇的狀態管理案例,使用 vuex
方式共享我們的 msg
屬性,先建立 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>
複製程式碼
同樣對 comp2
做相同修改。當然還得在 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 基本使用,建議先閱讀官方文件。
好了,我們已經實現 msg
的共享了,並且對其變化進行了 watch
,在輸入框發生改變時,通過 $store.dispatch
來觸發相應 UPDATE_MSG
actions 操作,實現狀態修改。但是你會發現修改 comp1
中的輸入框,通過 vue-devtools
也可檢視到 Vuex 中的的 state.msg
的確也跟著變了,但是 comp2
中輸入框並沒有發生改變,當然這因為我們初始化 msg
時,是直接變數賦值,並未監聽 $store.state.msg
的變化,所以兩個元件沒法實現同步。
有人又會說了,再新增個 watch
屬性,監聽 $store.state.msg
改變,重新賦值元件中的 msg
不就行了,確實可以實現,但是這樣程式碼是不是不太優雅,為了一個簡單的 msg
同步,我們需要給 data
新增屬性,外加兩個監聽器,是不是太不划算?
其實這裡是可以通過計算屬性很好地解決的,因為元件中的 msg
就是依賴 $store.state.msg
的,我們直接定義計算屬性 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>
複製程式碼
我們再次修改 comp1
中的輸入框,開啟控制檯,會報如下錯誤:
vue.esm.js?efeb:591 [Vue warn]: Computed property "msg" was assigned to but it has no setter.
...
複製程式碼
因為我們使用的是 v-model
來繫結 msg
到 input 上的,當輸入框改變,必然觸發 msg
的 setter(賦值)
操作,但是計算屬性預設會幫我定義好 getter
,並未定義 setter
,這就是為什麼會出現上面錯誤提示的原因,那麼我們再自定義下 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
值得時候,將其新值傳遞到我們的 vuex
中,這樣豈不是一舉兩得了。同樣的對 comp2
做相同修改。執行專案,你會發祥,comp1 輸入框的值
、comp2 輸入框的值
和 store 中的值
實現同步更新了。而且相對與上面的方案,程式碼量也精簡了很多~
可配置的 watch
先來看段程式碼:
// ...
watch: {
username() {
this.getUserInfo();
},
},
methods: {
getUserInfo() {
const info = {
username: 'yugasun',
site: 'yugasun.com',
};
/* eslint-disable no-console */
console.log(info);
},
},
created() {
this.getUserInfo();
},
// ...
複製程式碼
這裡很好理解,元件建立的時候,獲取使用者資訊,然後監聽使用者名稱,一旦發生變化就重新獲取使用者資訊,這個場景在實際開發中非常常見。那麼能不能再優化下呢?
答案是肯定的。其實,我們在 Vue 例項中定義 watcher
的時候,監聽屬性可以是個物件的,它含有三個屬性: deep
、immediate
、handler
,我們通常直接以函式的形式定義時,Vue 內部會自動將該回撥函式賦值給 handler
,而剩下的兩個屬性值會預設設定為 false
。這裡的場景就可以用到 immediate
屬性,將其設定為 true
時,表示建立元件時 handler
回撥會立即執行,這樣我們就可以省去在 created
函式中再次呼叫了,實現如下:
watch: {
username: {
immediate: true,
handler: 'getUserInfo',
},
},
methods: {
getUserInfo() {
const info = {
username: 'yugasun',
site: 'yugasun.com',
};
/* eslint-disable no-console */
console.log(info);
},
},
複製程式碼
Url改變但元件未變時,created 無法觸發的問題
首先預設專案路由是通過 vue-router 實現的,其次我們的路由是類似下面這樣的:
// ...
const routes = [
{
path: '/',
component: Index,
},
{
path: '/:id',
component: Index,
},
];
複製程式碼
公用的元件 src/views/index.vue
程式碼如下:
<template>
<div class="index">
<router-link :to="{path: '/1'}">挑戰到第二頁</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;
// 模擬請求
setTimeout(() => {
if (id) {
this.username = 'Yuga Sun';
} else {
this.username = 'yugasun';
}
}, 300);
},
},
created() {
this.getName();
},
};
</script>
複製程式碼
兩個不同路徑使用的是同一個元件 Index
,然後 Index 元件中的 getName
函式會在 created
的時候執行,你會發現,讓我們切換路由到 /1
時,我們的頁面並未改變,created
也並未重新觸發。
這是因為
vue-router
會識別出這兩個路由使用的是同一個元件,然後會進行復用,所以並不會重新建立元件,那麼created
周期函式自然也不會觸發。
通常解決辦法就是新增 watcher
監聽 $route
的變化,然後重新執行 getName
函式。程式碼如下:
watch: {
$route: {
immediate: true,
handler: 'getName',
},
},
methods: {
getName() {
const id = this.$route.params.id;
// 模擬請求
setTimeout(() => {
if (id) {
this.username = 'Yuga Sun';
} else {
this.username = 'yugasun';
}
}, 300);
},
},
複製程式碼
ok,問題是解決了,但是有沒有其他不用改動 index.vue
的偷懶方式呢?
就是給 router-view
新增一個 key
屬性,這樣即使是相同元件,但是如果 url
變化了,Vuejs就會重新建立這個元件。我們直接修改 src/App.vue
中的 router-view
如下:
<router-view :key="$route.fullPath"></router-view>
複製程式碼
被遺忘的 $attrs
大多數情況下,從父元件向子元件傳遞資料的時候,我們都是通過 props
實現的,比如下面這個例子:
<!-- 父元件中 -->
<Comp3
:value="value"
label="使用者名稱"
id="username"
placeholder="請輸入使用者名稱"
@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>
複製程式碼
這樣一階元件,實現起來很簡單,也沒什麼問題,我們只需要在子元件的 props
中寫一遍 id, value, placeholder...
這樣的屬性定義就可以了。但是如果子元件又包含了子元件,而且同樣需要傳遞 id, value, placeholder...
呢?甚至三階、四階...呢?那麼就需要我們在 props
中重複定義很多遍了,這怎麼能忍呢?
於是 vm.$attrs 可以閃亮登場了,先來看官方解釋:
包含了父作用域中不作為 prop 被識別 (且獲取) 的特性繫結 (class 和 style 除外)。當一個元件沒有宣告任何 prop 時,這裡會包含所有父作用域的繫結 (class 和 style 除外),並且可以通過 v-bind="$attrs" 傳入內部元件—— 在建立高階別的元件時非常有用。
作者還特別強調了 在建立高階別的元件時非常有用
,他就是為了解決剛才我提到的問題的。它也沒什麼難度,那麼趕緊用起來吧,程式碼修改如下:
<!-- 父元件中 -->
<Comp3
:value="value"
label="使用者名稱"
id="username"
placeholder="請輸入使用者名稱"
@input="handleInput"
>
<!-- 子元件中 -->
<template>
<label>
{{ $attrs.label }}
<input
v-bind="$attrs"
@input="$emit('input', $event.target.value)"
/>
</label>
</template>
<script>
export default {
}
</script>
複製程式碼
這樣看起來是不是清爽多了,而且就運算元元件中再次引用類似的子元件,我們也不怕了。因為有了 $attrs
,哪裡不會點哪裡......
總結
當然 Vuejs 的實踐技巧遠不止如此,這裡只是總結了個人在實際開發中遇到的,而且正好是很多朋友容易忽視的地方。如果你有更好的實踐方法,歡迎評論或者發郵件給我,一起交流學習。