zero-blog icon indicating copy to clipboard operation
zero-blog copied to clipboard

[email protected] 源码分析

Open PerseveranceZ opened this issue 6 years ago • 0 comments

[email protected] 源码分析

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

vuex

如果您还不熟悉 Vuex 的使用,请先去过一遍 Vuex 的 api ,跟着官方的 demo 走一遍,了解了 Vuex 的神奇之后,带着问题来一起看源码。

在进入源码之前,先列出几点对于Vuex的问题:

  1. action,mutation,getter 的回调函数里面的是怎么放入一个Store实例相关对象的(包含:dispatch,commit,state,rootState,getters,rooteGetters等)。

  2. registerModule 和 unregisterModule 。

  3. state 和 getter 是不是通过 new Vue() 来使其变为响应式的。

  4. 我们不能修改 state 是怎么做到的。

下面我们带着这几个问题来进入源码。

进入源码

分析源码使用的是 Vuex 2.3.1版本,建议 clone 下代码,使用本文提供的例子,跟着一起走,能加快理解。

目录结构

目录结构

  • module-collection.js:按照传入的选项,进行 module 收集,建立 module 之间的关系;

  • module.js:生成一个 module 对象 包含私有属性和方法;

  • devtool.js:chrome 插件相关;

  • logger.js:订阅相关操作日志;

  • helpers.js:工具函数,mapState,mapMutations,mapGetters,mapActions;

  • index.js:入口文件;

  • mixin.js:在Vue实例上注册 options.store;

  • store.js:提供 store 的各个 module 构建安装;

  • util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法;

入口开始

//index.js 入口文件

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'

export default {
  Store,
  install, 
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}

入口文件很简单,是vuex对外暴露的API:

  • Store:从我们 new Vuex.Store() 这样的方式,我们就该知道的是 Store 就是为我们生成状态树,并提供操作方法,定义操作规则的类

  • install:Vue 插件安装,通过 Vue.use(Vuex) 执行,顺便看下 Vue.use 的代码;

  • mapState,mapMutations,mapGetters,mapActions 都是方便我们操作的函数。

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    /* istanbul ignore if */
    if (plugin.installed) {
      return
    }
    // additional parameters
    const args = toArray(arguments, 1)
    //参数首位放入 Vue
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    plugin.installed = true
    return this
  }
}

plugin.install.apply(plugin, args) 调用了插件的 install 方法

//store.js
import applyMixin from './mixin'
import devtoolPlugin from './plugins/devtool'
import ModuleCollection from './module/module-collection'
import { forEachValue, isObject, isPromise, assert } from './util'

let Vue

class Store {...}
//被调用 Vue 被传进来
export function install (_Vue) {
  if (Vue) {
  	 //Vue.use(Vuex) 第二次就会报错 通过保存 Vue 为变量来进行判断
  	 //在调用 Vue 内部的方法时,减少作用域链查找
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue) //挂载方法和变量
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

不难看出,当 Vue 执行了 install 函数,把 _Vue 作为参数传了进来,全局的 Vue 才有值,在用 Vue.use 便直接报错

applyMixin:

//mixins.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
  	//vue 2.0以上 使用 Vue 自带的 mixin 方法 在实例的 init 或者 beforeCreate上挂载 vuexInit
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
   // vue 1.x 也做了相应的兼容 可以看出来没有 mixin 注册起来会很蛋疼 重写 init 生命周期
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

	// Vuex 初始化钩子
	// 每次通过 new vue(),vue.component() 创建新的 Vue 实例时候,都会被注入执行
  function vuexInit () {
    const options = this.$options
    // store 注入
    if (options.store) {
    	// vueInit 步骤①
    	// this 都指向传入的 Vue
    	// this.$options.store 和 this.$store 访问的是一个 store
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
    	// vueInit 步骤②
      this.$store = options.parent.$store
    }
  }
}

因为对每个实例和组件执行以下方法,这就带来了一些问题, 我们公共型的组件是不需要 store 的, 但 Vue 其实并无法区分公共型组件和业务型组件,便取了一个折中的方式,都给 store 。

先不用着急理解 vuexInit 方法,我们先回顾下开始是如何初始注入 Store 的。

new Vue({
   el: '#root',
   router,
   store,
   render: h => h(App)
})

我们在 Vue 应用的根组件上注入了我们的 Store ,在生命周期执行到 init 或者 beforeCreated 的时候,执行了 vuexInit 步骤①,直接在当前根 Vue 上放了一个对象 store 并代理为 $store 。

我们创建子组件,编译过后其实执行的是 Vue.component() 方法,发现自身开始是没有 store 这个对象的,便“继承”一下父级的 store,通过 vueInit 步骤② $options.parent 来建立父子组件的关系

这也就是所说的单一状态树,在单页面应用中,所有组件默认访问同一个 Store 实例,这个 Store 实例是会自动向下游组件渗透的

走进 class Store

装载实例,配置 store :

import Vue from 'vue'
import Vuex from 'vuex'

// install Vuex 框架
Vue.use(Vuex)

//我们配置 OPTIONS,注意,module2 的 namespaced 为 true
const OPTIONS =  {
    // rootModule
    state: {
        name: 'commonState'
    },
    actions: {
        fetchApi({commit}) {
            commit('changeName')
        }
    },
    getters: {
        commonNameGetter: state => state.name
    },
    mutations: {
        changeName(state) {
            state.name = 'commonStateChanged'
        }
    },
     modules: {
        module1: {
            state: {
                name: 'module1State'
            },
            actions: {
                fetchApi({commit}) {
                    commit('changeName')
                }
            },
            getters: {
                nameGetter: state => state.name
            },
            mutations: {
                changeName(state) {
                    state.name = 'module1Changed'
                }
            },
            modules: {
                module2: {
                    state: {
                        name: 'module2State'
                    },
                    actions: {
                        fetchApi({commit}) {
                            commit('changeName')
                        }
                    },
                    getters: {
                        nameGetter: state => state.name
                    },
                    mutations: {
                        changeName(state) {
                            state.name = 'module2Changed'
                        }
                    },
                    namespaced: true
                }
            }
       }
    },
    strict: process.env.NODE_ENV !== 'production'
}
export default new Vuex.Store(OPTIONS)

在这里先区分一下几个概念:

  • options 下的第一层,是根模块( rootModule )
  • rootModule 下有一个孩子**( _children )名字叫 module1** 。
  • module1 下有一个孩子**( _children )名字叫 module2** 。

你应该已经看出,我们的实例配置的地方也有几个值得注意的点:

  • 三个 module 的 state 都有属性 name
  • 三个 module 的 actions 里面都有个 fetchApi 的方法
  • 三个 module的 mutations 里面都有个 changeName 的方法
  • module1 和 module2 都有 nameGetter
  • module2 有 namespaced 为 true 的配置

很明显,我们想通过这个实例看看 namespaced 的重要性。

因为有new操作符 Vuex.Store 就是个类,所以我们暴露出去的就是一个 Store 的实例,我们把整个状态树是一次性全部传入 Store 类的传入的 OPTIONS 也就是下面的 options

export class Store {
	// 构造函数, 实例化执行
  constructor (options = {}) {
  	// 确保 Vue 存在,new Store 之前,必须 Vue.use(Vuex)
	assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
	// 浏览器 Promise不存在,我们需要通过 package.json 中添加对 babel-polyfill 的依赖并在代码的入口加上 import 'babel-polyfill'
	assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
	
	// 断言函数代码
	// export function assert (condition, msg) { if (!condition) throw new Error(`[vuex] ${msg}`) }

	// es6 赋值结构取值
	// state 表示 rootState(rootModule的state),plugins 表示应用的插件、strict 表示是否开启严格模式。
    const {
      state = {},
      plugins = [],
      strict = false
    } = options
    
    if (typeof state === 'function') {
      state = state()
    }
    /***① 添加store内部状态 start***/
    this._committing = false 
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    /***① end***/

    const store = this
    const { dispatch, commit } = this
    
    /***② dispatch,commit start***/
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }
    /***② end***/
	 
    /***③ 严格模式 start***/	 
    this.strict = strict
    /***③ end***/	 
	
    /***④ installModule start***/	
    // 初始化rootModule
    // 同时递归的注册所有子 modules ,
    // 在 this._wrappedGetters 中收集所有 module 的 getters
    installModule(this, state, [], this._modules.root)
    /***④ end***/	
    
    /***⑤ resetStoreVM start***/	
    // 在 vm 中初始化响应式的 store
    // 同时注册所有的 _wrappedGetters 作为计算属性
    resetStoreVM(this, state)
    /***⑤ end***/	
    
    // 插件注册
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
  ....
  ....
}

整个constructor分为五个大块,我们逐一分解。

① 添加 store 内部状态
  • this._committing 标志一个提交状态,作用是保证对 Vuex 中 state 的修改只能通过 _withCommit 方法,在 mutation 的回调函数中,而不能在外部随意修改 state。
  • this._actions 用来存储用户定义的所有 actions。
  • this._mutations 用来存储用户定义所有 mutatins。
  • this._wrappedGetters 用来存储用户定义的所有 getters。
  • this._modules 用来存储所有运行时 module,并定义好他们的关系。
  • this._modulesNamespaceMap 用来存储 options.namespaced 为 true 的 module。
  • this._subscribers 用来存储所有对 mutation 变化的订阅者。
  • this._watcherVM 是一个 Vue 对象的实例,调用 $watch 方法。

我们注意到 this._modules 是通过 ModuleCollection 类来生成的。

//module-collection.js
export default class ModuleCollection {
  // rawRootModule 也就是我们初始配置的 options
  constructor (rawRootModule) {
    // ModuleCollection 实例的 root 对象存放 Module 实例
    // 第一次构建是非运行时,为 false
    this.root = new Module(rawRootModule, false)

    // rootModule 如果有 modules,注册所有相邻的 modules。
    // 根据 options,这里注册的是 module1
    if (rawRootModule.modules) {
      forEachValue(rawRootModule.modules, (rawModule, key) => {
        // key: 'module1', rawModule: module1的值
        this.register([key], rawModule, false)
      })
    }
  }
  
  get (path) {
  	 // 从 rootModule 中找到 path 中对应的 module
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  
  register (path, rawModule, runtime = true) {
    const parent = this.get(path.slice(0, -1))
    const newModule = new Module(rawModule, runtime)
    parent.addChild(path[path.length - 1], newModule)

    // 递归注册子 modules
    // 根据 options,这里注册 module2
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
  ....

ModuleCollection实例包含:

  • 属性:root
  • 原型方法:get,getNamespace,update (传入一个新 module,递归更新所有 module 的 actions,mutations,getters),register,unregister

进入Module类来看下constructor

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  // namespaced 属性是可以传可以不传的,所以默认都返回 false
  get namespaced () {
    return !!this._rawModule.namespaced
  }
  ...
}

Module的实例包含:

  • 属性:runtime,children,rawModule,rawState,namespaced;
  • 原型方法:addChild,removeChild,getChild,update (更新 module 的 _rawModule),forEachChild,forEachGetter,forEachAction,forEachMutation;

最终在 Store 实例的this._modules结构如下:

总结一下这两个类的作用:

就是构造一棵由 module 实例为节点的树( root 对象),并给这颗树和树的节点分别添加相应的属性和方法。

② dispatch和commit
dispatch (_type, _payload) {
  const {
      type,
      payload
  } = unifyObjectStyle(_type, _payload) // 配置参数处理

  // 所有的 action 是存在放 Store 内部状态 _actions 中
  const entry = this._actions[type]
  if (!entry) {
    console.error(`[vuex] unknown action type: ${type}`)
    return
  }
  // 写法简便兼顾性能
  return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
}

拿到 _actions 入口的引用entry

entry是个数组,长度等于1时直接执行里面的函数,当拿到的entry长度大于1,就会遍历按顺序执行。

不妨做个假设,因为 rootModule 和 module1 的 action 名字相同,但他们都没有 namespaced 为 true 的设置,所以在这里可能是在同一个entry里,mutation 同理。

  commit (_type, _payload, _options) {
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    
    // 所有的 mutation 是存在放 Store 内部状态 _mutations 中
    const entry = this._mutations[type]
    if (!entry) {
      console.error(`[vuex] unknown mutation type: ${type}`)
      return
    }
    // 专用修改 state 方法,下面会做说明
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 订阅者函数遍历执行,传入 mutation,state
    // 告诉所有订阅函数本次操作的 type,payload 和当前 state 状态啊。
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (options && options.silent) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

对于 actionsmutations 集合,是在 installModule函数 中添加的,稍后会看到。 现在我们来看下commit中的 _withCommit

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

只有 _withCommit 方法,才能更改当前 committing 的状态,也就是标识当前 state 是 Vuex 内部修改的。

③ 严格模式

resetStoreVM 中调用 enableStrictMode 函数会用到 this.strict 做判断来开启严格模式,具体在 resetStoreVM 中会说到。

有一点需要注意的是。严格模式下会观测所有 state 的变化,有一定性能开销,线上建议关闭。

④ installModule

把我们通过 options 传入的各种属性模块注册和安装,如果当前 module 中存在 modules 这个属性会被递归调用。

已实例来说,会被调用三次,分别是 rootModule,module1 和 module2。

// ①构造函数调用 installModule(this, state, [], this._modules.root)
// ②自身递归调用 installModule(store, rootState, path.concat(key), child, hot)

// store 表示当前 Store 实例,rootState 表示根 state,path 表示当前嵌套模块的路径数组,module 表示当前安装的模块,hot 当动态改变 modules 或者热更新的时候为 true。
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  
  // namespace 在只在一种情况下有值:
  // namespaced = true && 当前 module 不是 rootModule
  // (未设置 namespaced 为 true 和 rootModule,得到的 namespace 都为空)
  // 所以 rootModule 和 module1 都为空
  // module2 为 ['module2']
  const namespace = store._modules.getNamespace(path)

  // 在 _modulesNamespaceMap 注册 module.namespaced 为 true 的 module
  // 递归结束后这里存放了 module2
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }
	
  // 设置 state
  // ① rootModule 不进行设置,② 热更新 true 不进行设置
  if (!isRoot && !hot) {
    // 找到当前 module 的 父module,和 moduleName
    // module1 找的就是 rootModule,module2 同理
    // 所以 state 自动以 moduleName 来做命名空间了
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    // Vuex 自己修改了 state
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 分别对 rootModule,module1,module2 制作对应的上下文对象
  const local = module.context = makeLocalContext(store, namespace, path)
  

  module.forEachMutation((mutation, key) => {
    // 给每个 module._rawModule 下的每个 mutation 方法添加命名空间
    const namespacedType = namespace + key
    // 把当前 module 中 mutation 方法都放入当前 Store 实例的 _mutations
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    // 给每个 module._rawModule 下的每个 action 方法添加命名空间
    const namespacedType = namespace + key
    // 把当前 module 中 action 方法都放入当前 Store 实例的 _actions
    registerAction(store, namespacedType, action, local)
  })

  module.forEachGetter((getter, key) => {
    // 给每个 module._rawModule 下的每个 getter 方法添加命名空间
    const namespacedType = namespace + key
    // 把当前 module 中 getter 方法都放入当前 Store 实例的 _wrappedGetters
    registerGetter(store, namespacedType, getter, local)
  })
  //递归安装当前 module._children 中的 module
  module.forEachChild((child, key) => {
    // rootModule 安装,传入 module1 安装,然后在传入 module2
    // rootModule 的 path 是[],以后每个模块的名字都被 concat入path
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

这里比较重要的是** namespace,state,makeLocalContext(制作上下文),registerMutation,registerAction,registerGetter**。

(1) namespace:

namespace 在只在一种情况下有值,

当前 module 的 namespaced 传入 true,并且当前 module 不是 rootModule

未设置 namespaced 为 true 和 rootModule (因为 path 为[]),得到的 namespace 都为空。

(2) state:

state 一直是 options 中传入的 state,递归之后变成如下,自动由 moduleName 来做了命名空间。

按照逻辑热更新状态下,安装 module,是不会改变 state,先记下逻辑,后面会深入。

state

(3) makeLocalContext (制作上下文):

// 针对不同 module 制作局部的 dispatch, cimmit, getters 和 state方法
// 如果没有 namespace, 就用 root 上的方法
// path 暂未用到

function makeLocalContext (store, namespace, path) {
  // 未设置 namespaced 为 true 的时候,得到的 namespace 都为空
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      
      //如果传入 option ,或者里面有 root 这个属性,type 不做处理,不加命名空间
      if (!options || !options.root) {
        type = namespace + type
        if (!store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      
      //如果传入 option,或者里面有 root 这个属性,type 不做处理,不加命名空间
      if (!options || !options.root) {
        type = namespace + type
        if (!store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }
  
  // 在 local 添加 getters 和 state 两个属性,分别只定义了 get 方法
  // 只有被调用的时候才会触发 get 方法
    Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      //根据 path 来找出 state
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

定义了一个 local 本地化对象,在上面挂载 getters 和 state 属性,dispatch,commit 方法,本质就是根据 noNamespace 做判断,来找到相应的方法和属性。

makeLocalGettersgetNestedState 方法都很简单,不细说了。

这里有两点其实还是很模糊:

  • local 到底为我们做了什么?
  • gettersstate 里面的 store.getters 和 store.state 是什么时候绑定的。(目前为止还没有,绑定 getters 在 resetStoreVM 里,后面我们会看到。)

(4) registerMutation,registerAction,registerGetter:

 ...
 function installModule() { 
  ...
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  ...
} 
...
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
  	// mutation 能访问 state,还能携带参数
  	// 如果是 rootModule 的mutation 默认访问的就是 rootState
  	// 如果是 module1 就访问 module1 的 state,module2 也一样
    handler(local.state, payload)
  })
}

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
  	 // action 能访问下面6个属性
    let res = handler({
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    
    // 不是 promise 就返回 promise 也就是一开始为什么要断言 Promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    console.error(`[vuex] duplicate getter key: ${type}`)
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // getter 能访问下面4个属性
    return rawGetter(
      local.state, 
      local.getters, 
      store.state, 
      store.getters
    )
  }
}

installModule 全部完成之后,我们的 Store 实例发生了变化

当前Store实例

不难发现,由于 namespaced 为 false,我们无论是在 rootModule 还是在 module1 里面 dispatch fetchApi,都是会执行两次,fetchApi 里面的 commit 方法,也会触发两次,修改2个 module 的 state。

所以 module.context ( local 对象)就是为了 namespaced 而存在的。

module2实例

本文开头的问题1,便找到了答案。

action mutation getter 的回调函数里面的是怎么放入一个 Store 实例相关对象的(包含:dispatch,commit,state,rootState,getters,rooteGetters等)。

  • 在installModule阶段,首先制作了当前 module 的执行上下文,而上下文对象中包含了当前上下文的需要的属性**(state,getters,rootState,rootGetters)和 dispatch 和 commit 方法**。(其中要注意的是当 module 的 namespaced 为 true 时,dispatch 和 commit 触发的时候自动帮我们加上了 namespace ,这也就是我们能在 module2 中 commit('changeName') 更改的是 module2 中 state 的 name 的)

  • 在制作完上下文之后,便开始注册我们的 options 中的 action,mutation 和 getters,分别放入 Store 实例的 _actions,_mutations 和 _wrappedGetters 中,然后通过 local 对象和 Store 实例对象来组成传入工具对象,传入到我们定义的 action,mutation 和 getters 的回调函数中。

⑤ resetStoreVM

带着上面 getters 和 store.state 何时绑定的疑问,我们进入 resetStoreVM。

resetStoreVM方法就是初始化 store._vm观测 state 和 getters 的变化。

function resetStoreVM (store, state, hot) {
  // 初始化是没有设置过这个值的
  const oldVm = store._vm

  // 绑定 store 实例上的 getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // 全部放入 computed 对象
    computed[key] = () => fn(store)
    // 定义 store.getters 下每个 getter 的访问器属性
    Object.defineProperty(store.getters, key, {
      //每次 get 都会从 _vm 去寻找
      get: () => store._vm[key],
      enumerable: true
    })
  })

  // 用一个 Vue 实例来存储 state 树。
  // 有时全局的 mixins 会报错,先让 Vue 忽略。
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 创建storeVM
  store._vm = new Vue({
    data: {
      // state对象终于挂到了一个东西上
      $$state: state
    },
    // 用计算属性来计算 getters,所以可以通过 store._vm.xxx 访问(代理)
    computed
  })
  // 至此 this.state 才能被访问到
  Vue.config.silent = silent

  为新的 vm 开启严格模式。
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // 热更新的话就直接清空 oldVm 的 state
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 销毁老的 storeVM
    Vue.nextTick(() => oldVm.$destroy())
  }
}

问题3:

state 和 getter 是不是通过 new Vue 来使其变为响应式的。

  • 是的,每次在执行 resetStoreVM 的时候都会 new Vue() 来创建一个新的 store ._vm, 然后把老的 vm 销毁,并且我们看到,state 和 getters 都是挂载在新的 store._vm 上的。

get state & set state

class Store{
	...
	  get state () {
	  	 // resetStoreVM 后才能获取到
	    return this._vm._data.$$state
	  }
	
	  set state (v) {
	  	 // 修改 state 只能通过 store.replaceState()
	    assert(false, `Use store.replaceState() to explicit replace store state.`)
	  }
	...
}

问题4:

我们不能直接修改 state 是怎么做到的。

  • 当前我们设置 this.$store.state 的时候,会断言报错,并告诉我们只能通过 store.replaceState() 来修改 state,而 this.$store.state 其实并未访问到真实 state ,只是代理 store._vm 上的 state,加上前面我们说的标识 committing,我就可以知道怎么修改 state 成功而不报错:
changeState() {
    // _withCommit 修改 committing 标识。
    this._withCommit((state) => {
        this._vm._data.$$state = state
    })
}

而我们看下上面提到的 store.replaceState() 方法:

replaceState (state) {
	this._withCommit(() => {
  		this._vm._data.$$state = state
	})
}

和我们预想的一样,所以外部修改 state,就需要调用 store.replaceState() 来假装自己是内部的修改,直接修改的就是一个断言函数,什么都没有。

enableStrictMode

resetStoreVM 中还通过 strict 判断来执行 enableStrictMode 函数,我们来看一下:

function enableStrictMode (store) {
  // this 是 store._vm
  store._vm.$watch(function () { return this._data.$$state }, () => {
  	// _committing 在这里派上用场了
    assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
  }, { deep: true, sync: true })
}

store._vm.$watch开启了深监听,必然消耗性能的,所以线上建议关闭。

主要流程到这里就过完了,其他中还有些工具类方法和简单的方法,需要了解的再往下看,或者去源码中拜读就行。

读源码必然枯燥,但也是能学习尤大大代码的最短捷径,没有之一,无论是代码组织结构,功能模块解耦,函数职责单一,命名简单而又不简单,设计思路等都是我们非常直接借鉴的地方。

本文还有很多不足,有不对的地方望大家及时指出改正。

其他

动态注册 module 和注销 module。

Store类提供了两个方法:registerModuleunregisterModule

也就是我们的问题2

  registerModule (path, rawModule) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    this._modules.register(path, rawModule)
    // 把当前 module 安装到当前实例
    installModule(this, this.state, path, this._modules.get(path))
    // 重设当前 store._vm,其实就是删了重新建一个
    resetStoreVM(this, this.state)
  }

  unregisterModule (path) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    // 删除同理
    this._modules.unregister(path)
    // 这里已经从父级上删除了 state 了
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    resetStore(this)
  }

path 是数组,每次只取数组最后一位来做命名空间,register 方法其实是在指定父 module 上插入 _children :

  • 如果 path 是 ['module3'],则会创建一个和 rootModule 平级的 module。
  • 如果 path 是 ['module2', 'module3'],则会在 module2 的 _children 添加 module3。

this.state 是在 Store 实例化完成之后才可以访问的属性,所以也不难推断出,这两个方法是为 Store 实例服务的。

我们注意到在 unregisterModule 中,我们首先就从父级上删掉了我们要注销的 state,然后往下走到resetStore方法。

function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}

可以知道

  • 动态注销 module ,是要又从 rootModule 开始递归安装 module,很浪费性能。
  • installModule 中 hot 传了 true,就不会改变 state,说明下面这段逻辑不会执行。
function installModule (store, rootState, path, module, hot) { 
	... 
	// set state
  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)
    })
  }
	...
}

由于 hot 为 true,state 保持不变(之前已经被干掉了,也不需要变)。

subscribe

Store 实例提供了 subscribe 接口,作用是订阅 store 的 mutation。

  subscribe (fn) {
    const subs = this._subscribers
    if (subs.indexOf(fn) < 0) {
      subs.push(fn)
    }
    return () => {
      const i = subs.indexOf(fn)
      if (i > -1) {
        subs.splice(i, 1)
      }
    }
  }

接受的参数是一个回调函数,会把这个回调函数保存到 this._subscribers 上,并返回一个函数,当我们调用这个返回的函数,便可以解除当前函数对 store 的 mutation 变化的监听。

mapState

mapState 工具函数会将 Store 实例中的 state 映射到局部计算属性中。

使用示例:

import { mapState } from 'vuex'
export default {
  computed: mapState({
    nameGetter1: state => state.name,
    nameGetter2: 'name',
    nameGetter3 (state) {
      return state.name 
    },
  }),
  mapState(['name'])
}

mapState 函数可以接受一个对象,也可以接收一个数组,走进函数看一下:

export function mapState (states) {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      return typeof val === 'function'
        ? val.call(this, this.$store.state, this.$store.getters)
        : this.$store.state[val]
    }
  })
  return res
}

函数首先使用了normalizeMap来格式化传入的参数,进入这个函数看一下:

function normalizeMap (map) {
  return Array.isArray(map)
    // 传入的参数是数组
    // map = [{name: 'name', val: 'name'}]
    ? map.map(key => ({ key, val: key }))
    // 传入的参数是对象
    // map = [
    	//	{name: 'nameGetter1', val: state => state.name},
    	//	{name: 'nameGetter2', val: 'name'},
    	//	{name: 'nameGetter3', val(state) {return state.name }},]
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
  • 回到 mapState 函数,在调用了 normalizeMap 函数后,把传入的 states 转换成由 {key, val} 对象构成的数组,接着调用 forEach 方法遍历这个数组,构造一个新的对象,这个新对象每个元素都返回一个新的函数 mappedState,函数对 val 的类型判断,如果 val 是一个函数,则直接调用这个 val 函数,把当前 store 上的 state 和 getters 作为参数,返回值作为 mappedState 的返回值;否则直接把 this.$store.state[val] 作为 mappedState 的返回值。

  • 那么为何 mapState 函数的返回值是这样一个对象呢,因为 mapState 的作用是把全局的 state 和 getters 映射到当前组件的 computed 计算属性中,我们知道在 Vue 中 每个计算属性都是一个函数。 为了更加直观地说明,回到刚才的例子:

import { mapState } from 'vuex'
export default {
  computed: mapState({
    nameGetter1: state => state.name,
    nameGetter2: 'name',
    nameGetter3 (state) {
      return state.name 
    },
  }),
  mapState(['name'])
}

经过 mapState 函数调用后的结果,如下所示:

import { mapState } from 'vuex'
export default {
  // ...
  computed: {
    nameGetter1() {
      return this.$store.state.name
    },
    nameGetter2() {
      return this.$store.state['name']
    },
    nameGetter3() {
      return this.$store.state.name
    },
    name() {
      return this.$store.state.name
    }
  }
}

mapActionsmapGettersmapMutations 实现方式都差不多,这里就不多赘述,如果需要了解就去源码中查看。

PerseveranceZ avatar Jun 12 '18 02:06 PerseveranceZ