为了账号安全,请及时绑定邮箱和手机立即绑定

Vuex 2.0 源码分析(中)

大家好,我叫黄轶,来自滴滴公共前端团队,最近在幕课网上线了一门 Vue.js 的实战课程——《Vue.js高仿饿了么外卖App 2016最火前端框架》,同时,我们团队最近写了一本书 ——《Vue.js 权威指南》,内容丰富,由浅入深。不过有一些同学反馈说缺少 Vuex 的介绍的章节。既然 Vue.js 2.0 已经正式发布了,我们也要紧跟步伐,和大家聊一聊 Vuex 2.0。本文并不打算讲官网已有的内容,而会通过源码分析的方式,让同学们从另外一个角度认识和理解 Vuex 2.0。

wrapGetters

了解完 registerAction 后,我们来看看 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
      )
    }
  })
}

wrapGetters 是对 store 的 getters 初始化,它接受 3个 参数, store 表示当前 Store 实例,moduleGetters 表示当前模块下的所有 getters, modulePath 对应模块的路径。细心的同学会发现,和刚才的 registerMutation 以及 registerAction 不同,这里对 getters 的循环遍历是放在了函数体内,并且 getters 和它们的一个区别是不允许 getter 的 key 有重复。

这个函数做的事情就是遍历 moduleGetters,把每一个 getter 包装成一个方法,添加到 store._wrappedGetters 对象中,注意 getter 的 key 是不允许重复的。在这个包装的方法里,会执行 getter 的回调函数,并把当前模块的 state,store 的 getters 和 store 的 rootState 作为它参数。来看一个例子:

export const cartProducts = state => {
  return state.cart.added.map(({ id, quantity }) => {
    const product = state.products.all.find(p => p.id === id)
    return {
      title: product.title,
      price: product.price,
      quantity
    }
  })
}

这里我们定义了一个 getter,通过刚才的 wrapGetters 方法,我们把这个 getter 添加到 store._wrappedGetters 对象里,这和回调函数的参数 state 对应的就是当前模块的 state,接下来我们从源码的角度分析这个函数是如何被调用,参数是如何传递的。

我们有必要知道 getter 的回调函数的调用时机,在 Vuex 中,我们知道当我们在组件中通过 this.$store.getters.xxxgetters 可以访问到对应的 getter 的回调函数,那么我们需要把对应 getter 的包装函数的执行结果绑定到 `this.$store 上。这部分的逻辑就在 resetStoreVM 函数里。我们在 Store 的构造函数中,在执行完 installModule 方法后,就会执行 resetStoreVM 方法。来看一下它的定义:

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

这个方法主要是重置一个私有的 _vm 对象,它是一个 Vue 的实例。这个 _vm 对象会保留我们的 state 树,以及用计算属性的方式存储了 store 的 getters。来具体看看它的实现过程。我们把这个函数拆成几个部分来分析:

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

这部分留了现有的 store._vm 对象,接着遍历 store._wrappedGetters 对象,在遍历过程中,依次拿到每个 getter 的包装函数,并把这个包装函数执行的结果用 computed 临时变量保存。接着用 es5 的 Object.defineProperty 方法为 store.getters 定义了 get 方法,也就是当我们在组件中调用this.$store.getters.xxxgetters 这个方法的时候,会访问 store._vm[xxxgetters]。我们接着往下看:

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

这部分的代码首先先拿全局 Vue.config.silent 的配置,然后临时把这个配置设成 true,接着实例化一个 Vue 的实例,把 store 的状态树 state 作为 data 传入,把我们刚才的临时变量 computed 作为计算属性传入。然后再把之前的 silent 配置重置。设置 silent 为 true 的目的是为了取消这个 _vm 的所有日志和警告。把 computed 对象作为 _vm 的 computed 属性,这样就完成了 getters 的注册。因为当我们在组件中访问 this.$store.getters.xxxgetters 的时候,就相当于访问 store._vm[xxxgetters],也就是在访问 computed[xxxgetters],这样就访问到了 xxxgetters 对应的回调函数了。这段代码最后判断 strict 属性决定是否开启严格模式,我们来看看严格模式都干了什么:

function enableStrictMode (store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
  }, { deep: true, sync: true })
}

严格模式做的事情很简单,监测 store._vm.state 的变化,看看 state 的变化是否通过执行 mutation 的回调函数改变,如果是外部直接修改 state,那么 store._committing 的值为 false,这样就抛出一条错误。再次强调一下,Vuex 中对 state 的修改只能在 mutation 的回调函数里。

回到 resetStoreVM 函数,我们来看一下最后一部分:

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

这里的逻辑很简单,由于这个函数每次都会创建新的 Vue 实例并赋值到 store._vm 上,那么旧的 _vm 对象的状态设置为 null,并调用 $destroy 方法销毁这个旧的 _vm 对象。

那么到这里,Vuex 的初始化基本告一段落了,初始化核心就是 installModule 和
resetStoreVM 函数。通过对 mutations 、actions 和 getters 的注册,我们了解到 state 的是按模块划分的,按模块的嵌套形成一颗状态树。而 actions、mutations 和 getters 的全局的,其中 actions 和 mutations 的 key 允许重复,但 getters 的 key 是不允许重复的。官方推荐我们给这些全局的对象在定义的时候加一个名称空间来避免命名冲突。
从源码的角度介绍完 Vuex 的初始化的玩法,我们再从 Vuex 提供的 API 方向来分析其中的源码,看看这些 API 是如何实现的。

Vuex API 分析

Vuex 常见的 API 如 dispatch、commit 、subscribe 我们前面已经介绍过了,这里就不再赘述了,下面介绍的一些 Store 的 API,虽然不常用,但是了解一下也不错。

watch(getter, cb, options)

watch 作用是响应式的监测一个 getter 方法的返回值,当值改变时调用回调。getter 接收 store 的 state 作为唯一参数。来看一下它的实现:

watch (getter, cb, options) {
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
    return this._watcherVM.$watch(() => getter(this.state), cb, options)
  }

函数首先断言 watch 的 getter 必须是一个方法,接着利用了内部一个 Vue 的实例对象 `this._watcherVM 的 $watch 方法,观测 getter 方法返回值的变化,如果有变化则调用 cb 函数,回调函数的参数为新值和旧值。watch 方法返回的是一个方法,调用它则取消观测。

registerModule(path, module)

registerModule 的作用是注册一个动态模块,有的时候当我们异步加载一些业务的时候,可以通过这个 API 接口去动态注册模块,来看一下它的实现:

registerModule (path, module) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    this._runtimeModules[path.join('.')] = module
    installModule(this, this.state, path, module)
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }

函数首先对 path 判断,如果 path 是一个 string 则把 path 转换成一个 Array。接着把 module 对象缓存到 this._runtimeModules 这个对象里,path 用点连接作为该对象的 key。接着和初始化 Store 的逻辑一样,调用 installModule 和 resetStoreVm 方法安装一遍动态注入的 module。

unregisterModule(path)

和 registerModule 方法相对的就是 unregisterModule 方法,它的作用是注销一个动态模块,来看一下它的实现:

unregisterModule (path) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    delete this._runtimeModules[path.join('.')]
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    resetStore(this)
  }

函数首先还是对 path 的类型做了判断,这部分逻辑和注册是一样的。接着从 this._runtimeModules 里删掉以 path 点连接的 key 对应的模块。接着通过 this._withCommit 方法把当前模块的 state 对象从父 state 上删除。最后调用 resetStore(this) 方法,来看一下这个方法的定义:

function resetStore (store) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  const state = store.state
  // init root module
  installModule(store, state, [], store._options, true)
  // init all runtime modules
  Object.keys(store._runtimeModules).forEach(key => {
    installModule(store, state, key.split('.'), store._runtimeModules[key], true)
  })
  // reset vm
  resetStoreVM(store, state)
}

这个方法作用就是重置 store 对象,重置 store 的 _actions、_mutations、_wrappedGetters 等等属性。然后再次调用 installModules 去重新安装一遍 Module 对应的这些属性,注意这里我们的最后一个参数 hot 为true,表示它是一次热更新。这样在 installModule 这个方法体类,如下这段逻辑就不会执行

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

由于 hot 始终为 true,这里我们就不会重新对状态树做设置,我们的 state 保持不变。因为我们已经明确的删除了对应 path 下的 state 了,要做的事情只不过就是重新注册一遍 muations、actions 以及 getters。

回调 resetStore 方法,接下来遍历 this._runtimeModules 模块,重新安装所有剩余的 runtime Moudles。最后还是调用 resetStoreVM 方法去重置 Store 的 _vm 对象。

hotUpdate(newOptions)

hotUpdate 的作用是热加载新的 action 和 mutation。 来看一下它的实现:

hotUpdate (newOptions) {
  updateModule(this._options, newOptions)
  resetStore(this)
}

函数首先调用 updateModule 方法去更新状态,其中当前 Store 的 opition 配置和要更新的 newOptions 会作为参数。来看一下这个函数的实现:

function updateModule (targetModule, newModule) {
  if (newModule.actions) {
    targetModule.actions = newModule.actions
  }
  if (newModule.mutations) {
    targetModule.mutations = newModule.mutations
  }
  if (newModule.getters) {
    targetModule.getters = newModule.getters
  }
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!(targetModule.modules && targetModule.modules[key])) {
        console.warn(
          `[vuex] trying to add a new module '${key}' on hot reloading, ` +
          'manual reload is needed'
        )
        return
      }
      updateModule(targetModule.modules[key], newModule.modules[key])
    }
  }
}

首先我们对 newOptions 对象的 actions、mutations 以及 getters 做了判断,如果有这些属性的话则替换 targetModule(当前 Store 的 options)对应的属性。最后判断如果 newOptions 包含 modules 这个 key,则遍历这个 modules 对象,如果 modules 对应的 key 不在之前的 modules 中,则报一条警告,因为这是添加一个新的 module ,需要手动重新加载。如果 key 在之前的 modules,则递归调用 updateModule,热更新子模块。

调用完 updateModule 后,回到 hotUpdate 函数,接着调用 resetStore 方法重新设置 store,刚刚我们已经介绍过了。

replaceState

replaceState的作用是替换整个 rootState,一般在用于调试,来看一下它的实现:

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

函数非常简单,就是调用 this._withCommit 方法修改 Store 的 rootState,之所以提供这个 API 是由于在我们是不能在 muations 的回调函数外部去改变 state。

到此为止,API 部分介绍完了,其实整个 Vuex 源码下的 src/index.js 文件里的代码基本都过了一遍。

点击查看更多内容
9人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
1.8万
获赞与收藏
3713

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消