手寫Vue2核心(八):vuex實(shí)現(xiàn)

準(zhǔn)備工作

如果前面有自行實(shí)現(xiàn)過vue-router吗坚,那這里就沒有工作了馏谨,否則移步手寫Vue2核心(七):vue-router實(shí)現(xiàn)

VueRouter與install

vuex的引用比vue-router拆分得細(xì)一點(diǎn)彼绷,但實(shí)現(xiàn)原理等同于vue-router权旷,一些重復(fù)的實(shí)現(xiàn)原理就不過多贅述了织盼,直接上代碼

// vuex/index.js
import { Store, install } from './store'

// 兩種導(dǎo)出方式闻牡,方便用戶可以通過 import {Store},或者通過 import Vuex,通過 Vuex.Store 和 Vuex.install
export {
    Store,
    install
}

export default {
    Store,
    install
}
// vuex/store.js
import { applyMixin } from './mixin'

export let Vue // 此 Vue 是用戶注冊(cè)插件時(shí)傳入的 Vue 構(gòu)造函數(shù)
export class Store {
    constructor (options) {
        console.log(options)
    }
}

// 搞得太花里胡哨驮捍,但最終還是在 vuex/index.js 中將 install和store 導(dǎo)出疟呐,所以這里怎么華麗花哨并不重要,能導(dǎo)出install進(jìn)行mixin注入即可
// 實(shí)現(xiàn)原理依舊等同于 vue-router
export const install = (_Vue) => {
    Vue = _Vue
    applyMixin(Vue)
}
// vuex/minxin.js东且,等同于vue-router的install.js
function vueInit () {
    if (this.$options.store) {
        this.$store = this.$options.store // 給根屬性增加 $store 屬性
    } else if (this.$parent && this.$parent.$store) {
        this.$store = this.$parent.$store
    }
}

export const applyMixin = (Vue) => {
    Vue.mixin({
        beforeCreate: vueInit // 繼續(xù)拆启具,原理還是一樣,通過查找父組件的$store屬性來判斷獲取實(shí)例
    })
}

響應(yīng)式數(shù)據(jù)珊泳,實(shí)現(xiàn)state與getters

vuexstate鲁冯,相當(dāng)于datagetters相當(dāng)于computed色查,因此getters是具備緩存薯演,且不同于computed,是不允許設(shè)置值的(vuex中提供的commit和dipath都不會(huì)直接操作getters)

vuex是衍生于Vue的秧了,只能供Vue使用跨扮,其主要原因在于實(shí)現(xiàn)中,是通過創(chuàng)建一個(gè)新的Vue實(shí)例验毡,來掛載到Store._vm上衡创,這樣做的原因是Store是具備響應(yīng)式數(shù)據(jù)變化的,當(dāng)數(shù)據(jù)變化時(shí)晶通,會(huì)觸發(fā)視圖渲染

頁(yè)面中對(duì)Store取值時(shí)璃氢,會(huì)觸發(fā)Vue的依賴收集,但是state本身是沒必要去掛載到Vue._vm上的(不會(huì)變?yōu)閷?shí)例屬性)录择。Vue中提供了$符拔莱,來設(shè)置這些屬性不會(huì)被Vue代理。文檔傳送門:vue.data

getters是以函數(shù)的形式來定義取值的方法隘竭,具備緩存功能。而由于所有屬性均為函數(shù)讼渊,所以需要執(zhí)行才能取值动看,并且不能默認(rèn)幫用戶全部執(zhí)行,否則取值就會(huì)各種不正確爪幻,而是應(yīng)該在使用時(shí)再進(jìn)行取值

通過Object.defineProperty來對(duì)getters進(jìn)行劫持菱皆,當(dāng)訪問屬性時(shí),去調(diào)用其對(duì)應(yīng)的函數(shù)執(zhí)行挨稿。而getters是具備緩存功能的仇轻,所以需要將所有getters中定義的屬性都放到計(jì)算屬性中

// vuex/store.js
+ const forEachValue = (obj, cb) => {
+     Object.keys(obj).forEach(key => cb(obj[key], key))
+ }

export let Vue // 此 Vue 是用戶注冊(cè)插件時(shí)傳入的 Vue 構(gòu)造函數(shù)
export class Store {
    constructor (options) {
+       const computed = {}

+       // getters實(shí)現(xiàn)
+       this.getters = {}
+       forEachValue(options.getters, (value, key) => {
+           // 通過計(jì)算屬性替換直接執(zhí)行函數(shù)獲取值的形式,計(jì)算屬性具備緩存
+           computed[key] = () => value.call(this, this.state)

+           // value 是函數(shù)奶甘,getter 獲取的是屬性值篷店,所以在獲取的時(shí)候再去執(zhí)行函數(shù)獲取其對(duì)應(yīng)的值
+           // 而且這樣操作是每次取值時(shí)都能取到最新結(jié)果,否則直接執(zhí)行函數(shù)取值后面就沒法變更了
+           Object.defineProperty(this.getters, key, {
+               // 這里要用箭頭函數(shù)保證this指向,否則里面就不能用 call(this)
+               get: () => {
+                   // 用call是為了防止用戶在 getters 中使用了this疲陕,當(dāng)然正常都是通過傳入的state state.xxx方淤,而不是 this.state.xxx
+                   // return value.call(this, this.state) // 每次取值都會(huì)重新執(zhí)行用戶方法,性能差蹄殃,所以需要替換成計(jì)算屬性取值
+                   return this._vm[key]
+               }
+           })
+       })

+       // 用戶肯定是先使用 Vue.use携茂,再進(jìn)行 new Vue.Store({...}),所以這里的 Vue 已經(jīng)是可以拿到構(gòu)造函數(shù)的了
+       // 必須放到f forEachValue 后面诅岩,確保 computed 已經(jīng)有值
+       this._vm = new Vue({
+           data: {
+               // Vue中不會(huì)對(duì) $開頭的屬性進(jìn)行代理操作(不會(huì)掛到_vm上進(jìn)行代理)
+               // 但是其屬性依舊會(huì)被代理到(頁(yè)面獲取時(shí)依然會(huì)被收集依賴)讳苦,因?yàn)槲覀儾粫?huì)直接操作state,而是操作state.xxx吩谦,性能優(yōu)化
+               $$state: options.state
+           },
+           computed
+       })
+   }
+   get state () { // 屬性訪問器
+       return this._vm._data.$$state
+   }
}

實(shí)現(xiàn)commit與dispatch

簡(jiǎn)單的實(shí)現(xiàn)鸳谜,沒啥好說的,唯一需要講一下的是這里類的箭頭函數(shù)逮京,因?yàn)槲覀兪褂?code>commit或dispatch時(shí)卿堂,是可以通過解構(gòu)賦值的方式來調(diào)用函數(shù)的,但這樣取值會(huì)導(dǎo)致this指向當(dāng)前執(zhí)行上下文
而ES7中的箭頭函數(shù)是通過詞法解析來決定this指向的懒棉,所以解構(gòu)賦值取得的this會(huì)依舊指向Store

mutationsdispatch實(shí)現(xiàn):

export class Store {
    constructor (options) {
        // code...

+       // mutations實(shí)現(xiàn)
+       this.mutations = {}
+       this.actions = {}
+
+       forEachValue(options.mutations, (fn, key) => {
+           this.mutations[key] = payload => fn.call(this, this.state, payload)
+       })
+
+       forEachValue(options.actions, (fn, key) => {
+           this.actions[key] = payload => fn.call(this, this, payload)
+       })
    }
    get state () { // 屬性訪問器
        return this._vm._data.$$state
    }
+   commit = (type, payload) => { // ES7語(yǔ)法草描,類的箭頭函數(shù),表示this永遠(yuǎn)指向store實(shí)例
+       this.mutations[type](payload)
+   }
+   dispatch = (type, payload) => {
+       this.actions[type](payload)
+   }
}

ES7類的箭頭函數(shù)示例:

// ES7 類的箭頭函數(shù)編譯結(jié)果示例
window.name = 'window'

function Store () {
    this.name = 'Store'
    
    // 注釋掉下面四行策严,則commit方法中的this會(huì)指向window
    let { commit } = this
    this.commit = () => { // 獲取時(shí)穗慕,實(shí)例上的屬性優(yōu)先于原型上的
        commit.call(this) // 通過call,將commit執(zhí)行時(shí)this指向Store實(shí)例
    }
}

Store.prototype.commit = function () {
    console.log(this.name)
}

let {commit} = new Store() // 這里解構(gòu)取得的commit妻导,this指向的window

// 上面解構(gòu)賦值后相當(dāng)于這樣逛绵,所以調(diào)用的時(shí)候this指向其調(diào)用的上下文環(huán)境,所以為window
// let commit = Store.prototype.commit
commit() // 實(shí)例上也有一個(gè)commit倔韭,commit通過箭頭函數(shù)綁定了this指向

寫到這里术浪,一個(gè)簡(jiǎn)易版的vuex就實(shí)現(xiàn)了,但vuex里有一個(gè)東西叫模塊modules寿酌,這東西的實(shí)現(xiàn)胰苏,導(dǎo)致上面這個(gè)簡(jiǎn)易版的vuex需要完全重寫(只是重寫Store
但是上面的代碼是很好理解的,所以分開來說醇疼,下面開始真正實(shí)現(xiàn)官方vuex

vuex中模塊的用法

modules硕并,模塊化管理,具備命名空間進(jìn)行數(shù)據(jù)隔離秧荆。通過使用namespaced進(jìn)行隔離倔毙,沒有指定該屬性中mutations和actions會(huì)影響全局
而對(duì)到state,會(huì)將模塊名作為鍵乙濒,將其state作為值陕赃,添加到全局上
具體直接看文檔吧,說的很清楚了。官方文檔傳送門:modules

export default new Vuex.Store({
    state: { // data
        name: 'state',
        age: 10
    },
    getters: { // computed
        gettersAge (state) {
            return state.age + 20
        }
    },
    mutations: { // 同步變更
        changeAge (state, payload) {
            state.age = state.age + payload
        }
    },
    actions: {
        changeAge ({ commit }, payload) {
            setTimeout(() => {
                commit('changeAge', payload)
            })
        }
    },
    modules: {
        a: {
            state: {
                name: 'modules-a',
                age: 10
            },
            getters: {
                getName (staste) {
                    return staste.name
                }
            },
            mutations: { // 同步變更
                changeAge (state, payload) {
                    state.age = state.age + payload
                }
            },
            modules: {
                c: {
                    namespaced: true, // 有命名空間
                    state: {
                        name: 'modules-a-c',
                        age: 40
                    }
                }
            }
        },
        b: { // 沒有命名空間凯正,則changeAge方法也會(huì)影響到該模塊中的state屬性值
            namespaced: true, // 有命名空間
            state: {
                name: 'modules-b',
                age: 20
            },
            mutations: { // 同步變更
                changeAge (state, payload) {
                    state.age = state.age + payload
                }
            }
        }
    }
})

vuex中的模塊收集

其實(shí)就是轉(zhuǎn)換成一個(gè)樹形結(jié)構(gòu)來進(jìn)行管理毙玻,采用遞歸的方式,將用戶傳入的store參數(shù)轉(zhuǎn)換為樹形結(jié)構(gòu)廊散。每個(gè)模塊都被重新包裝成一個(gè)module

// module/module.js
export default class Module {
    constructor (rawModule) {
        this._raw = rawModule
        this._children = {}
        this.state = rawModule.state
    }
    getChild (key) { // 獲取子節(jié)點(diǎn)中的某一個(gè)
        return this._children[key]
    }
    addChild (key, module) { // 添加子節(jié)點(diǎn)
        this._children[key] = module
    }
}
// module/module-collection.js
import { forEachValue } from '../util'
import Module from './module'

// 將傳入的store轉(zhuǎn)成樹型結(jié)構(gòu) _row為該模塊鍵值桑滩,_children為該模塊modules中的鍵值(也轉(zhuǎn)為樹形結(jié)構(gòu)),_state為該模塊中寫的state允睹,深度優(yōu)先
export default class ModuleCollection {
    constructor (options) { // 遍歷用戶的屬性對(duì)數(shù)據(jù)進(jìn)行格式化操作
        this.root = null
        this.register([], options)
        console.log(this.root)
    }
    register (path, rootModule) {
        const newModule = new Module(rootModule)

        if (path.length === 0) { // 初始化
            this.root = newModule
        } else {
            // 將當(dāng)前模塊定義在父親身上
            const parent = path.slice(0, -1).reduce((memo, current) => {
                return memo.getChild(current)
            }, this.root)

            parent.addChild(path[path.length - 1], newModule)
        }

        // 如果還有modules就繼續(xù)遞歸
        if (rootModule.modules) {
            forEachValue(rootModule.modules, (module, moduleName) => {
                this.register(path.concat(moduleName), module)
            })
        }
    }
}
store構(gòu)造成樹形結(jié)構(gòu)

vuex中的模塊實(shí)現(xiàn)

這里實(shí)現(xiàn)的時(shí)沒有namespace的邏輯运准,具體是將模塊中的參與合并到全局上,對(duì)于用戶傳入配置分別進(jìn)行以下處理:

  • state: 將模塊中的state合并到全局缭受,通過模塊名稱作為全局state的鍵胁澳,并設(shè)置成響應(yīng)式
  • getters:將模塊中的getters合并到全局,同名的屬性米者,后面的會(huì)覆蓋前面的韭畸,并設(shè)置到自行創(chuàng)建的vue.computed上、
  • mutations和actions:實(shí)現(xiàn)邏輯一致蔓搞,就是將其放入棧中胰丁,等調(diào)用的時(shí)候依次調(diào)用
    所以邏輯總結(jié)起來就兩步:將用戶傳入的配置合并到全局,將數(shù)據(jù)設(shè)置為響應(yīng)式
// store.js
/**
 * @param {Object} store store實(shí)例
 * @param {Array} path 模塊父子關(guān)系喂分,初始為空
 * @param {Object} module 轉(zhuǎn)化為樹結(jié)構(gòu)后的模塊
 * @param {*} rootState 全局store的state
 * @descript 將模塊中的mutations和actions都合并到全局上锦庸,通過棧的方式依次push,調(diào)用的時(shí)候依次執(zhí)行
 * 將模塊中的 state 和 getters 也合并到全局上蒲祈,state會(huì)將模塊名設(shè)置為全局的鍵甘萧,而getters則是沒用namespace的話會(huì)合并到全局,后面同名的會(huì)覆蓋前面的
 */
const installMudole = (store, path, module, rootState) => {
    // store => [], store.modules => ['a'], store.modules.modules => ['a', 'c']
    if (path.length > 0) { // 是子模塊
        const parent = path.slice(0, -1).reduce((memo, current) => {
            return memo[current]
        }, rootState)

        // vue-router是使用Vue.util.defineReactive梆掸,所以這里寫成Vue.util.defineReactive(parent, path[path.length - 1], module.state)也可以
        // 因?yàn)槟繕?biāo)就是要把模塊定義成響應(yīng)式的扬卷,源碼路徑:/src/core/util
        // 這里不用set也能實(shí)現(xiàn)響應(yīng)式,因?yàn)橄旅鏁?huì)把 state 設(shè)置到創(chuàng)建的 Vue 上來實(shí)現(xiàn)響應(yīng)式酸钦,不過源碼中就是用的set
        Vue.set(parent, path[path.length - 1], module.state)
        // parent[path[path.length - 1]] = module.state // 但是這樣操作子模塊不是響應(yīng)式的
    }

    module.forEachMutation((mutation, key) => {
        store.mutations[key] = store.mutations[key] || []
        store.mutations[key].push(payload => mutation.call(store, module.state, payload))
    })
    module.forEachAction((action, key) => {
        store.actions[key] = store.actions[key] || []
        store.actions[key].push(payload => action.call(store, store, payload))
    })
    module.forEachChildren((childModule, key) => {
        installMudole(store, path.concat(key), childModule, rootState) // childModule.state
    })
    // 沒用namespace邀泉,則所有模塊的getters默認(rèn)都會(huì)合并到一個(gè)對(duì)象里,都是直接getters.xxx即可钝鸽,而不用getters.a.xxx
    module.forEachGetters((getterFn, key) => {
        store.wrapGetters[key] = () => getterFn.call(store, module.state)
    })
}

export class Store {
    constructor (options) {
        // 格式化用戶傳入的配置,格式化成樹結(jié)構(gòu)
        this._modules = new ModuleCollection(options)

        this.mutations = {} // 將用戶所有模塊的mutation都放到這個(gè)對(duì)象中
        this.actions = {} // 將用戶所有模塊的action都放到這個(gè)對(duì)象中
        this.getters = {}
        this.wrapGetters = {} // 臨時(shí)變量庞钢,存儲(chǔ)getters
        const state = options.state // 用戶傳入的全局state拔恰,還是非響應(yīng)式的

        // 將所有模塊中的mutations和actions合并到全局上,合并state和getters到全局上
        installMudole(this, [], this._modules.root, state)
        // 初始化與重置(源碼中因?yàn)樾枰獙?duì)熱更新進(jìn)行判斷基括,熱更新需要重置颜懊,但這里就是單純的初始化)
        // 主要干兩件事:將state設(shè)置成響應(yīng)式掛到store._vm上(通過new Vue),將getters掛到computed上
        resetStoreVM(this, state)
    }
    get state () { // 屬性訪問器
        return this._vm._data.$$state
    }
}

function resetStoreVM (store, state) {
    const computed = {}

    forEachValue(store.wrapGetters, (fn, key) => {
        computed[key] = fn // 將是所有的屬性放到computed中
        Object.defineProperty(store.getters, key, {
            get: () => store._vm[key]
        })
    })

    // 用戶肯定是先使用 Vue.use,再進(jìn)行 new Vue.Store({...})河爹,所以這里的 Vue 已經(jīng)是可以拿到構(gòu)造函數(shù)的了
    // 必須放到f forEachValue 后面匠璧,確保 computed 已經(jīng)有值
    store._vm = new Vue({
        data: {
            // Vue中不會(huì)對(duì) $開頭的屬性進(jìn)行代理操作(不會(huì)掛到_vm上進(jìn)行代理)
            // 但是其屬性依舊會(huì)被代理到(頁(yè)面獲取時(shí)依然會(huì)被收集依賴),因?yàn)槲覀儾粫?huì)直接操作state咸这,而是操作state.xxx夷恍,性能優(yōu)化
            $$state: state
        },
        computed
    })
}
parent[path[path.length - 1]] = module.state處理,未定義成響應(yīng)式

實(shí)現(xiàn)commit和dispatch

記錄了namespace后媳维,在獲取與調(diào)用對(duì)應(yīng)方法時(shí)酿雪,則是通過路徑名+方法的方式來調(diào)用。比如commit('a/getterAge', 20)侄刽,dispatch也是如此指黎。因此在初始化installMudole時(shí),需要將mutations/actions/getters都加上對(duì)應(yīng)路徑州丹。當(dāng)然這里的實(shí)現(xiàn)是不健全的醋安,vuex中如果存在namespace,則dispatch里使用commit墓毒,是不需要帶上相對(duì)路徑的吓揪,會(huì)去找自己的mutations中對(duì)應(yīng)的方法,這里并未實(shí)現(xiàn)

// store.js
const installMudole = (store, path, module, rootState) => {
+   const namespace = store._modules.getNamespace(path)
    // code...

    module.forEachMutation((mutation, key) => {
+       store.mutations[namespace + key] = store.mutations[namespace + key] || []
+       store.mutations[namespace + key].push(payload => mutation.call(store, module.state, payload))
    })
    module.forEachAction((action, key) => {
+       store.actions[namespace + key] = store.actions[namespace + key] || []
+       store.actions[namespace + key].push(payload => action.call(store, store, payload))
    })

    // 沒用namespace蚁鳖,則所有模塊的getters默認(rèn)都會(huì)合并到一個(gè)對(duì)象里磺芭,都是直接getters.xxx即可,而不用getters[a/xxx]
    module.forEachGetters((getterFn, key) => {
+       store.wrapGetters[namespace + key] = () => getterFn.call(store, module.state)
    })
}

export class Store {
+   commit = (type, payload) => { // ES7語(yǔ)法醉箕,類的箭頭函數(shù)钾腺,表示this永遠(yuǎn)指向store實(shí)例
+       if (this.mutations[type]) {
+           this.mutations[type].forEach(fn => fn(payload)) // 不同于之前,現(xiàn)在的mutations已經(jīng)是個(gè)包含模塊中mutations的數(shù)組
+       }
+   }
+   dispatch = (type, payload) => {
+       if (this.actions[type]) {
+           this.actions[type].forEach(fn => fn(payload))
+       }
+   }
}

module中添加獲取命名空間讥裤,構(gòu)建成樹結(jié)果時(shí)可進(jìn)行命名空間判斷放棒,是否需要添加成a/method的形式,否則調(diào)用路徑依舊為全局直接調(diào)方法名的方式

// module/module.js
export default class Module {
+   get namespaced () {
+       return !!this._raw.namespaced
+   }
}

// module/module-collection.js
export default class ModuleCollection {
+   getNamespace (path) {
+       let module = this.root
+       return path.reduce((namespaced, key) => {
+           module = module.getChild(key)
+           // 如果父模塊沒有namespaced己英,子模塊有间螟,那么調(diào)用的時(shí)候就只需要寫子模塊,比如 c/ 否則就是a/c/
+           return namespaced + (module.namespaced ? key + '/' : '')
+       }, '')
+   }
}

插件實(shí)現(xiàn)

這里不是在實(shí)現(xiàn)vuex损肛,而是在實(shí)現(xiàn)自己開發(fā)一個(gè)vuex插件厢破,因?yàn)椴粫?huì)實(shí)現(xiàn)plugin,所以需要自行切換成原生vuex

或許大多數(shù)人都不知道vuex插件治拿,官網(wǎng)高階中有寫摩泪,傳送門:vuex插件

面試題:如何實(shí)現(xiàn)vuex持久化緩存?

  • vuex無(wú)法實(shí)現(xiàn)持久化緩存劫谅,頁(yè)面刷新的時(shí)候就會(huì)清除已經(jīng)保存的數(shù)據(jù)见坑。而有一個(gè)插件就是專門用于解決vuex持久化問題的:vuex-persist
    那為什么有localstorage嚷掠,還需要借助vuex-persist呢?
  • 因?yàn)?code>localstorage數(shù)據(jù)變了頁(yè)面數(shù)據(jù)也不會(huì)自動(dòng)刷新荞驴,并非響應(yīng)式的

插件接收一個(gè)數(shù)組不皆,數(shù)組每一項(xiàng)均為函數(shù),如果有多個(gè)插件熊楼,自上而下執(zhí)行
官方提供了一個(gè)開發(fā)使用的插件logger霹娄,當(dāng)然因?yàn)榛径紩?huì)安裝vue-devtools,所以并不會(huì)用到

vuex主要提供了兩個(gè)方法來讓用戶自定義插件孙蒙,分別是subscribereplaceState项棠,subscribe用于訂閱觸發(fā)commit事件,replaceState用于初始化時(shí)替換頁(yè)面數(shù)據(jù)
支持自定義模式挎峦,這里replaceState只實(shí)現(xiàn)storage

插件的實(shí)現(xiàn)思路比較簡(jiǎn)單香追,就是發(fā)布訂閱。但是有一個(gè)問題坦胶,就是installMudole中透典,之前的實(shí)現(xiàn)是通過用戶定義的state(掛載store._vm._data.$$state上),初始化模塊時(shí)顿苇,會(huì)為commit注冊(cè)事件
replaceState的實(shí)現(xiàn)峭咒,更改的是store上的state,導(dǎo)致視圖渲染無(wú)效纪岁。因此需要在commit時(shí)重新去store上獲取對(duì)應(yīng)的值

+ // 最開始定義的時(shí)候凑队,用的是用戶傳入的 state,但是一旦執(zhí)行了replaceState幔翰,則 $$state 被替換
+ // Vue.set(parent, path[path.length - 1], module.state) 用的是最初傳入定義成響應(yīng)式的state(也就是rootState),而replaceState設(shè)置的是store的state
+ // 一個(gè)是 module的state漩氨,一個(gè)是變更的store的state,就會(huì)導(dǎo)致commit時(shí)數(shù)據(jù)取值不正確(一直是舊數(shù)據(jù))遗增,所以需要去store上重新獲取
+ const getState = (store, path) => { // store.state獲取的是最新狀態(tài)
+     return path.reduce((rootState, current) => {
+         return rootState[current]
+     }, store.state)
+ }

const installMudole = (store, path, module, rootState) => {
    // code...

    module.forEachMutation((mutation, key) => {
        store.mutations[namespace + key] = store.mutations[namespace + key] || []
-       store.mutations[namespace + key].push(payload => mutation.call(store, module.state, payload))
+       store.mutations[namespace + key].push(payload => mutation.call(store, getState(store, path), payload))
    })
    // 沒用namespace叫惊,則所有模塊的getters默認(rèn)都會(huì)合并到一個(gè)對(duì)象里,都是直接getters.xxx即可做修,而不用getters[a/xxx]
    module.forEachGetters((getterFn, key) => {
-       store.wrapGetters[namespace + key] = () => getterFn.call(store, module.state)
+       store.wrapGetters[namespace + key] = () => getterFn.call(store, getState(store, path))
    })
}

export class Store {
    constructor (options) {
        // code...
+       this._subscribe = [] // 因?yàn)槟軅魅攵鄠€(gè)插件霍狰,所以會(huì)有多個(gè)訂閱

+       // 默認(rèn)插件就會(huì)被執(zhí)行,從上往下執(zhí)行
+       options.plugins.forEach(plugin => plugin(this))
    }
+   subscribe (fn) {
+       this._subscribe.push(fn)
+   }
+   replaceState (newState) {
+       this._vm._data.$$state = newState
+   }
    commit = (type, payload) => { // ES7語(yǔ)法饰及,類的箭頭函數(shù)蔗坯,表示this永遠(yuǎn)指向store實(shí)例
        if (this.mutations[type]) {
            this.mutations[type].forEach(fn => fn(payload)) // 不同于之前,現(xiàn)在的mutations已經(jīng)是個(gè)包含模塊中mutations的數(shù)組

            // 變更后燎含,觸發(fā)插件訂閱執(zhí)行
+           this._subscribe.forEach(fn => fn({ type, payload }, this.state))
        }
    }
}

斷言 - 非法操作實(shí)現(xiàn)

原生vuex步悠,如果采用嚴(yán)格模式strict: true,那么在mutations中采用異步待會(huì)將會(huì)報(bào)錯(cuò)瘫镇,非合法操作也會(huì)報(bào)錯(cuò)
主要通過在store_withCommiting來包裹合法操作賦值鼎兽,實(shí)現(xiàn)思路是通過watcher進(jìn)行監(jiān)聽(同步,深度)铣除,store中添加標(biāo)記位谚咬,當(dāng)數(shù)據(jù)變化時(shí),如果斷言為false則會(huì)出現(xiàn)報(bào)錯(cuò)尚粘。挺好懂的择卦,文章已經(jīng)這么長(zhǎng)了,能看到這里你估計(jì)只是來復(fù)習(xí)底層的郎嫁,直接看代碼

const installMudole = (store, path, module, rootState) => {
    if (path.length > 0) { // 是子模塊
-       Vue.set(parent, path[path.length - 1], module.state)
+       store._withCommiting(() => Vue.set(parent, path[path.length - 1], module.state))
    }
}

export class Store {
    constructor (options) {
        // code...
+       this.strict = options.strict
+       this._commiting = false
+       this._withCommiting = function (fn) {
+           const commiting = this._commiting
+           this._commiting = true
+           fn() // 修改狀態(tài)的邏輯
+           this._commiting = !commiting
+       }
    }
    replaceState (newState) {
-       this._vm._data.$$state = newState
+       this._withCommiting(() => (this._vm._data.$$state = newState))
    }
    commit = (type, payload) => { // ES7語(yǔ)法秉继,類的箭頭函數(shù),表示this永遠(yuǎn)指向store實(shí)例
        if (this.mutations[type]) {
-           this.mutations[type].forEach(fn => fn(payload))
+           // 執(zhí)行_withCommiting時(shí)泽铛,_commiting為true尚辑,所以不會(huì)報(bào)錯(cuò)
+           // 如果mutations中有異步代碼,那么異步代碼執(zhí)行后盔腔,觸發(fā)watcher監(jiān)聽變化杠茬,此時(shí)的_commiting會(huì)為false,就會(huì)報(bào)錯(cuò)
+           this._withCommiting(() => this.mutations[type].forEach(fn => fn(payload))) // 不同于之前弛随,現(xiàn)在的mutations已經(jīng)是個(gè)包含模塊中mutations的數(shù)組

            // 變更后瓢喉,觸發(fā)插件訂閱執(zhí)行
            this._subscribe.forEach(fn => fn({ type, payload }, this.state))
        }
    }
}

function resetStoreVM (store, state) {
    // code...

+   if (store.strict) {
+       // 因?yàn)閣atcher執(zhí)行時(shí)異步的,需要加上 {sync: true} 設(shè)置為同步舀透,文檔沒有栓票,需要自行看源碼
+       store._vm.$watch(() => store._vm._data.$$state, () => {
+           console.assert(store._commiting, '非法操作')
+       }, { sync: true, deep: true })
+   }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市愕够,隨后出現(xiàn)的幾起案子走贪,更是在濱河造成了極大的恐慌,老刑警劉巖链烈,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厉斟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡强衡,警方通過查閱死者的電腦和手機(jī)擦秽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漩勤,“玉大人感挥,你說我怎么就攤上這事≡桨埽” “怎么了触幼?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)究飞。 經(jīng)常有香客問我置谦,道長(zhǎng)堂鲤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任媒峡,我火速辦了婚禮瘟栖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谅阿。我一直安慰自己半哟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布签餐。 她就那樣靜靜地躺著寓涨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪氯檐。 梳的紋絲不亂的頭發(fā)上戒良,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音男摧,去河邊找鬼蔬墩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛耗拓,可吹牛的內(nèi)容都是我干的拇颅。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼乔询,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼樟插!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竿刁,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤黄锤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后食拜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸵熟,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年负甸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了流强。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呻待,死狀恐怖打月,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚕捉,我是刑警寧澤奏篙,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站迫淹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜虫几,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梗脾。 院中可真熱鬧,春花似錦盹靴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至辕狰,卻和暖如春改备,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔓倍。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工悬钳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人偶翅。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓默勾,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親聚谁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子母剥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容