Vuex源码阅读小笔记(一)

890 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

这篇文章是从源码角度分析vuex的核心概念,记录下自己花一周时间看vuex源码后的笔记,这里我看的vuex源码版本是3.6.0,后面有时间后会看vuex4的源码,既然是看核心概念,就跟着vuex的官方文档一起来看 vuex3官方文档地址v3.vuex.vuejs.org/zh/

前期准备

在看源码之前应该要使用过vuex或者对vuex有过了解,这样在看源码的过程中才能更加明白,如果还没有了解的话或者忘记了的话可以跟着文档再学习一下,这里我是通过vuex源码中example文件夹中的例子以及文档再去了解了vuex,而通过源码中的例子学习还有一个好处就是方便调试理解代码,veux主要源码再src文件夹中,核心文件便是store.js

State

我们可以在vuex中以下面两种方式定义一个state

const state = {
  count: 0,
}

const state = () => ({
  items: [],
})

获取到这个state,我们可以this.$store.state,而且我们知道veux中状态的存储是响应式的,并且state状态的改变只能通过提交mutation,我们拆解源码来分析下

export class Store {
  constructor(options = {}) {
    this.committing = false
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)
    resetStoreVM(this, state)
  }
}

在Store类中,this.committing的作用是表示提交的状态,也就是说如果是通过提交mutations来改变状态的话,它的值为true,state改变完后它的值为false,this._modules是根据传入的options,注册各个模块,这里我们在Module那细讲,state变量也就是将根模块的state赋值给了state变量,installModule就是注册完善各个模块内的信息,resetStoreVM也就是生成一个Vue实例去管理state

installModule对state的处理

function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length

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

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

function getNestedState(state, path) {
  return path.reduce((state, key) => state[key], state)
}

函数中先判断是否是根模块,如果不是根模块,通过getNestedState获取到当前模块的父模块中的state,并用Vue.set响应式地设置到父模块的state上

resetStoreVM

function resetStoreVM(store, state, hot) {
  const oldVm = store._vm
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

resetStoreVM函数响应式地初始化store._vm,并且如果存在旧的实例就销毁,并且state是只读的,我们可以看Store中的get和set

get state() {
    return this._vm.data.$$state
  }

set state(v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,并且mutations必须是同步函数, 提交mutations的方式可以是以下几种

store.commit('increment')
store.commit('increment', 10)
store.commit('increment', {
  amount: 10
})
store.commit({
  type: 'increment',
  amount: 10
})
this._mutations = Object.create(null)
    const store = this
    const { dispatch, commit } = this
    this.commit = function boundCommit(type, payload, options) {
      return commit.call(store, type, payload, options)
    }

在Store类里,this._mutations的作用是存放注册的mutation,commit方法也用call绑定到store实例上

commit(_type, _payload, _options) {
    const { type, payload, options } = unifyObjectStyle(_type, _payload, _options)
    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }

    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })
  }
}

function unifyObjectStyle(type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (__DEV__) {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}

unifyObjectStyle函数用于处理两种不同风格的提交方式,去this._mutations找对应的方法,有对应的方法便forEach遍历执行

mutations注册函数

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

registerMutation参数中出现了local,这个local是什么呢?这个local就是设置当前模块的上下文context,后面再往下看,这个方法的调用是在installModule

const local = module.context = makeLocalContext(store, namespace, path)

Actions

Actions类似于 mutation,不同的是Action 提交的是 mutation,而不是直接变更状态,可以包含任意异步操作并且内部是返回一个Promise用以处理异步流程

Actions是通过dispatch分发

store.dispatch('increment')
store.dispatch('incrementAsync', {
  amount: 10
})
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
this._actions = Object.create(null)
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch(type, payload) {
      return dispatch.call(store, type, payload)
    }

this._actions存放注册的actions函数,跟commit方法一样,dispatch方法也进行了绑定

dispatch(_type, _payload) {
    const { type, payload } = unifyObjectStyle(_type, _payload)
    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return new Promise((resolve, reject) => {
      result.then(res => {
        resolve(res)
      }, error => {
        reject(error)
      })
    })
  }

dispatch方法内部与commit方法不一样的就是使用了Promise.all去处理异步,一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。并且还返回一个Promise对象,这也就是我们可以在外部使用then进行链式操作和使用async await的原因了

actions注册函数

function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler(payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    return res
  })
}

registerAction函数里,注册的actions方法接收两个参数,一个是context,包括(dispatch,commit,state,getters),一个是payload,对函数返回的结果判断是否是Promise,如果不是用Promise.resolve包装

Modules

Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

Module

export default class Module {
  constructor(rawModule, runtime) {
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  addChild(key, module) {
    this._children[key] = module
  }
  getChild(key) {
    return this._children[key]
  }
  forEachChild(fn) {
    forEachValue(this._children, fn)
  }
  forEachGetter(fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }
  forEachAction(fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }
  forEachMutation(fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
 }
  }
}

Module类里都比较好理解,这里没什么可说的,主要是添加子模块,获取子模块,以及遍历当前模块下的mutationactiongetter执行一些回调操作

ModuleCollection

export default class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule, false)
  }
  get(path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  getNamespace(path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }
  register(path, rawModule, runtime = true) {
    const newModule = new Module(rawModule, runtime)
    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)
      })
    }
  }
}

ModuleCollection类是递归注册模块,如果rawModule还有子模块,则递归调用register注册子模块,get方法是获取父模块,父模块在调用addChild添加子模块,getNamespace为获取当前模块命名空间,ModuleModuleCollection看完后我们可以继续去看installModule函数了,在之前我们知道了installModulestate的处理,下面接着看installModule还做了什么

再回到installModule

function installModule(store, rootState, path, module, hot) {
  const namespace = store._modules.getNamespace(path)
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }
  const local = module.context = makeLocalContext(store, namespace, path)
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

分析代码,先是取得当前模块的namespace,如果当前模块设置了namespaced,则在modulesNamespaceMap中存储一下当前模块,接着就是注册模块下所有的mutationactiongetter,子模块,这里需要注意的是action,可以在带命名空间的模块注册全局action

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

于是才有了源码中对typehandler的处理

makeLocalContext

function makeLocalContext(store, namespace, path) {
  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

      if (!options || !options.root) {
        type = namespace + type
        if (__DEV__ && !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

      if (!options || !options.root) {
        type = namespace + type
        if (__DEV__ && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }
      store.commit(type, payload, options)
    }
  }
  
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

makeLocalContext根据是否设置命名空间返回一个本地的dispatchcommitgetters,没有设置命名空间则返回全局的。if (!options || !options.root)判断是否传入第三个参数{root:true},如果为true,则也是调用全局上的方法,最后在用Object.definePropertieslocal进行代理

Getters

getter类似于计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

this._wrappedGetters = Object.create(null)
this._makeLocalGettersCache = Object.create(null)

this._wrappedGetters用来存放注册的gettersthis._makeLocalGettersCache是本地的getters缓存

getters注册函数

function registerGetter(store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter(store) {
    return rawGetter(
      local.state,
      local.getters,
      store.state,
      store.getters
    )
  }
}

再回到resetStoreVM

看看resetStoreVM是怎么实现getters类似于计算属性的效果的

function resetStoreVM(store, state, hot) {
  store.getters = {}
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true
    })
  });
}

store上定义一个getters对象,遍历getters,将每一个getter注册到store.getters,访问对应getter时会去vm上访问对应的computed

最后

附上一张简单的图对store做个总结,这次对vuex源码的阅读在花了五天时间,完整地阅读下来后会发现其实并不难,耐心下来看代码,看官方的例子再结合调试基本上都会弄明白的,写文章也是一件费时间的事,还有一些像辅助函数,vuex的注册,插件等,等周末有空了在写上一篇,Ending......

Store.png