vuex
做爲Vue
全家桶不可或缺的一部分,學習並理解其源碼,不只能夠學習到做者的優秀開發思路和代碼編寫技巧,還能夠幫助咱們在開發過程當中寫出更好更規範的代碼,知其然,知其因此然前端
源碼版本是3.1.2,在調試源碼時儘可能不要直接使用console.log
,由於有些時候其輸出並非你指望的數據,建議使用debugger
進行調試閱讀源碼,接下來的文章中會適當的將源碼中一些兼容性和健壯性處理忽略,只看主要的流程vue
在vue
中使用插件時,會調用Vue.use(Vuex)
將插件進行處理,此過程會經過mixin
在各個組件中的生命鉤子beforeCreate
中爲每一個實例增長$store
屬性vuex
在vue
項目中,使用vuex
進行數據管理,首先作的就是將vuex
引入並Vue.use(Vuex)
數組
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
複製代碼
在執行Vue.use(Vuex)
時,會觸發vuex
中暴露出來的方法install
進行初始化,並缺會將Vue
做爲形參傳遞,全部的vue
插件都會暴露一個install
方法,用於初始化一些操做,方法在/src/store.js
中暴露微信
let Vue // bind on install
export function install (_Vue) {
// 容錯判斷
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue // 只初始化賦值一次--單例模式
applyMixin(Vue)
}
複製代碼
首先會在store
中定義一個變量Vue
,用來接受Vue
實例數據結構
install
函數中,首先會判斷是否已經調用了Vue.use(Vuex)
,而後調用applyMixin
方法進行初始化的一些操做app
總結:install
方法僅僅作了一個容錯處理,而後調用applyMixin
,Vue
賦值異步
applyMixin
方法在/src/mixin
中暴露,該方法只作了一件事情,就是將全部的實例上掛載一個$store
對象函數
export default function (Vue) {
// 獲取當前的vue版本號
const version = Number(Vue.version.split('.')[0])
// 如果2以上的vue版本,直接經過mixin進行掛載$store
if (version >= 2) {
// 在每一個實例beforeCreate的時候進行掛載$store
Vue.mixin({ beforeCreate: vuexInit })
} else {
// vue 1.x版本處理 省略...
}
function vuexInit () {
// 1. 獲取每一個組件實例的選項
const options = this.$options
// 2. 檢測options.store是否存在
if (options.store) {
// 下面詳細說明
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 檢測當前組件實例的父組件是夠存在,而且其父組件存在$store
// 存在,則爲當前組件實例掛載$store屬性
this.$store = options.parent.$store
}
}
}
複製代碼
整個mixin
文件的難點在於理解this.$store = typeof options.store === 'function' ? options.store() : options.store
作了什麼事學習
在使用vuex
的時候,會將store
掛載在根組件之上
import Vue from 'vue'
import Counter from './Counter.vue'
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(Counter)
})
複製代碼
在第一次調用vuexInit
函數時,options.store
就是根選項的store
,所以會判斷其類型是否是function
,如果則執行函數並將結果賦值給根實例的$store
中,不然直接賦值。
總結:整個mixin
文件作的事情,就是利用mixin
在各個實例的生命鉤子beforeCreate
中爲其增長屬性$store
併爲其賦值,保證在每一個實例中均可以直接經過this.$store
獲取數據和行爲。
module
模塊主要的功能:是將咱們定義的store
根據必定的規則轉化爲一顆樹形結構,在實例化Store
的時候執行,會將其獲得的樹形結構賦值給this._modules
,後續會基於這顆樹進行操做。
首先是咱們在vuex
中定義一些狀態和模塊,觀察其轉化的樹形結構爲什麼物
const state = {
count: 0
}
const getters = {
}
const mutations = {
}
const actions = {
}
const modules = {
moduleA:{
state: {
a: 'module a'
},
modules: {
moduleB:{
state: {
b: 'module b'
}
}
}
},
moduleC: {
state: {
c: 'module c'
}
}
}
export default new Vuex.Store({
modules,
state,
getters,
actions,
mutations
})
複製代碼
vuex
在獲取到定義的狀態和模塊,會將其格式化成一個樹形結構,後續的不少操做都是基於這顆樹形結構進行操做和處理,能夠在任意一個使用的組件中打印this.$store._modules.root
觀察其結構
格式化以後的樹形結構,每一層級都會包含state
、_rawModule
、_children
三個主要屬性
樹形節點結構
{
state:{},
_rawModule:{},
_children: {}
}
複製代碼
state
根模塊會將自身還有其包含的所有子模塊state
數據按照模塊的層級按照樹級結構放置,根模塊的state
會包含自身以及全部的子模塊數據,子模塊的state
會包含自身以及其子模塊的數據
{
state: {
count: 0,
moduleA: {
a: 'module a',
moduleB: {
b: 'module b'
}
},
moduleC: {
c: 'module c'
}
}
}
複製代碼
_rawModule
每一層樹形結構都會包含一個_rawModule
節點,就是在調用store
初始化的時候傳入的options
,根上的_rawModule
就是初始化時的全部選項,子模塊上就是各自初始化時使用的options
{
modules:{},
state:{},
getters:{},
actions:{},
mutations:{}
}
複製代碼
_children
_children
會將當前模塊以及其子模塊按照約定的樹形結構進行格式化,放在其父或者跟組件的_children
中,鍵名就是其模塊名
{
moduleA:{
state: {},
_rawModule:{},
_children:{
moduleB:{
state: {},
_rawModule:{},
_children:{}
},
}
},
moduleC:{
state: {},
_rawModule:{},
_children:{}
}
}
複製代碼
總結:根據調用store
初始化時傳入的參數,在其內部將其轉化爲一個樹形結構,能夠經過this.$store._modules.root
查看
知道了轉化處理以後的樹形結構,接下來看看vuex
中是如何經過代碼處理的,在src/module
文件夾中,存在module-collection.js
和module.js
兩個文件,主要經過ModuleCollection
和Module
兩個類進行模塊收集
Module
Module
的主要做用就是根據設計好的樹形節點結構生成對應的節點結構,實例化以後會生成一個基礎的數據結構,並在其原型上定一些操做方法供實例調用
import { forEachValue } from '../util'
export default class Module {
constructor (rawModule, runtime) {
// 是否爲運行時 默認爲true
this.runtime = runtime
// _children 初始化是一個空對象
this._children = Object.create(null)
// 將初始化vuex的時候 傳遞的參數放入_rawModule
this._rawModule = rawModule
// 將初始化vuex的時候 傳遞的參數的state屬性放入state
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// _children 初始化是一個空對象,爲其增長子模塊
addChild (key, module) {
this._children[key] = module
}
// 根據 key,獲取對應的模塊
getChild (key) {
return this._children[key]
}
}
複製代碼
ModuleCollection
結合Module
用來生成樹形結構
import Module from './module'
import { forEachValue } from '../util'
export default class ModuleCollection {
constructor (rawRootModule) {
// 根據options 註冊模塊
this.register([], rawRootModule, false)
}
// 利用 reduce,根據 path 找到此時子模塊對應的父模塊
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
register (path, rawModule, runtime = true) {
// 初始化一個節點
const newModule = new Module(rawModule, runtime)
if (path.length === 0) { // 根節點, 此時 path 爲 []
this.root = newModule
} else { // 子節點處理
// 1. 找到當前子節點對應的父
// path ==> [moduleA, moduleC]
// path.slice(0, -1) ==> [moduleA]
// get ==> 獲取到moduleA
const parent = this.get(path.slice(0, -1))
// 2. 調用 Module 的 addChild 方法,爲其 _children 增長節點
parent.addChild(path[path.length - 1], newModule)
}
// 如果存在子模塊,則會遍歷遞歸調用 register
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
複製代碼
ModuleCollection
時傳遞的實參爲new Vuex.Store({....options})
中的options
,此時的rawRootModule
就是options
,接下來的操做都是基於rawRootModule
進行操鎖options
的數據結構簡寫
{
modules: {
moduleA:{
modules: {
moduleB:{
}
}
},
moduleC: {
}
}
}
複製代碼
執行this.register([], rawRootModule, false)
[]
對應形參path
,保存的是當前模塊的層級路徑,例如moduleB
對應的路徑["moduleA", "moduleB"]
rawRootModule
對應形參rawModule
,表明在初始化參數options
中對應的數據,例如moduleA
對應的rawModule
爲:
moduleA:{
state: {
a: 'module a'
},
mutations:{
incrementA: ({ commit }) => commit('increment'),
decrementA: ({ commit }) => commit('decrement'),
},
modules: {
moduleB:{
state: {
b: 'module b'
}
}
}
}
複製代碼
每次執行register
時都會實例化Module
,生成一個樹形的節點newModule
,以後即是經過判斷path
的長度來決定newModule
放置的位置,第一次執行register
時path
爲[]
,則直接將newModule
賦值給this.root
,其他狀況,即是經過path
找到當前節點對應的父節點並將其放置在_children
中
判斷rawModule.modules
是否存在,如果存在子模塊,便遍歷rawModule.modules
進行遞歸調用register
進行遞歸處理,最終會生成一個指望的樹形結構
經歷了前面的鋪墊,終於到了vuex
的核心類store
,在store
中會對定義的state
,mutations
,actions
,getters
等進行處理
首先看看Store
的總體結構
class Store {
constructor (options = {}) {}
get state () {}
set state (v) {}
commit (_type, _payload, _options) {}
dispatch (_type, _payload) {}
subscribe (fn) {}
subscribeAction (fn) {}
watch (getter, cb, options) {}
replaceState (state) {}
registerModule (path, rawModule, options = {}) {}
unregisterModule (path) {}
hotUpdate (newOptions) {}
_withCommit (fn) {}
}
複製代碼
在使用vuex
中,會看到經常使用的方法和屬性都定義在store
類中,接下來經過完善類中的內容逐步的實現主要功能
在模塊中定義的state
經過vux
以後處理以後,即可以在vue
中經過$store.state.xxx
使用,且當數據變化時會驅動視圖更新
首先會在store
中進行初始化
class Store {
constructor(options) {
// 定義一些內部的狀態 ....
this._modules = new ModuleCollection(options)
const state = this._modules.root.state
// 初始化根模塊,會遞歸註冊全部的子模塊
installModule(this, state, [], this._modules.root)
// 初始化 store、vm
resetStoreVM(this, state)
}
// 利用類的取值函數定義state,其實取的值是內部的_vm傷的數據,代理模式
get state() {
return this._vm._data.$$state
}
_withCommit (fn) {
fn()
}
}
複製代碼
首先會執行installModule
,遞歸調用,會將全部的子模塊的數據進行註冊,函數內部會進行遞歸調用自身進行對子模塊的屬性進行便利,最終會將全部子模塊的模塊名做爲鍵,模塊的state
做爲對應的值,模塊的嵌套層級進行嵌套,最終生成所指望的數據嵌套結構
{
count: 0,
moduleA: {
a: "module a",
moduleB: {
b: "module b"
}
},
moduleC: {
c: "module c"
}
}
複製代碼
installModule
關於處理state
的核心代碼以下
/** * @param {*} store 整個store * @param {*} rootState 當前的根狀態 * @param {*} path 爲了遞歸使用的,路徑的一個數組 * @param {*} module 從根模塊開始安裝 * @param {*} hot */
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length // 是否是根節點
// 設置 state
// 非根節點的時候 進入,
if (!isRoot && !hot) {
// 1. 獲取到當前模塊的父模塊
const parentState = getNestedState(rootState, path.slice(0, -1))
// 2. 獲取當前模塊的模塊名
const moduleName = path[path.length - 1]
// 3. 調用 _withCommit ,執行回調
store._withCommit(() => {
// 4. 利用Vue的特性,使用 Vue.set使剛設置的鍵值也具有響應式,不然Vue監控不到變化
Vue.set(parentState, moduleName, module.state)
})
}
// 遞歸處理子模塊的state
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
複製代碼
將state
處理成指望的結構以後,會結合resetStoreVM
對state
進行處理,如果直接在Store中定義變量state,外面能夠獲取到,可是當修改了以後並不能利用的vue的數據綁定驅動視圖的更行,因此利用vue的特性,將vue的實例放置在_vm上,而後利用類的取值函數獲取
當使用$store.state.count
的時候,會先根據類的取值函數get state
進行取值,取值函數內部返回的就是resetStoreVM
所賦值_vm
,結合vue
進行響應適處理
function resetStoreVM(store, state) {
store._vm = new Vue({
data: {
$$state: state
}
})
}
複製代碼
在vuex
中,對於同步修改數據狀態時,推薦使用mutations
進行修改,不推薦直接使用this.$store.state.xxx = xxx
進行修改,能夠開啓嚴格模式strict: true
進行處理
vuex
對於mutations
的處理,分爲兩部分,第一步是在installModule
時將全部模塊的mutations
收集訂閱,第二步在Store
暴露commit
方法發佈執行所對應的方法
首先在Store
中處理增長一個_mutations
屬性
constructor(options){
// 建立一個_mutations 空對象,用於收集各個模塊中的 mutations
this._mutations = Object.create(null)
}
複製代碼
在installModule
中遞歸調用的處理全部的mutations
const local = module.context = makeLocalContext(store, '', path)
// 處理 mutations
module.forEachMutation((mutation, key) => {
registerMutation(store, key, mutation, local)
})
複製代碼
在registerMutation
函數中進行對應的nutations
收集
// 註冊 mutations 的處理 -- 訂閱
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
複製代碼
此時全部模塊的mutations
都會被訂閱在_mutations
中,只須要在調用執行時找到對應的mutations
進行遍歷執行,這裏使用一個數組收集訂閱,由於在vuex
中,定義在不一樣模塊中的同名mutations
都會被依次執行,因此須要使用數組訂閱,並遍歷調用,所以也建議在使用vuex
的時候,若項目具備必定的複雜度和體量,建議使用命名空間namespaced: true
,能夠減小沒必要要的重名mutations
所有被執行,致使不可控的問題出現
makeLocalContext
函數將vuex
的選項進行處理,省略開啓命名空間的代碼,主要是將getters
和 state
進行劫持處理
function makeLocalContext (store, namespace, path) {
const local = {
dispatch: store.dispatch,
commit: store.commit
}
// getters 和 state 必須是懶獲取,由於他們的修改會經過vue實例的更新而變化
Object.defineProperties(local, {
getters: {
get: () => store.getters
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
複製代碼
收集訂閱完成以後,須要Store
暴露一個方法用於觸發發佈,執行相關的函數修改數據狀態
首先在Store
類上定義一個commit
方法
{
// 觸發 mutations
commit (_type, _payload, _options) {
// 1. 區分不一樣的調用方式 進行統一處理
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
// 2. 獲取到對應type的mutation方法,便利調用
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
}
}
複製代碼
可是在源碼中,外界調用時並非直接調用調用類上的commit
方法,而是在構造constructor
中重寫的commit
constructor(options){
// 1. 獲取 commit 方法
const { commit } = this
// 2. 使用箭頭函數和call 保證this的指向
this.commit = (type, payload, options) => {
return commit.call(this, type, payload, options)
}
}
複製代碼
unifyObjectStyle
方法作了一個參數格式化的處理,調用 mutations 可使用this.$store.commit('increment', payload)
和this.$store.commit({type: 'increment', payload})
兩種方式,unifyObjectStyle函數就是爲了將不一樣的參數格式化成一種狀況,actions
同理
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
return { type, payload, options }
}
複製代碼
actions
用於處理異步數據改變,mutations
用於處理同步數據改變,二者的區別主要在因而否是異步處理數據,所以二者在實現上具有不少的共通性,首先將全部的actions
進行訂閱收集,而後暴露方法發佈執行
首先在Store
中處理增長一個_actions
屬性
constructor(options){
// 建立一個 _actions 空對象,用於收集各個模塊中的 actions
this._actions = Object.create(null)
}
複製代碼
在installModule
中遞歸調用的處理全部的actions
const local = module.context = makeLocalContext(store, '', path)
// 處理actions
module.forEachAction((action, key) => {
const type = action.root ? key : '' + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
複製代碼
在registerAction
函數中進行對應的actions
收集
// 註冊 actions 的處理
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
// actions 和 mutations 在執行時,第一個參數接受到的不同
entry.push(function wrappedActionHandler (payload) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 判斷是否時Promise
if (!isPromise(res)) {
res = Promise.resolve(res)
}
return res
})
}
複製代碼
actions
的發佈執行,和mutations
處理方式一致,區別在於dispatch
方法須要多作一些處理
// 觸發 actipns
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
// 如果多個,則使用Promise.all(),不然執行一次
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 拿到執行結果 進行判斷處理
return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {}
return res
})
}
複製代碼
此文章還沒寫做完成,可是想借這個平臺獲取一些內推的前端職位,因此提早發了出來,後續會繼續編寫,😄 上海前端有合適崗位的小夥伴能夠聯繫我,杭州的機會也在看,失業中.... 微信號: Nnordon_Wang