Vuex源碼分析(轉)

 

當咱們用vue在開發的過程當中,常常會遇到如下問題html

 多個vue組件共享狀態vue

   Vue組件間的通信vuex

在項目不復雜的時候,咱們會利用全局事件bus的方式解決,但隨着複雜度的提高,用這種方式將會使得代碼難以維護,所以vue官網推薦了一種更好用的解決方案Vuex。api

Vuex是什麼數組

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。說直白一點,vuex就是把組件的共享狀態抽取出來,以一個全局單例模式管理的數據倉庫,裏面提供了對應用的數據存儲和對數據的操做,維護整個應用的公共數據。app

看源代碼以前,咱們看一下vuex的使用方法框架

/***code例子*****/
/**
 *  store.js文件
 *  建立store對象,配置state、action、mutation以及getter
 *   
 **/
import Vue from 'vue'
import Vuex from 'vuex'
//註冊插件
Vue.use(Vuex)

//建立store倉庫
export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

/**
 *  vue-index.js文件
 *  
 *
 **/

import Vue from 'vue'
import App from './../pages/app.vue'
import store from './store.js'
//將store掛載到根組件
new Vue({
  el: '#root',
  router,
  store, 
  render: h => h(App)
})

//app.vue
export default {
  computed: {
    
  },
  methods: {
    increment (obj) {
    //組件中調用increment方法
      this.$store.dispatch('increment', obj)
    }
  }
}

整個vuex的使用步驟以下:異步

一、安裝vuex插件ide

二、建立store狀態管理倉庫函數

三、將store注入到vue組件

四、使用dispatch調用action方法

框架核心流程

在分析源代碼以前,咱們先來看一下整個vuex的運行流程,下面這張圖是官方文檔中提供的核心思想圖

 

Vue Components:Vue組件。HTML頁面上,負責接收用戶操做等交互行爲,執行dispatch方法觸發對應action進行迴應。

dispatch:操做行爲觸發方法,是惟一能執行action的方法。

actions:操做行爲處理模塊。負責處理Vue Components接收到的全部交互行爲。包含同步/異步操做,支持多個同名方法,按照註冊的順序依次觸發。向後臺API請求的操做就在這個模塊中進行,包括觸發其餘action以及提交mutation的操做。

commit:狀態改變提交操做方法。對mutation進行提交,是惟一能執行mutation的方法。

mutations:狀態改變操做方法。是Vuex修改state的惟一推薦方法

state:頁面狀態管理容器對象。集中存儲Vue components中data對象的零散數據,全局惟一,以進行統一的狀態管理。頁面顯示所需的數據從該對象中進行讀取,利用Vue的細粒度數據響應機制來進行高效的狀態更新。

總體流程:

用戶在組件上的交互觸發的action,action經過調用(commit)mutations的方法去改變state的狀態,而state因爲加入了vue的數據響應機制,會致使對應的vue組件更新視圖,完成整個狀態更新。

目錄結構

源代碼分析前,首先咱們看一下目錄結構

 

module

  module-collection.js:建立模塊樹

  Module.js:建立模塊

plugins

  devtool.js:開發環境工具插件,針對chorme安裝 vue-tool 插件

  logger.js:state狀態改變日誌查詢

helpers.js:提供mapSate,mapMutations,mapActions,mapGetters 等操做api函數

index.js

index.exm.js:都是入口文件

mixin.js:混入方法,提供將store 倉庫掛載到每一個vue實例的方法

store.js:整個代碼核心,建立store對象

util.js:工具類

Vuex的安裝

Vue 經過Vue.use(Vuex)安裝vuex, use經過調用vuex對象的install方法將vuex載入,咱們看一下install方法的實現:

let Vue // 安裝的時候增長vue對象,避免引入vue包
//安裝插件
export function install(_Vue) {
    //Vue已經被初始化,說明已經安裝過,給出提示
    if (Vue) {
        if (process.env.NODE_ENV !== 'production') {
            console.error(
                '[vuex] already installed. Vue.use(Vuex) should be called only once.'
            )
        }
        return
    }
    Vue = _Vue
        //安裝vuex
    applyMixin(Vue)
}

// 自動安裝模塊
if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
}

這裏用了一個比較巧的方法,vue做爲參數傳入vuex,因此咱們不用經過導入vue,代碼打包的時候就不會將vue包含進來。

來看下applyMixin方法內部代碼

const version = Number(Vue.version.split('.')[0])
    //2.0以上用混入
if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
} else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    //重寫init方法
    const _init = Vue.prototype._init
    Vue.prototype._init = function(options = {}) {
        options.init = options.init ? [vuexInit].concat(options.init) :
            vuexInit
        _init.call(this, options)
    }
}

  function vuexInit() {
    const options = this.$options
        // store 注入
    if (options.store) { //根組件
        this.$store = typeof options.store === 'function' ?
            options.store() : //模塊重用
            options.store
    } else if (options.parent && options.parent.$store) { //其餘非根組件注入,向上傳遞$store,保證任何組件均可以訪問到
        this.$store = options.parent.$store
    }
}

根據不一樣版本,2.x.x以上版本,使用 hook 的形式進行注入,或使用封裝並替換Vue對象原型的_init方法,實現注入。

將初始化Vue根組件時傳入的store設置到this對象的$store屬性上,子組件從其父組件引用$store屬性,層層嵌套進行設置。在任意組件中執行 this.$store 都能找到裝載的那個store對象

接下來咱們來看狀態管理庫的建立

咱們從構造函數一步一步分析

ModuleCollection構建

if (process.env.NODE_ENV !== 'production') {
        assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) //沒有vue對象說明沒有注入
        assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
        assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
        plugins = [], //這個選項暴露出每次 mutation 的鉤子。Vuex 插件就是一個函數,它接收 store 做爲惟一參數
            strict = false //嚴格模式下,不管什麼時候發生了狀態變動且不是由 mutation 函數引發的,將會拋出錯誤
    } = options

    let {
        state = {} //狀態
    } = options
    if (typeof state === 'function') {
        state = state() //狀態複用,使用一個函數來聲明模塊狀態
    }

    // store internal state
    this._committing = false //是否在提交狀態
    this._actions = Object.create(null) //action對象存儲
    this._mutations = Object.create(null) //mutation對象存儲
    this._wrappedGetters = Object.create(null) //裝後的getters集合對象
    this._modules = new ModuleCollection(options) //封裝module集合對象
    this._modulesNamespaceMap = Object.create(null) //建立命名空間
    this._subscribers = []
    this._watcherVM = new Vue() //vue實例,Vue組件用於watch監視變化,至關於bus

首先對執行環境作判斷,vuex必須已經安裝而且支持Promise語法,接下來對輸入的參數進行簡單的處理以及建立一些屬性,用於後續操做,其中有一步操做this._modules = new ModuleCollection(options),對傳入的options進行處理,生成模塊集合。來看一下ModuleCollection的操做

constructor(rawRootModule) {
    // register root module (Vuex.Store options)
    //註冊根模塊
    this.register([], rawRootModule, false)
}
register(path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
        assertRawModule(path, rawModule)
    }
    //生成模塊對象
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
        this.root = newModule //根模塊
    } else {
        //子元素把關係添加到父元素,形式關係樹
        const parent = this.get(path.slice(0, -1))
        parent.addChild(path[path.length - 1], newModule)
    }

    // 註冊嵌套的模塊
    if (rawModule.modules) {
        forEachValue(rawModule.modules, (rawChildModule, key) => {
            this.register(path.concat(key), rawChildModule, runtime)
                //this.register(['cart',{cartmodules},runtime])
        })
    }
}

ModuleCollection主要將傳入的options對象整個構造爲一個module對象,並循環調用 this.register([key], rawModule, false) 爲其中的modules屬性進行模塊註冊,使其都成爲module對象,最後options對象被構形成一個完整的狀態樹。ModuleCollection類還提供了modules的更替功能

modules屬性中每個模塊都是一個模塊對象,而傳入的option會被當成是整個狀態樹的根模塊。

咱們接下來看一下了解一下module對象

    constructor(rawModule, runtime) {
        this.runtime = runtime //運行時間
        this._children = Object.create(null) //children子模塊
        this._rawModule = rawModule //模塊內容
        const rawState = rawModule.state //模塊狀態
        this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} //state
    }

對象是對傳入的每個module參數進行封裝,添加了存儲的子模塊,已達到狀態樹的構建目的,同時提供一些基本操做的方法。

咱們來看一下官網提供的購物車的例子構建完的moduleCollection對象

這個利用樹形結構,清晰地表達出了整個數據倉庫數據的關係,moduleCollection將成爲後期數據和操做從新組織和查找基礎。

dispatch和commit函數設置

回到store的構造函數

const store = this
const { dispatch, commit } = this
//dispatch 方法
this.dispatch = function boundDispatch(type, payload) {
        return dispatch.call(store, type, payload)
    }
    //commit 方法
this.commit = function boundCommit(type, payload, options) {
    return commit.call(store, type, payload, options)
}

封裝dispatch 和commit方法,具體實現以下:

commit(_type, _payload, _options) {
        // check object-style commit
        const {
            type,
            payload,
            options
        } = unifyObjectStyle(_type, _payload, _options)

        const mutation = { type, payload }
        const entry = this._mutations[type] //變化有沒有註冊這個類型  commit致使mutation
        if (!entry) {
            if (process.env.NODE_ENV !== 'production') {
                console.error(`[vuex] unknown mutation type: ${type}`)
            }
            return
        }
        //處理commit
        this._withCommit(() => {
            entry.forEach(function commitIterator(handler) {
                handler(payload) //mutation操做
            })
        })

        // 訂閱者函數遍歷執行,傳入當前的mutation對象和當前的state
        this._subscribers.forEach(sub => sub(mutation, this.state))

        //新版本silent去掉了
        if (
            process.env.NODE_ENV !== 'production' &&
            options && options.silent
        ) {
            console.warn(
                `[vuex] mutation type: ${type}. Silent option has been removed. ` +
                'Use the filter functionality in the vue-devtools'
            )
        }
    }
    //觸發action
dispatch(_type, _payload) {
    // check object-style dispatch
    const {
        type,
        payload
    } = unifyObjectStyle(_type, _payload)

    const entry = this._actions[type] //觸發action配置表
    if (!entry) {
        if (process.env.NODE_ENV !== 'production') {
            console.error(`[vuex] unknown action type: ${type}`)
        }
        return
    }
    return entry.length > 1 ?
        Promise.all(entry.map(handler => handler(payload))) :
        entry[0](payload)
}

dispatch的功能是觸發並傳遞一些參數(payload)給對應type的action。Dispatch提供了兩種方法的調用:

因此使用了unifyObjectStyle對傳入的參數作了統一化處理。接着找到對應type的action,並逐個執行。

 

commit方法和dispatch方法大同小異,可是比dispatch複雜一點。每次調用都要對訂閱者遍歷執行,這個操做的緣由後面再解析。同時每次執行mutation方法集合時,都要調用函數_withCommit方法。

_withCommit是一個代理方法,全部觸發mutation的進行state修改的操做都通過它,由此來統一管理監控state狀態的修改。實現代碼以下。

    _withCommit(fn) {
        const committing = this._committing
        this._committing = true
        fn()
        this._committing = committing
    }

每次提交前保存當前commit狀態,而後置爲true後進行本次操做,操做完成後再還本來次修改以前的狀態。咱們上面說過mutation是Vuex修改state的惟一推薦方法。這樣設置的做用有利於狀態的跟蹤,state的修改來自於mutation。那是如何跟蹤的呢?咱們看下面一段代碼:

function enableStrictMode(store) {
    store._vm.$watch(function() { return this._data.$$state }, () => {
        if (process.env.NODE_ENV !== 'production') {
            //若是不是經過commit函數,提示
            assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
        }
    }, { deep: true, sync: true })
}

這裏經過vue $watch 方法監控每一次data的變化,若是在開發環境而且committing爲false的話,則代表不是經過mutation操做狀態變化,提示非法。同時這也解釋了官網要求mutation必須爲同步操做state。由於若是異步的話,異步函數調用完成後committing已經還原狀態,而回調函數還沒調用,$watch尚未接收到data的變化,當回調成功時,committing已經被置爲false,不利於狀態的跟蹤。

模塊的安裝

接下來,構造函數進行模塊的安裝

// init root module. 初始化根模塊
// this also recursively registers all sub-modules 遞歸全部子模塊
// and collects all module getters inside this._wrappedGetters 把全部的getters對象都收集到Getter裏面
installModule(this, state, [], this._modules.root)

咱們看一下installModule的具體內容

function installModule(store, rootState, path, module, hot) {
    const isRoot = !path.length //是否爲根元素
    const namespace = store._modules.getNamespace(path) //命名空間

    // register in namespace map 若是是命名空間模塊,推送到map對象裏面
    if (module.namespaced) {
        store._modulesNamespaceMap[namespace] = module
    }

    // set state
    //將模塊的state添加到state鏈中,是的能夠按照 state.moduleName 訪問
    if (!isRoot && !hot) {
        const parentState = getNestedState(rootState, path.slice(0, -1))
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
            //確保屬性被建立後也是響應式的
            Vue.set(parentState, moduleName, module.state)
        })
    }
    //模塊上下文
    /*local {
        dispatch,
        commit,
        getters,
        state
    }
    */
    const local = module.context = makeLocalContext(store, namespace, path)
        // 註冊對應模塊的mutation,供state修改使用
    module.forEachMutation((mutation, key) => {
            const namespacedType = namespace + key
            registerMutation(store, namespacedType, mutation, local)
        })
        // 註冊對應模塊的action,供數據操做、提交mutation等異步操做使用
    module.forEachAction((action, key) => {
            const namespacedType = namespace + key
            registerAction(store, namespacedType, action, local)
        })
        // 註冊對應模塊的getters,供state讀取使用
    module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key
        registerGetter(store, namespacedType, getter, local)
    })

  
}

判斷是不是根目錄,以及是否設置了命名空間,若存在則在namespace中進行module的存儲,在不是根組件且不是 hot 條件的狀況下,經過getNestedState方法拿到該module父級的state,拿到其所在moduleName,調用 Vue.set(parentState, moduleName, module.state) 方法將其state設置到父級state對象的moduleName屬性中,由此實現該模塊的state註冊。這裏創建了一條可觀測的data數據鏈。

接下來定義local變量和module.context的值,執行makeLocalContext方法,爲該module設置局部的 dispatch、commit方法以及getters和state,爲的是在局部的模塊內調用模塊定義的action和mutation。也就是官網提供的這種調用方式

定義local環境後,循環註冊咱們在options中配置的action以及mutation等,這種註冊操做都是大同小異,咱們來看一下注冊mutation的步驟:

/***registerMutation***/
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)
    })
}

type做爲id,把對應的值函數封裝後存儲在數組裏面,而後做爲store._mutations的屬性。store._mutations收集了咱們傳入的全部mutation函數

action和getter的註冊也是同理的,只是action handler比mutation handler以及getter wrapper多拿到dispatch和commit操做方法,所以action能夠進行dispatch action和commit mutation操做。

註冊完了根組件的actions、mutations以及getters後,遞歸調用自身,爲子組件註冊其state,actions、mutations以及getters等。

/***子模塊代碼****/
  module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child, hot)
    })
/***子模塊代碼end****/

Store._vm組件安裝

執行完各module的install後,執行resetStoreVM方法,進行store組件的初始化。

resetStoreVM(this, state)
 function resetStoreVM(store, state, hot) {
    const oldVm = store._vm

    // bind store public getters
    store.getters = {}
    const wrappedGetters = store._wrappedGetters
    const computed = {}
        // 循環全部處理過的getters,並新建computed對象進行存儲,經過Object.defineProperty方法爲getters對象創建屬性,
        //使得咱們經過this.$store.getters.xxxgetter可以訪問到該getters
    forEachValue(wrappedGetters, (fn, key) => {
        // use computed to leverage its lazy-caching mechanism
        computed[key] = () => fn(store)
        Object.defineProperty(store.getters, key, {
            get: () => store._vm[key],
            enumerable: true // for local getters
        })
    })

    // use a Vue instance to store the state tree
    // suppress warnings just in case the user has added
    // some funky global mixins
    const silent = Vue.config.silent
    Vue.config.silent = true
        // 設置新的storeVm,將當前初始化的state以及getters做爲computed屬性(剛剛遍歷生成的)
    store._vm = new Vue({
        data: {
            $$state: state //至關於總線
        },
        computed
    })
    Vue.config.silent = silent

    // enable strict mode for new vm
    // 該方法對state執行$watch以禁止從mutation外部修改state
    if (store.strict) {
        enableStrictMode(store)
    }
    // 若不是初始化過程執行的該方法,將舊的組件state設置爲null,
    //強制更新全部監聽者(watchers),待更新生效,DOM更新完成後,執行vm組件的destroy方法進行銷燬,減小內存的佔用
    if (oldVm) {
        if (hot) {
            // dispatch changes in all subscribed watchers
            // to force getter re-evaluation for hot reloading.
            store._withCommit(() => {
                oldVm._data.$$state = null
            })
        }
        Vue.nextTick(() => oldVm.$destroy())
    }
}

resetStoreVm方法建立了當前store實例的_vm組件,至此store就建立完畢了

plugin注入

最後執行plugin插件

//打印變化先後的值
plugins.forEach(plugin => plugin(this))

//若是配置了開發者工具
if (Vue.config.devtools) {
    devtoolPlugin(this)
}

Plugins.foreach 默認執行了createlogger方法,咱們來看createLogger的實現代碼:

export default function createLogger({
    collapsed = true,
    filter = (mutation, stateBefore, stateAfter) => true,
    transformer = state => state,
    mutationTransformer = mut => mut
} = {}) {
    return store => {
        let prevState = deepCopy(store.state)

        store.subscribe((mutation, state) => {
            if (typeof console === 'undefined') {
                return
            }
            const nextState = deepCopy(state)

            if (filter(mutation, prevState, nextState)) {
                const time = new Date()
                const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
                const formattedMutation = mutationTransformer(mutation)
                const message = `mutation ${mutation.type}${formattedTime}`
                const startMessage = collapsed ?
                    console.groupCollapsed :
                    console.group

                // render
                try {
                    startMessage.call(console, message)
                } catch (e) {
                    console.log(message)
                }

                console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
                console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
                console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))

                try {
                    console.groupEnd()
                } catch (e) {
                    console.log('—— log end ——')
                }
            }

            prevState = nextState
        })
    }
}

createLogger經過store.subscribe將方法添加到訂閱,待觸發的時候調用。這裏經過先後state狀態的打印對比,跟蹤狀態變動。

devtoolPlugin則是對vue-tool控制檯信息的添加監控

mapSate,mapMutations,mapActions,mapGetters函數實現

Vuex還提供了mapSate,mapMutations,mapActions,mapGetters輔助函數來幫助咱們生成計算屬性和對應方法。下面用mapMutations爲例來分析一下實現過程

首先看調用的方式:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 將 `this.increment()` 映射爲 `this.$store.commit('increment')`

      // `mapMutations` 也支持載荷:
      'incrementBy' // 將 `this.incrementBy(amount)` 映射爲 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 將 `this.add()` 映射爲 `this.$store.commit('increment')`
    }),
    ...mapMutations('some/nested/module', [  //在模塊some/nested/module查看
    'foo', //將 `this.increment()` 映射爲 `this.$store.commit('some/nested/module/foo')`
    'bar'
  ])
  }
}

看一下源代碼:

export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedMutation(...args) {
        if (namespace && !getModuleByNamespace(this.$store, 'mapMutations', namespace)) {
            return
        }
        return this.$store.commit.apply(this.$store, [val].concat(args))
    }
})
return res
})

  //返回對象數組 key-value
function normalizeMap(map) {
    return Array.isArray(map) ?
        map.map(key => ({ key, val: key })) :
        Object.keys(map).map(key => ({ key, val: map[key] }))
}

//處理命名空間參數問題
function normalizeNamespace(fn) {
    return (namespace, map) => {
        if (typeof namespace !== 'string') {
            map = namespace
            namespace = ''
        } else if (namespace.charAt(namespace.length - 1) !== '/') {
            namespace += '/'
        }
        return fn(namespace, map)
    }
}

//經過命名空間找模塊
function getModuleByNamespace(store, helper, namespace) {
    const module = store._modulesNamespaceMap[namespace]
    if (process.env.NODE_ENV !== 'production' && !module) {
        console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
    }
    return module
}

其實就是用normalizeNamespace和normalizeMap去統一化參數,生成key-value的對象組,而後對函數驚醒封裝,調用commit方法。

至此,整個vuex的源碼核心就分析完成。源碼中還有一些工具函數相似registerModule、unregisterModule、hotUpdate、watch以及subscribe等,這些方法都是對上面代碼行爲的封裝調用,而後暴露出來的接口,都比較容易理解。

 

連接:https://www.cnblogs.com/caizhenbo/p/7380200.html

相關文章
相關標籤/搜索