vuex實(shí)現(xiàn)及簡略解析(小結(jié))
大家都知道vuex是vue的一個狀態(tài)管理器,它采用集中式存儲管理應(yīng)用的所有組件的狀態(tài),并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化。先看看vuex下面的工作流程圖

通過官方文檔提供的流程圖我們知道,vuex的工作流程,
1、數(shù)據(jù)從state中渲染到頁面;
2、在頁面通過dispatch來觸發(fā)action;
3、action通過調(diào)用commit,來觸發(fā)mutation;
4、mutation來更改數(shù)據(jù),數(shù)據(jù)變更之后會觸發(fā)dep對象的notify,通知所有Watcher對象去修改對應(yīng)視圖(vue的雙向數(shù)據(jù)綁定原理)。
使用vuex
理解vuex的工作流程我們就看看vuex在vue中是怎么使用的。
首先用vue-cli創(chuàng)建一個項目工程,如下圖,選擇vuex,然后就是一路的回車鍵

安裝好之后,就有一個帶有vuex的vue項目了。
進(jìn)入目錄然后看到,src/store.js,在里面加了一個狀態(tài){count: 100},如下
import Vue from 'vue'
import Vuex from 'vuex' // 引入vuex
Vue.use(Vuex) // 使用插件
export default new Vuex.Store({
state: {
count: 100 // 加一個狀態(tài)
},
getter: {
},
mutations: {
},
actions: {
}
})
最后在App.vue文件里面使用上這個狀態(tài),如下
<template>
<div id="app">
這里是stort------->{{this.$store.state.count}}
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
</style>
項目跑起來就會看到頁面上看到,頁面上會有100了,如下圖

到這里我們使用vuex創(chuàng)建了一個store,并且在我們的App組件視圖中使用,但是我們會有一些列的疑問。
store是如何被使用到各個組件上的??- 為什么
state的數(shù)據(jù)是雙向綁定的?? - 在組件中為什么用
this.$store.dispch可以觸發(fā)store的actions?? - 在組件中為什么用
this.$store.commit可以觸發(fā)store的mutations?? - ....等等等等
帶著一堆問題,我們來自己實(shí)現(xiàn)一個vuex,來理解vuex的工作原理。
安裝并使用store
在src下新建一個vuex.js文件,然后代碼如下
'use strict'
let Vue = null
class Store {
constructor (options) {
let { state, getters, actions, mutations } = options
}
}
// Vue.use(Vuex)
const install = _Vue => {
// 避免vuex重復(fù)安裝
if (Vue === _Vue) return
Vue = _Vue
Vue.mixin({
// 通過mixins讓每個組件實(shí)例化的時候都會執(zhí)行下面的beforeCreate
beforeCreate () {
// 只有跟節(jié)點(diǎn)才有store配置,所以這里只走一次
if (this.$options && this.$options.store) {
this.$store = this.$options.store
} else if (this.$parent && this.$parent.$store) { // 子組件深度優(yōu)先 父 --> 子---> 孫子
this.$store = this.$parent.$store
}
}
})
}
export default { install, Store }
然后修改store.js中的引入vuex模塊改成自己的vuex.js
import Vuex from './vuex' // 自己創(chuàng)建的vuex文件
在我們的代碼中export default { install, Store }導(dǎo)出了一個對象,分別是install和Store
install的作用是,當(dāng)Vue.use(Vuex)就會自動調(diào)用install方法,在install方法里面,我們用mixin混入了一個beforeCreate的生命周期的鉤子函數(shù),使得當(dāng)每個組件實(shí)例化的時候都會調(diào)用這個函數(shù)。
在beforeCreate中,第一次根組件通過store屬性掛載$store,后面子組件調(diào)用beforeCreate掛載的$store都會向上找到父級的$store,這樣子通過層層向上尋找,讓每個組件都掛上了一個$store屬性,而這個屬性的值就是我們的new Store({...})的實(shí)例。如下圖

通過層層向上尋找,讓每個組件都掛上了一個$store屬性
設(shè)置state響應(yīng)數(shù)據(jù)
通過上面,我們已經(jīng)從每個組件都通過this.$store來訪問到我們的store的實(shí)例,下面我們就編寫state數(shù)據(jù),讓其變成雙向綁定的數(shù)據(jù)。下面我們改寫store類
class Store {
constructor (options) {
let { state, getters, actions, mutations } = options // 拿到傳進(jìn)來的參數(shù)
this.getters = {}
this.mutations = {}
this.actions = {}
// vuex的核心就是借用vue的實(shí)例,因為vuex的數(shù)據(jù)更改回更新視圖
this._vm = new Vue({
data: {
state
}
})
}
// 訪問state對象時候,就直接返回響應(yīng)式的數(shù)據(jù)
get state() { // Object.defineProperty get 同理
return this._vm.state
}
}
傳進(jìn)來的state對象,通過new Vue({data: {state}})的方式,讓數(shù)據(jù)變成響應(yīng)式的。當(dāng)訪問state對象時候,就直接返回響應(yīng)式的數(shù)據(jù),這樣子在App.vue中就可以通過this.$store.state.count拿到state的數(shù)據(jù)啦,并且是響應(yīng)式的呢。
編寫mutations、actions、getters
上面我們已經(jīng)設(shè)置好state為響應(yīng)式的數(shù)據(jù),這里我們在store.js里面寫上mutations、actions、getters,如下
import Vue from 'vue'
import Vuex from './vuex' // 引入我們的自己編寫的文件
Vue.use(Vuex) // 安裝store
// 實(shí)例化store,參數(shù)數(shù)對象
export default new Vuex.Store({
state: {
count : 1000
},
getters : {
newCount (state) {
return state.count + 100
}
},
mutations: {
change (state) {
console.log(state.count)
state.count += 10
}
},
actions: {
change ({commit}) {
// 模擬異步
setTimeout(() => {
commit('change')
}, 1000)
}
}
})
配置選項都寫好之后,就看到getters對象里面有個newCount函數(shù),mutations和actions對象里面都有個change函數(shù),配置好store之后我們在App.vue就可以寫上,dispatch和commit,分別可以觸發(fā)actions和mutations,代碼如下
<template>
<div id="app">
這里是store的state------->{{this.$store.state.count}} <br/>
這里是store的getter------->{{this.$store.getters.newCount}} <br/>
<button @click="change">點(diǎn)擊觸發(fā)dispach--> actions</button>
<button @click="change1">點(diǎn)擊觸發(fā)commit---> mutations</button>
</div>
</template>
<script>
export default {
name: 'app',
methods: {
change () {
this.$store.dispatch('change') // 觸發(fā)actions對應(yīng)的change
},
change1 () {
this.$store.commit('change') // 觸發(fā)mutations對應(yīng)的change
}
},
mounted () {
console.log(this.$store)
}
}
</script>
數(shù)據(jù)都配置好之后,我們開始編寫store類,在此之前我們先編寫一個循環(huán)對象工具函數(shù)。
const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))
// 作用:
// 例如{a: '123'}, 把對象的key和value作為參數(shù)
// 然后就是函數(shù)運(yùn)行callback(a, '123')
工具函數(shù)都準(zhǔn)備好了,之后,下面直接縣編寫getters、mutations和actions的實(shí)現(xiàn)
class Store {
constructor (options) {
let { state = {}, getters = {}, actions = {}, mutations = {} } = options
this.getters = {}
this.mutations = {}
this.actions = {}
// vuex的核心就是借用vue的實(shí)例,因為vuex的數(shù)據(jù)更改回更新視圖
this._vm = new Vue({
data: {
state
}
})
// 循環(huán)getters的對象
myforEach(getters, (getterName, getterFn) => {
// 對this.getters對象進(jìn)行包裝,和vue的computed是差不多的
// 例如 this.getters['newCount'] = fn(state)
// 執(zhí)行 this.getters['newCount']()就會返回計算的數(shù)據(jù)啦
Object.defineProperty(this.getters, getterName, {
get: () => getterFn(state)
})
})
// 這里是mutations各個key和值都寫到,this.mutations對象上面
// 執(zhí)行的時候就是例如:this.mutations['change']()
myforEach(mutations, (mutationName, mutationsFn) => {
// this.mutations.change = () => { change(state) }
this.mutations[mutationName] = () => {
mutationsFn.call(this, state)
}
})
// 原理同上
myforEach(actions, (actionName, actionFn) => {
// this.mutations.change = () => { change(state) }
this.actions[actionName] = () => {
actionFn.call(this, this)
}
})
const {commit , dispatch} = this // 先存一份,避免this.commit會覆蓋原型上的this.commit
// 解構(gòu) 把this綁定好
// 通過結(jié)構(gòu)的方式也要先調(diào)用這類,然后在下面在調(diào)用原型的對應(yīng)函數(shù)
this.commit = type => {
commit.call(this, type)
}
this.dispatch = type => {
dispatch.call(this, type)
}
}
get state() { // Object.defineProperty 同理
return this._vm.state
}
// commi調(diào)用
commit (type) {
this.mutations[type]()
}
// dispatch調(diào)用
dispatch (type) {
this.actions[type]()
}
}
通過上面的,我們可以看出,其實(shí)mutations和actions都是把傳入的參數(shù),賦值到store實(shí)例上的this.mutations和this.actions對象里面。
當(dāng)組件中this.$store.commit('change')的時候 其實(shí)是調(diào)用this.mutations.change(state),就達(dá)到了改變數(shù)據(jù)的效果,actions同理。
getters是通過對Object.defineProperty(this.getters, getterName, {})
對this.getters進(jìn)行包裝當(dāng)組件中this.$store.getters.newCount其實(shí)是調(diào)用getters對象里面的newCount(state),然后返回計算結(jié)果。就可以顯示到界面上了。
大家看看完成后的效果圖。

到這里大家應(yīng)該懂了vuex的內(nèi)部代碼的工作流程了,vuex的一半核心應(yīng)該在這里了。為什么說一半,因為還有一個核心概念module,也就是vuex的數(shù)據(jù)的模塊化。
vuex數(shù)據(jù)模塊化
由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會集中到一個比較大的對象。當(dāng)應(yīng)用變得非常復(fù)雜時,store 對象就有可能變得相當(dāng)臃腫。
為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊——從上至下進(jìn)行同樣方式的分割
例如下面的store.js
// 實(shí)例化store,參數(shù)數(shù)對象
export default new Vuex.Store({
modules: {
// 模塊a
a: {
state: {
count: 4000
},
actions: {
change ({state}) {
state.count += 21
}
},
modules: {
// 模塊b
b: {
state: {
count: 5000
}
}
}
}
},
state: {
count : 1000
},
getters : {
newCount (state) {
return state.count + 100
}
},
mutations: {
change (state) {
console.log(state.count)
state.count += 10
}
},
actions: {
change ({commit}) {
// 模擬異步
setTimeout(() => {
commit('change')
}, 1000)
}
}
})
然后就可以在界面上就可以寫上this.$store.state.a.count(顯示a模塊count),this.$store.state.a.b.count(顯示a模塊下,b模塊的count),這里還有一個要注意的,其實(shí)在組件中調(diào)用this.$store.dispatch('change')會同時觸發(fā),根的actions和a模塊的actions里面的change函數(shù)。
下面我們就直接去實(shí)現(xiàn)models的代碼,也就是整個vuex的實(shí)現(xiàn)代碼,
'use strict'
let Vue = null
const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))
class Store {
constructor (options) {
let state = options.state
this.getters = {}
this.mutations = {}
this.actions = {}
// vuex的核心就是借用vue的實(shí)例,因為vuex的數(shù)據(jù)更改回更新視圖
this._vm = new Vue({
data: {
state
}
})
// 把模塊之間的關(guān)系進(jìn)行整理, 自己根據(jù)用戶參數(shù)維護(hù)了一個對象
// root._children => a._children => b
this.modules = new ModulesCollections(options)
// 無論子模塊還是 孫子模塊 ,所有的mutations 都是根上的
// 安裝模塊
installModules(this, state, [], this.modules.root)
// 解構(gòu) 把this綁定好
const {commit , dispatch} = this
// 通過結(jié)構(gòu)的方式也要先調(diào)用這類,然后在下面在調(diào)用原型的對應(yīng)函數(shù)
this.commit = type => {
commit.call(this, type)
}
this.dispatch = type => {
dispatch.call(this, type)
}
}
get state() { // Object.defineProperty 同理
return this._vm.state
}
commit (type) {
// 因為是數(shù)組,所以要遍歷執(zhí)行
this.mutations[type].forEach(fn => fn())
}
dispatch (type) {
// 因為是數(shù)組,所以要遍歷執(zhí)行
this.actions[type].forEach(fn => fn())
}
}
class ModulesCollections {
constructor (options) { // vuex []
// 注冊模塊
this.register([], options)
}
register (path, rawModule) {
// path 是空數(shù)組, rawModule 就是個對象
let newModule = {
_raw: rawModule, // 對象
_children: {}, // 把子模塊掛載到這里
state: rawModule.state
}
if (path.length === 0) { // 第一次
this.root = newModule
} else {
// [a, b] ==> [a]
let parent = path.slice(0, -1).reduce((root, current) => {
return root._children[current]
}, this.root)
parent._children[path[path.length - 1]] = newModule
}
if (rawModule.modules) {
// 遍歷注冊子模塊
myforEach(rawModule.modules, (childName, module) => {
this.register(path.concat(childName), module)
})
}
}
}
// rootModule {_raw, _children, state }
function installModules (store, rootState, path, rootModule) {
// rootState.a = {count:200}
// rootState.a.b = {count: 3000}
if (path.length > 0) {
// 根據(jù)path找到對應(yīng)的父級模塊
// 例如 [a] --> path.slice(0, -1) --> [] 此時a模塊的父級模塊是跟模塊
// 例如 [a,b] --> path.slice(0, -1) --> [a] 此時b模塊的父級模塊是a模塊
let parent = path.slice(0, -1).reduce((root, current) => {
return root[current]
}, rootState)
// 通過Vue.set設(shè)置數(shù)據(jù)雙向綁定
Vue.set(parent, path[path.length - 1], rootModule.state)
}
// 設(shè)置getter
if (rootModule._raw.getters) {
myforEach(rootModule._raw.getters, (getterName, getterFn) => {
Object.defineProperty(store.getters, getterName, {
get: () => {
return getterFn(rootModule.state)
}
})
})
}
// 在跟模塊設(shè)置actions
if (rootModule._raw.actions) {
myforEach(rootModule._raw.actions, (actionName, actionsFn) => {
// 因為同是在根模塊設(shè)置,子模塊也有能相同的key
// 所有把所有的都放到一個數(shù)組里面
// 就變成了例如 [change, change] , 第一個是跟模塊的actions的change,第二個是a模塊的actions的change
let entry = store.actions[actionName] || (store.actions[actionName] = [])
entry.push(() => {
const commit = store.commit
const state = rootModule.state
actionsFn.call(store, {state, commit})
})
})
}
// 在跟模塊設(shè)置mutations, 同理上actions
if (rootModule._raw.mutations) {
myforEach(rootModule._raw.mutations, (mutationName, mutationFn) => {
let entry = store.mutations[mutationName] || (store.mutations[mutationName] = [])
entry.push(() => {
mutationFn.call(store, rootModule.state)
})
})
}
// 遞歸遍歷子節(jié)點(diǎn)的設(shè)置
myforEach(rootModule._children, (childName, module) => {
installModules(store, rootState, path.concat(childName), module)
})
}
const install = _Vue => {
// 避免vuex重復(fù)安裝
if (Vue === _Vue) return
Vue = _Vue
Vue.mixin({
// 通過mixins讓每個組件實(shí)例化的時候都會執(zhí)行下面的beforeCreate
beforeCreate () {
// 只有跟節(jié)點(diǎn)才有store配置
if (this.$options && this.$options.store) {
this.$store = this.$options.store
} else if (this.$parent && this.$parent.$store) { // 子組件深度優(yōu)先 父 --> 子---> 孫子
this.$store = this.$parent.$store
}
}
})
}
export default { install, Store }
看到代碼以及注釋,主要流程就是根據(jù)遞歸的方式,處理數(shù)據(jù),然后根據(jù)傳進(jìn)來的配置,進(jìn)行操作數(shù)據(jù)。
至此,我們把vuex的代碼實(shí)現(xiàn)了一遍,在我們App.vue的代碼里添加
<template>
<div id="app">
這里是store的state------->{{this.$store.state.count}} <br/>
這里是store的getter------->{{this.$store.getters.newCount}} <br/>
這里是store的state.a------->{{this.$store.state.a.count}} <br/>
<button @click="change">點(diǎn)擊觸發(fā)dispach--> actions</button>
<button @click="change1">點(diǎn)擊觸發(fā)commit---> mutations</button>
</div>
</template>
最后查看結(jié)果。

源碼地址:https://github.com/naihe138/write-vuex
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 基于vue2.0+vuex的日期選擇組件功能實(shí)現(xiàn)
- 詳解使用Vue Router導(dǎo)航鉤子與Vuex來實(shí)現(xiàn)后退狀態(tài)保存
- vuex實(shí)現(xiàn)簡易計數(shù)器
- VUE利用vuex模擬實(shí)現(xiàn)新聞點(diǎn)贊功能實(shí)例
- vuex實(shí)現(xiàn)登錄狀態(tài)的存儲,未登錄狀態(tài)不允許瀏覽的方法
- Vuex實(shí)現(xiàn)計數(shù)器以及列表展示效果
- Vuex模塊化實(shí)現(xiàn)待辦事項的狀態(tài)管理
- 詳解如何實(shí)現(xiàn)一個簡單的 vuex
- 使用Vuex實(shí)現(xiàn)一個筆記應(yīng)用的方法
- 如何使用vuex實(shí)現(xiàn)兄弟組件通信
相關(guān)文章
Vue 項目中如何使用fullcalendar 時間段選擇插件(類似課程表格)
最近完成一個項目,需要選擇一個會議室,但是最好能夠通過在圖上顯示出該 會議室在某某時間段內(nèi)已經(jīng)被預(yù)定了,初看這個功能感覺很棘手,仔細(xì)分析下實(shí)現(xiàn)起來還是挺容易的,下面通過示例代碼講解Vue項目中使用fullcalendar時間段選擇插件,感興趣的朋友一起看看吧2024-07-07
Vue計算屬性與監(jiān)視屬性實(shí)現(xiàn)方法詳解
最近在學(xué)習(xí)vue,學(xué)習(xí)中遇到了一些感覺挺重要的知識點(diǎn),感覺有必要整理下來,這篇文章主要給大家介紹了關(guān)于Vue.js中計算屬性、監(jiān)視屬性的相關(guān)資料,需要的朋友可以參考下2022-08-08
Vue如何整合mavon-editor編輯器(markdown編輯和預(yù)覽)
這篇文章主要介紹了Vue整合mavon-editor編輯器(markdown編輯和預(yù)覽)的相關(guān)知識,mavon-editor是目前比較主流的markdown編輯器,重點(diǎn)介紹它的使用方法,需要的朋友可以參考下2022-10-10
vue2.0的contextmenu右鍵彈出菜單的實(shí)例代碼
本篇文章主要介紹了vue2.0的contextmenu右鍵彈出菜單的實(shí)例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07

