Warcry mpi
  1. 1 Warcry mpi
  2. 2 Time Bomb Veela
  3. 3 Hypocrite Nush
  4. 4 Quiet Storm Lyn
  5. 5 Life Will Change Lyn
  6. 6 かかってこいよ NakamuraEmi
  7. 7 One Last You Jen Bird
  8. 8 The Night We Stood Lyn
  9. 9 Flower Of Life 发热巫女
  10. 10 Libertus Chen-U
  11. 11 Last Surprise Lyn
2018-05-05 15:54:39

Vuex源码解析——结构初始化

和Redux相似,在Vue中也有一套状态管理模式。相比于Redux,其充分利用了Vue数据劫持的响应式机制特点进行状态更新,显得更加简洁和高效,但相对应的也舍弃了Redux的通用性。下面就从源码来梳理Vuex的初始化流程。

store注入

通过Vuex获取的state都是从store实例中读取的,由此导致的问题就是每个需要使用state的组件中都要频繁导入。

Vuex通过在根组件中使用store选项注入到每一个子组件中,使用如下:

Vue.use(Vuex);

const app = new Vue({
    el: '#app',
      store
})

子组件就能通过this.$store访问到了。那么这个过程是如何实现的呢?

其实看到Vue.use就应该很熟悉了,这是Vue使用插件的一个通用方法。也就是说Vuex可以看做一个插件,注入方法也遵守Vue第三方插件的通用安装规则。先看Vuex的入口文件index.js。

export default {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}

这个文件就是 Vuex对外暴露的API。其中的install方法就是我们的注入方法了。

function install (_Vue) {
  if (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)
}

这段代码主要做了两件事。

  1. 防止Vuex被重复安装
  2. 通过applyMixin方法初始化Vuex

再进入applyMixin看其实现:

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

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

可以看到主要是针对Vue版本进行不同的mixin处理。

如果是1.X版本,则把vuexInit方法混入到init阶段中。如果是2.X版本则混入到beforeCreate阶段中。

vuexInit就是完成Vue实例注入store的核心方法了。

store实例化

初始化内部变量

现在有了注入store的机制,当然store实例对象也不是随便传入的。必须依照Vuex的store构造函数传入拥有state,getters等等规定属性的对象去构造。下面就来看store构造函数做的事。

先看其实现源码:

class Store {
  constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

    const {
      state = {},
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._options = options
    this._committing = false
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // 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

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], options)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
  ...
}  

首先做两个断言达到两个目的:

  1. 确保Vuex的安装
  2. 确保Promise可用,因为Vuex依赖了Promise

然后创建了一些内部需要的属性:

  • _options: 存储参数
  • _committing: 提交标志,保证Vuex对state的修改只能在mutation的回调中
  • _actions:存储actions
  • _mutations:存储mutations
  • _wrappedGetters:存储getters
  • _runtimeModules:存储运行时modules
  • _subscribers:存储mutation的订阅者
  • _watcherVM:一个Vue实例,用来利用Vue的$watch机制观测变化

接下来更改dispatch和commit方法的this为当前store实例。

再判断是否为严格模式,严格模式下只允许使用mutation更新state。

初始化module

单一状态树的一个问题就是当应用规模扩大时,store对象会大到难以管理。module就是为了解决这个问题,其将单一状态树从上到下切分到各个模块管理。 installModule方法就是该功能的初始化。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const {
    state,
    actions,
    mutations,
    getters,
    modules
  } = module

  // 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, state || {})
    })
  }

  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }

  if (actions) {
    Object.keys(actions).forEach(key => {
      registerAction(store, key, actions[key], path)
    })
  }

  if (getters) {
    wrapGetters(store, getters, path)
  }

  if (modules) {
    Object.keys(modules).forEach(key => {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}

该方法接受如下5个参数:

  • store: 当前Store实例
  • rootState:根state
  • path: 当前嵌套模块的路径数组
  • module:当前安装的模块的options
  • hot: 热更新标志

module的state初始化

首先根据path数组长度判断是否为根,然后拿到options内的相关参数。

然后分别对mutations,actions,getters进行注册,再遍历modules递归调用installModule继续安装嵌套模块。

当我们不设置子模块时,完成以上的注册其实modules初始化就结束了。但当我们设置子模块时,需要重点关注以上的这段代码:

 if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
       })
 }

首先会调用getNestedState方法根据path查找state上的子state相对于根state的路径,然后把子state添加到根state中。这里我们通过_withCommit函数去修改当前模块的state,Vuex中对state的修改都会用该函数包装保证状态的正确性。

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

到此就完成了所有模块的state嵌套初始化。

registerMutation

function registerMutation (store, type, handler, path = []) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(getNestedState(store.state, path), payload)
  })
}

该函数接受4个参数:

  • store:当前Store实例
  • type:mutation的key
  • handler:mutation的回调
  • path:当前模块路径

其将注册的mutations回调包装一层后,挂载到store的_mutations内部属性上。

该包装函数的作用就是接受定义mutation时传入的payload参数,并将当前模块的state和payload一起作为回调的参数。

registerAction

function registerAction (store, type, handler, path = []) {
const entry = store._actions[type] || (store._actions[type] = [])
  const { dispatch, commit } = store
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler({
      dispatch,
      commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      rootState: store.state
    }, payload, cb)
    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
    }
  })
 }

action的注册和mutation大致相同,关键区别在于mutation是同步修改state,而action可以实现异步的修改。但有一点需要注意,Vuex中mutation是修改state的唯一途径,action异步修改的本质是修改提交mutation的时机。

其接受参数为context对象和payload,cb并没有被用到。context对象包括dispatch,commit,getters,模块state和根state。

既然包含了dispatch,就说明action内组合action调用也是允许的。

最后对返回值做一层Promise包装以及开发工具钩子检测。

wrapGetters

function wrapGetters (store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(getterKey => {
    const rawGetter = moduleGetters[getterKey]
    if (store._wrappedGetters[getterKey]) {
      console.error(`[vuex] duplicate getter key: ${getterKey}`)
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter (store) {
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}

getters的初始化即是遍历所有getters并挂载到store的_wrappedGetters属性中,并将其包装一层传入当前模块state,其他getter和根State作为参数。

至此module的初始化就完成了。

resetStoreVM

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

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })

  // 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
  store._vm = new Vue({
    data: { state },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation.
    store._withCommit(() => {
      oldVm.state = null
    })
    Vue.nextTick(() => oldVm.$destroy())
  }
}

我们虽然初始化了各种参数,但Vuex还有一个关键功能在于视图的响应式更新。这部分的实现原理和Vue的响应式原理相同,关键在于如何实现两者的桥接。也就是这个方法做的事了。

首先保留现有的_vm对象。这个对象是个Vue实例,用来实现Vuex数据和组件数据的中转。

接着遍历_wrappedGetters对象拿到所有包装函数并将其执行结果用computed变量保存,再通过Object.defineProperty方法使我们访问$store.getters时访问vm对象上的getters,即完成了中转的入口实现。

再将store的state作为data和computed变量作为Vue对象的属性实例化并挂载到store的_vm属性上,即完成了中转的出口实现。至此核心部分就完成了。

接下来就是判断是否开启严格模式,如果是严格模式则检测_vm.state的变化是否来自mutation,依据为内部commiting属性的值。

最后即是清理每次调用该函数时都会创建的旧vm对象了。

小结

以上就是Vuex的结构初始化全部核心内容。关键有两点:

  1. installModule上对mutations,actions,getters注册和模块的嵌套注册。
  2. resetStoreVM上的响应式数据中转。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「Vue」 标签。