zoukankan      html  css  js  c++  java
  • vuex源码简析

    前言


    基于 vuex 3.1.2 按如下流程进行分析:

    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      actions,
      getters,
      state,
      mutations,
      modules
      // ...
    });
                
    new Vue({store});
    

    Vue.use(Vuex)


    Vue.use() 会执行插件的 install 方法,并把插件放入缓存数组中。

    而 Vuex 的 install 方法很简单,保证只执行一次,以及使用 applyMixin 初始化。

    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)
    }
    

    applyMixin 方法定义在 vuex/src/mixin.js,vuex 还兼容了 vue 1 的版本,这里只关注 vue 2 的处理。

    export default function (Vue) {
      const version = Number(Vue.version.split('.')[0])
    
      if (version >= 2) {
        // 混入一个 beforeCreate 方法
        Vue.mixin({ beforeCreate: vuexInit })
      } else {
        // 这里是 vue 1 的兼容处理
      }
    
      // 这里的逻辑就是将 options.store 保存在 vue 组件的 this.$store 中,
      // options.store 就是 new Vue({...}) 时传入的 store,
      // 也就是 new Vuex.Store({...}) 生成的实例。
      function vuexInit () {
        const options = this.$options
        if (options.store) {
          this.$store = typeof options.store === 'function'
            ? options.store()
            : options.store
        } else if (options.parent && options.parent.$store) {
          this.$store = options.parent.$store
        }
      }
    }
    

    new Vuex.Store({...})


    先看 Store 的构造函数:

    constructor (options = {}) {
      const { plugins = [], strict = false } = options
    
      this._committing = false
      this._actions = Object.create(null)
      this._actionSubscribers = []
      this._mutations = Object.create(null)
      this._wrappedGetters = Object.create(null)
      // 构建 modules
      this._modules = new ModuleCollection(options)
      this._modulesNamespaceMap = Object.create(null)
      this._subscribers = []
      // store.watch
      this._watcherVM = new Vue()
      this._makeLocalGettersCache = Object.create(null)
    
      // bind commit and dispatch to self
      const store = this
      const { dispatch, commit } = this
      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)
      }
    
      // strict mode
      this.strict = strict
    	// 安装模块
      const state = this._modules.root.state
      installModule(this, state, [], this._modules.root)
    
      // 实现状态的响应式
      resetStoreVM(this, state)
    
      // apply plugins
      plugins.forEach(plugin => plugin(this))
    
      const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
      if (useDevtools) {
        devtoolPlugin(this)
      }
    }
    

    this._modules

    vuex 为了让结构清晰,允许我们将 store 分割成模块,每个模块拥有自己的 statemutationactiongetter,而且模块自身也可以拥有子模块。

    const moduleA = {...}
    const moduleB = {...}
    
    const store = new Vuex.Store({
      modules: {
        a: moduleA,
        b: moduleB
      }
    })
    
    store.state.a // -> moduleA 的状态
    store.state.b // -> moduleB 的状态
    

    而模块在 vuex 的构造函数中通过new ModuleCollection(options)生成,依然只看构造函数:

    // vuex/src/module/module-collection.js
    export default class ModuleCollection {
      constructor (rawRootModule) {
        // register root module (Vuex.Store options)
        this.register([], rawRootModule, false)
      }
    
      register (path, rawModule, runtime = true) {
        // 生成一个 module
        const newModule = new Module(rawModule, runtime)
        // new Vuex.Store() 生成的是根模块
        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)
          })
        }
      }
    }
    

    register 接收 3 个参数,其中 path 表示路径,即模块树的路径,rawModule 表示传入的模块的配置,runtime 表示是否是一个运行时创建的模块。

    register首先 new Module() 生成一个模块。

    // vuex/src/module/module.js
    export default class Module {
      constructor (rawModule, runtime) {
        this.runtime = runtime
        // 子模块
        this._children = Object.create(null)
        // module 原始配置
        this._rawModule = rawModule
        const rawState = rawModule.state
        // state
        this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
      }
    }
    

    实例化一个 module 后,判断当前的 path 的长度,如果为 0,就是是一个根模块,所以把 newModule 赋值给 this.root,而 new Vuex.Store() 生成的就是根模块。

    如果不为 0,就建立父子模块的关系:

    const parent = this.get(path.slice(0, -1))
    parent.addChild(path[path.length - 1], newModule)
    

    首先根据路径获取父模块,然后再调用父模块的 addChild 方法将子模块加入到 this._children 中,以此建立父子关系。

    register 最后会检测是否有嵌套的模块,然后进行注册嵌套的模块:

    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
    

    遍历当前模块定义中的所有 modules,根据 key 作为 path,递归调用 register 方法进行注册。

    installModule

    注册完模块就会开始安装模块:

    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)
    

    installModule 方法支持 5 个参数: store,state,path(模块路径),module(根模块),hot(是否热更新)。

    默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的,这样使得多个模块能够对同一 mutation 或 action 作出响应。但是如果有同名的 mutation 被提交,会触发所有同名的 mutation。

    因此 vuex 提供了 namespaced: true 让模块成为带命名空间的模块,当模块被注册后,它的所有 action、mutation 和 getter 都会自动根据模块注册的路径调整命名。例如:

    const store = new Vuex.Store({
      modules: {
        account: {
          namespaced: true,
          getters: {
            isAdmin () { ... } // -> getters['account/isAdmin']
          },
          // 进一步嵌套命名空间
          posts: {
            namespaced: true,
            getters: {
              popular () { ... } // -> getters['account/posts/popular']
            }
          }
        }
      }
    })
    

    回到 installModule 方法本身:

    function installModule (store, rootState, path, module, hot) {
      // 是否为根模块
      const isRoot = !path.length
      // 获取命名空间,
      // 比如说 { a: moduleA } => 'a/',
      // root 没有 namespace
      const namespace = store._modules.getNamespace(path)
    
      // 将有 namespace 的 module 缓存起来
      if (module.namespaced) {
        store._modulesNamespaceMap[namespace] = module
      }
    
      if (!isRoot && !hot) {
        // 给 store.state 添加属性,
        // 假如有 modules: { a: moduleA },
        // 则 state: { a: moduleA.state }
        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 函数,
      // 举个例子,modules: { a: moduleA },
      // moduleA 中有名为 increment 的 mutation,
      // 通过 makeLocalContext 函数,会将 increment 变成
      // a/increment,这样就可以在 moduleA 中找到定义的函数
      const local = module.context = makeLocalContext(store, namespace, path)
    
      // 下面这几个遍历循环函数,
      // 都是在注册模块中的 mutation、action 等等,
      // moduleA 中有名为 increment 的 mutation,
      // 那么会将 namespace + key 拼接起来,
      // 变为 'a/increment'
      module.forEachMutation((mutation, key) => {
        const namespacedType = namespace + key
        // 会用 this._mutations 将每个 mutation 存储起来,
        // 因为允许同一 namespacedType 对应多个方法,
        // 所以同一 namespacedType 是用数组存储的,
        // store._mutations[type] = []
        registerMutation(store, namespacedType, mutation, local)
      })
    
      module.forEachAction((action, key) => {
        const type = action.root ? key : namespace + key
        const handler = action.handler || action
        // 和上面一样,用 this._actions 将 action 存储起来
        registerAction(store, type, handler, local)
      })
    
      module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key
        // 会用 this._wrappedGetters 存储起来
        // getters 有一点不一样,
        // 这里保存的是一个返回 getters 的函数,
        // 而且同一 namespacedType 只能定义一个。
        registerGetter(store, namespacedType, getter, local)
      })
    
      // 递归安装子模块
      module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child, hot)
      })
    }
    
    

    makeLocalContext

    function makeLocalContext (store, namespace, path) {
      // 判断是否有 namespace
      const noNamespace = namespace === ''
    
      const local = {
        // 重写 dispatch
        // 为什么要重写,
        // 举个例子 modules: { a: moduleA }
        // 在 moduleA 的 action 中使用 dispatch 时,
        // 并不会传入完整的 path,
        // 只有在 vue 实例里调用 store.dispatch 才会传入完整路径
        dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options)
          const { payload, options } = args
          let { type } = args
    
          if (!options || !options.root) {
            type = namespace + type
          }
    
          return store.dispatch(type, payload)
        },
    
        // 重写 commit 方法
        // 同上
        commit: noNamespace ? store.commit : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options)
          const { payload, options } = args
          let { type } = args
    
          if (!options || !options.root) {
            type = namespace + type
          }
    
          store.commit(type, payload, options)
        }
      }
    
      Object.defineProperties(local, {
        getters: {
          get: noNamespace
            ? () => store.getters
          	// local.getters 本质上是通过匹配 namespace,
          	// 从 store.getters[type] 中获取
            : () => makeLocalGetters(store, namespace)
        },
        state: {
          // local.state 本质上是通过解析 path,
          // 从 store.state 中获取
          get: () => getNestedState(store.state, path)
        }
      })
    
      return local
    }
    

    resetStoreVM(this, state)

    初始化 store._vm,利用 Vue 将 store.state 进行响应式处理,并且将 getters 当作 Vue 的计算属性来进行处理:

    function resetStoreVM (store, state, hot) {
      const oldVm = store._vm
    
      store.getters = {}
      // reset local getters cache
      store._makeLocalGettersCache = Object.create(null)
      const wrappedGetters = store._wrappedGetters
      const computed = {}
      // 遍历 store._wrappedGetters 属性
      forEachValue(wrappedGetters, (fn, key) => {
        // 这里的 partial 其实就是创建一个闭包环境,
        // 保存 fn, store 两个变量,并赋值给 computed
        computed[key] = partial(fn, store)
        // 重写 get 方法,
        // store.getters.key 其实是访问了 store._vm[key],
        // 也就是去访问 computed 中的属性
        Object.defineProperty(store.getters, key, {
          get: () => store._vm[key],
          enumerable: true // for local getters
        })
      })
    
      // 实例化一个 Vue 实例 store._vm,
      // 用它来保存 state,computed(getters),
      // 也就是利用 Vue 的进行响应式处理
      const silent = Vue.config.silent
      Vue.config.silent = true
      // 访问 store.state 时,
      // 其实是访问了 store._vm._data.$$state
      store._vm = new Vue({
        data: {
          $$state: state
        },
        // 这里其实就是上面的 getters
        computed
      })
      Vue.config.silent = silent
    
      // 开启 strict mode,
      // 只能通过 commit 的方式改变 state
      if (store.strict) {
        enableStrictMode(store)
      }
    
      if (oldVm) {
        if (hot) {
          // 热重载
          store._withCommit(() => {
            oldVm._data.$$state = null
          })
        }
        // 这里其实是动态注册模块,
        // 将新的模块内容加入后生成了新的 store._vm,
        // 然后将旧的销毁掉
        Vue.nextTick(() => oldVm.$destroy())
      }
    }
    

    partial

    export function partial (fn, arg) {
      return function () {
        return fn(arg)
      }
    }
    

    常用 api


    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]
      if (!entry) {
        // 没有 mutation 会报错并退出
        return
      }
      this._withCommit(() => {
        // 允许同一 type 下,有多个方法,
        // 所以循环数组执行 mutation,
        // 实际上执行的就是安装模块时注册的 mutation
        // handler.call(store, local.state, payload)
        entry.forEach(function commitIterator (handler) {
          handler(payload)
        })
      })
    
      // 触发订阅了 mutation 的所有函数
      this._subscribers
        .slice()
        .forEach(sub => sub(mutation, this.state))
    }
    

    dispatch

    dispatch (_type, _payload) {
        // check object-style dispatch
        const {
          type,
          payload
        } = unifyObjectStyle(_type, _payload)
    
        const action = { type, payload }
        // 从 Store._actions 获取
        const entry = this._actions[type]
        if (!entry) {
    			// 找不到会报错
          return
        }
    
      	// 在 action 执行之前,
      	// 触发监听了 action 的函数
        try {
          this._actionSubscribers
            .slice() 
            .filter(sub => sub.before)
            .forEach(sub => sub.before(action, this.state))
        } catch (e) {
            console.error(e)
        }
    
      	// action 用 Promise.all 异步执行,
      	// 实际上执行的就是安装模块时注册的 action
      	/* 
      	handler.call(store, {dispatch, commit, getters, state, rootGetters, rootState}, payload, cb) 		
      	*/
        const result = entry.length > 1
          ? Promise.all(entry.map(handler => handler(payload)))
          : entry[0](payload)
    
        return result.then(res => {
          // 在 action 执行之后,
      		// 触发监听了 action 的函数
          try {
            this._actionSubscribers
              .filter(sub => sub.after)
              .forEach(sub => sub.after(action, this.state))
          } catch (e) {
              console.error(e)
          }
          return res
        })
      }
    

    watch

    watch (getter, cb, options) {
      // new Vuex.Store() 时创建 _watcherVM,
      // this._watcherVM = new Vue(),
      // 本质就是调用 vue 的 api
      return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
    }
    

    registerModule

    registerModule (path, rawModule, options = {}) {
      if (typeof path === 'string') path = [path]
    	
      // 注册模块
      this._modules.register(path, rawModule)
      // 安装模块
      installModule(this, this.state, path, this._modules.get(path), options.preserveState)
      // 重新生成 vue 实例挂载到 store 上,
      // 然后销毁旧的
      resetStoreVM(this, this.state)
    }
    

    备注


    希望疫情尽快过去吧。——2020/02/08 元宵

  • 相关阅读:
    ArcSDE 10.1安装、配置、连接 (SQL Server 2008)
    ASP.NET MVC 音乐商店
    ASP.NET MVC 音乐商店
    一些Web开发工具
    Apache 2 频率限制模块
    零起步高性能PHP框架 Phalcon 实战
    零配置Ubuntu下Wordpress安装
    零起步的Hadoop实践日记(hbase in action)
    零起步的Hadoop实践日记(hive in action)
    零起步的Hadoop实践日记(内存设置调整)
  • 原文地址:https://www.cnblogs.com/guolao/p/12284393.html
Copyright © 2011-2022 走看看