譯文地址
更新于2016年11月:這篇文章的代碼寫于2015年12月豌拙,并且使用的是非常老的vuex api版本。
但是蒜焊,這篇文章仍然很有價值匀谣,它深入分析了為什么vuex是如此的重要,vuex的工作原理以及vuex是如何讓你的應(yīng)用更加出色并易于保存武翎。
Vuex是由Vue.js作者創(chuàng)作的一種開發(fā)中的原型庫烈炭,它可以幫助你以一種更加可持續(xù)的方式構(gòu)建大型應(yīng)用,它的規(guī)則類似于Facebook的Flux庫(隨后被redux等社區(qū)迭代)宝恶。
相對于直接跳入vuex并開始使用它符隙,在本文中,我會解釋為什么vuex在可替代方案中是如此的受歡迎和vuex對你而言有什么價值垫毙。
我們要構(gòu)建的是什么霹疫?
我們要構(gòu)建的簡單應(yīng)用包含了一個按鈕和一個計數(shù)器。按下按鈕會增加計數(shù)器综芥。這個任務(wù)可以很容易的幫助我們理解以下概念丽蝎。
在這個應(yīng)用中有兩個組件:
- 一個按鈕(這是事件源)
- 一個計數(shù)器(這里會根據(jù)原始事件反映更新)
這兩個組件互不相識并且無法通信,哪怕在一些非常小的web應(yīng)用中膀藐,這都是一個非常常見的模型屠阻。在大型應(yīng)用中有大量的組件需要互相通信并且保持彼此連接,以下是一個基本的todo list的流程互動:
這篇文章的目標(biāo)
我們將使用三種方式來探索解決相同的問題:
- 使用事件廣播以使得組件間通信
- 使用共享的state對象
- 使用vuex
讀完本文后额各,希望你能夠理解:
- 如何使用vuex的一個基本的工作流程
- vuex解決的是什么類型的問題
- 為什么vuex會優(yōu)于其他的策略国觉,盡管它有點(diǎn)復(fù)雜和嚴(yán)格
建立一個開始點(diǎn)
我們將用3種不同的解決同一個問題,在開始之前臊泰,我們需要我有一個相同的開始點(diǎn)蛉加,如果你想一道開始,我建議你為本教程建立一個git repo缸逃,在開始之后創(chuàng)建一個commit并且為不同方法創(chuàng)建分支针饥。
$ npm install -g vue-cli
$ vue init webpack vuex-tutorial
$ cd vuex-tutorial
$ npm install
$ npm install --save vuex
$ npm run dev
現(xiàn)在你可以看到基礎(chǔ)的vue腳手架頁面,讓我們創(chuàng)建并更新一些文件以使其達(dá)到我們想要的效果需频。
首先丁眼,我們在src/components/IncrementButton.vue
中建立一個IncrementButton
組件:
<template>
<button @click="activate">+1</button>
</template>
<script>
export default {
methods: {
activate () {
console.log('+1 Pressed')
}
}
}
</script>
<style>
</style>
接下來我們創(chuàng)建一個CounterDisplay
組件來展示計數(shù)器,讓我們在src/components/CounterDisplay.vue
中創(chuàng)建一個新的基礎(chǔ)vue組件昭殉。
<template>
Count is {{ count }}
</template>
<script>
export default {
data () {
return {
count: 0
}
}
}
</script>
<style>
</style>
替換App.vue
文件:
<template>
<div id="app">
<h3>Increment:</h3>
<increment></increment>
<h3>Counter:</h3>
<counter></counter>
</div>
</template>
<script>
import Counter from './components/CounterDisplay.vue'
import Increment from './components/IncrementButton.vue'
export default {
components: {
Counter,
Increment
}
}
</script>
<style>
</style>
現(xiàn)在你可以使用npm run dev
命令并且在瀏覽器中打開頁面苞七,你會看到一個按鈕和一個計數(shù)器藐守,點(diǎn)擊按鈕會在console中輸出一個信息,現(xiàn)在我們已經(jīng)抵達(dá)開始點(diǎn)蹂风,接下來就是處理過程了卢厂。
方法一:事件廣播
讓我們在組件中進(jìn)行修改吧。首先惠啄,在
IncrementButton.vue
中我們使用$dispatch
來發(fā)送一個信息給父節(jié)點(diǎn)以表明按鈕被觸發(fā)了慎恒。
export default {
methods: {
activate () {
// 發(fā)送一個事件給App父節(jié)點(diǎn)
this.$dispatch('button-pressed')
}
}
}
在App.vue
中我們由子節(jié)點(diǎn)監(jiān)聽事件并重新廣播一個新的增量事件給所有節(jié)點(diǎn)。
export default {
components: {
Counter,
Increment
},
events: {
'button-pressed': function () {
// 發(fā)送一個信息給所有子節(jié)點(diǎn)
this.$broadcast('increment')
}
}
}
在CounterDisplay.vue
中我們收聽increemnt
事件并增加state的值撵渡。
export default {
data () {
return {
count: 0
}
},
events: {
increment () {
this.count ++
}
}
}
該方法的一些缺點(diǎn)
在本方法中并沒有什么技術(shù)錯誤融柬,對于在一個文件中書寫你的所有應(yīng)用邏輯,該方法也是沒有技術(shù)問題的趋距,但問題是可維護(hù)性粒氧,以下說明了這種方法是多么的難以維護(hù)。
- 對于每次行為节腐,父組件必須與正確的組件連接并調(diào)度事件外盯。
- 對于一個大型應(yīng)用很難分析事件是從何發(fā)出的。
- 沒有明確的空間給"業(yè)務(wù)邏輯"铜跑,
this.count++
在CounterDisplay
中门怪,但業(yè)務(wù)邏輯可能在任何地方,這就造成了難以維護(hù)性锅纺。
我來給出這個應(yīng)用可能引起bug的一個例子:
- 你聘請了兩個實習(xí)生:Alice和Bob,你告訴Alice你需要一個新的組件中的計數(shù)器肋殴,你告訴Bob區(qū)寫一個Reset按鈕囤锉。
- Alice寫下一個新的組件
FormattedCounterDisplay
并訂閱了increment
事件來增加他自己的state,Alice很高興并提交了代碼护锤。 - Bob寫了一個新的"Reset"組件并添加一個
reset
事件到App
來重新調(diào)度官地,他在CounterDisplay
實現(xiàn)了reset
以使得計數(shù)重置為0,但他不知道Alice的組件也訂閱了該計數(shù)烙懦。 - 你的用戶按下"+1"驱入,發(fā)現(xiàn)應(yīng)用可以正常工作,但當(dāng)他們按下"reset"氯析,只有一個計數(shù)重置了亏较。
這是一個很簡單的例子,但它說明了使用事件將分散state和業(yè)務(wù)邏輯整合在一起時會發(fā)生錯誤掩缓。
方法二:共享state
讓我們還原到方法一開始前雪情,我們新建一個新的文件src/store.js
export default {
state: {
counter: 0
}
}
我們第一次修改CounterDisplay.vue
:
<template>
Count is {{ sharedState.counter }}
</template>
<script>
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
}
}
</script>
我在在這個做了一些有趣的事情:
- 我們?nèi)〉?code>store對象,這是一個不變的對象你辣,但它是其他文件定義的巡通。
- 在我們的本地
data
中我們創(chuàng)建了一個名為sharedState
的新變量尘执,映射到store.state
。 - vue使得
data
反映了store.state
宴凉,意味著vue會在store.state
改變時自動更新sharedState
誊锭。
至此該工程還沒法工作,但現(xiàn)在弥锄,我們可以修改IncrementButton.vue
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
},
methods: {
activate () {
this.sharedState.counter += 1
}
}
}
- 在這里丧靡,我們輸入store并添加了它的映射就像之前的例子。
- 當(dāng)
activate
函數(shù)被調(diào)用時叉讥,經(jīng)過sharedState
反映到store.state
并增加計數(shù)器窘行。 - 所有訂閱
counter
的組件和計算屬性現(xiàn)在會更新。
這種方法比第一種好在哪
讓我們重新回到兩位實習(xí)生的問題-Alice和Bob图仓。
- Alice寫下
FormattedComponentDisplay
并訂閱Shared State Counter罐盔,這個計數(shù)器會始終展示最新的計數(shù)結(jié)果。 - Bob的
ResetButton
組件設(shè)置Shared State Counter為0救崔,這會影響CounterDisplay
和Alice寫的FormattedCounterDisplay
惶看。 - 用戶發(fā)現(xiàn)reset按鈕能夠正常工作了。
這種方法還有哪些不足
- 在實習(xí)的過程中六孵,Alice和Bob寫下更多的計數(shù)器展示纬黎、reset按鈕、以及不同類型的增加按鈕劫窒,所有這些都會更新同一個共享計數(shù)器本今。Life is good.
- 一旦他們回到學(xué)校,你需要維護(hù)他們的代碼主巍。
- Carol-新來的領(lǐng)導(dǎo)冠息,他要求"我不希望計數(shù)器超過100"。
現(xiàn)在你怎么辦孕索?
- 你會找出所有更新計數(shù)器的組件逛艰?那真令人沮喪。
- 你想增加一個filter或formatter搞旭?那也很麻煩散怖。
現(xiàn)在就出現(xiàn)了問題,業(yè)務(wù)邏輯分散在應(yīng)用的各個部分肄渗,這個問題在原理上很簡單镇眷,但對于維護(hù)來說就是一個大問題。
一個稍微好一點(diǎn)的方法
現(xiàn)在你重構(gòu)了所有格原始代碼并重寫了store.js
:
var store = {
state: {
counter: 0
},
increment: function () {
if (store.state.counter < 100) {
store.state.counter += 1;
}
},
reset: function () {
store.state.counter = 0;
}
}
export default store
這樣做可以使得代碼更清晰恳啥,你可以明確的調(diào)用increment
偏灿,并且所有的業(yè)務(wù)邏輯都存儲在其中,但是钝的,一個新的實習(xí)生并不知道這些翁垂,他發(fā)現(xiàn)從該應(yīng)用的其他部分可以很容易的修改store.state.counter
铆遭,這時就很難定位錯誤了。
之后你又加了很多嚴(yán)格的規(guī)則沿猜、指南枚荣、代碼review來確保所有人都使用store.js
中的函數(shù)來修改state,當(dāng)這些都不管用后啼肩,你不得不讓人力砍掉這個實習(xí)生計劃橄妆。
方法三:Vuex
讓我們還原到方法二開始之前的狀態(tài),從原理上vuex有點(diǎn)類似方法二祈坠,以下是一個有點(diǎn)復(fù)雜的圖:
我們首先重新新建一個
src/store.js
害碾,這次我們的代碼改為:
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter ++
}
}
})
export default store
我看看代碼中發(fā)生了什么:
- 我們獲得
Vuex
模塊并指導(dǎo)Vue
來使用它啟動插件。 - 我們的存儲不再是一個問題json對象赦拘,而是一個
Vuex.Store
的實例慌随。 - 我們在state中創(chuàng)建一個
counter
并設(shè)置為0。 - 我們有一個新的
mutations
對象躺同,它有一個INCREMENT
方法阁猜,輸入一個state并修改這個值。
這段代碼中有一些有趣的東西:
- 所有使用
require('../store.js')
或import store from '../store.js'
的代碼將由相同的store實例蹋艺。 - 我們從不編輯
store.state.counter
剃袍,但我們得到了這個state的一個可更新和修改的拷貝,這將會非常重要捎谨。
現(xiàn)在我們已經(jīng)解決了store問題民效,讓我們修改IncrementButton.vue
import store from '../store'
export default {
methods: {
activate () {
store.commit('INCREMENT')
}
}
}
這個組件還沒有任何數(shù)據(jù),但在點(diǎn)擊后會執(zhí)行store.commit('INCREMENT')
涛救。我們會很快回到這里研铆。
現(xiàn)在,更新CounterDisplay.vue
<template>
Count is {{ counter }}
</template>
<script>
import store from '../store'
export default {
computed: {
counter () {
return store.state.counter
}
}
}
</script>
這時事情變得有趣了州叠,我們不再訂閱共享state咧栗,我們使用vue的computed屬性從store中獲得計數(shù)。
Vue清楚counter
的computed屬性依賴于store.state.counter
致板,因此當(dāng)store更新時,它會更新所有的相關(guān)項斟或,這就是我們要的!
如果你要重載頁面萝挤,你會發(fā)現(xiàn)計數(shù)器正常工作御毅,這里是一步步發(fā)生的事情:
- Vue的事件處理器成為
activate
怜珍,我們寫的這個函數(shù)是store.dispatch('INCREMENT')
端蛆。 - 在這里,
INCREMENT
是一個行為的名稱酥泛,它代表“這是一種可改變state的類型”的標(biāo)示符今豆,同時,我們可以將額外的參數(shù)傳遞給調(diào)度函數(shù)柔袁。 - Vue能夠清楚計數(shù)器的調(diào)度呆躲,同樣的,我們也能夠讓它更加復(fù)雜從而服務(wù)于更大的應(yīng)用捶索。
- 調(diào)度器接收state的拷貝并更新它插掂,Vue保存了一份state的舊的拷貝,而這會被用于之后更高級的特征情组。
- state更新時燥筷,vue自動更新所有依賴于該state的組件。
- 如果你這樣做了院崇,這會讓你的代碼更具可測試性肆氓。
為什么這種方法比方法二更好:
- 如果所有states的一個拷貝存在于開發(fā)的整個流程中,vue開發(fā)者就可以潛在的構(gòu)建一個"Time Travelling Debugger"底瓣,除了有一個很cool的名字谢揪,它還允許你在應(yīng)用中撤銷行為并修改邏輯,提高開發(fā)效率捐凭。
- 你可以構(gòu)建一個中間件并使其工作在state改變時拨扶,例如,你可以構(gòu)建一個logger茁肠,它可以留存用戶的所有行為日志患民。如果發(fā)現(xiàn)了bug,你可以調(diào)出日志來查看到底是哪一步出了問題垦梆。
- 對比讓你自己去監(jiān)視所有的行為匹颤,讓你的團(tuán)隊成員都能改變應(yīng)用的state會變得更加高效。
仍有很多工作要做
這里只展示了vuex的皮毛托猩,現(xiàn)在僅僅只是構(gòu)建的開始印蓖,我相信這種開發(fā)模式要發(fā)展成熟仍然需要很多年。
你可以發(fā)現(xiàn)更多資料去組織你的store京腥,在vuex的文檔中學(xué)習(xí)更多的信息赦肃,消化所有的概念可能需要一些時間,我們可能也要做出一些錯誤的嘗試才能找出正確的方法。
結(jié)語:處理實習(xí)生的代碼
你可以移植你的應(yīng)用到vue.js他宛,你的實習(xí)生發(fā)現(xiàn)可以在他的組件中很便捷的寫入store.state.counter
船侧,你現(xiàn)在已經(jīng)拿到了最后一根稻草,現(xiàn)在在你的store.js
中加一行勺爱。
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter ++
}
},
strict: true // Vuex申請專利的反實習(xí)裝置
})
現(xiàn)在所有人可以直接寫入store琐鲁,這也會引起一個問題围段,注意這會降低你的應(yīng)用的速率投放,你可以在產(chǎn)品發(fā)布時安全的去掉它,去看看文檔中例子是怎么做的涝桅。