vuex源码分析(五)

734 阅读3分钟

五、Api解析(一)

假设我们的结构是这样的

const modulesA = {
  namespaced: true,
  state: {
    count: 1
  },
  getters: {
    computedConut(state) {
      return state.count + 1
    }
  },
  mutations: {
    add(state,num) {
      state.count += num
    }
  },
  actions: {
    addCount(context) {
      context.commit('add', 2)
    }
  }
}
const store = new Vuex.Store({
  modules: {
    modulesA
  }
})

1.state

假如我们想访问modulesA中state的属性count,官方文档给出的api是使用this.$store.state.modulesA.count。当访问到state的时候会触发Store实例的state的get函数,他会返回this._vm._data.$$state,之前提到过,store._vm在执行resetStoreVM时,会通过new Vue进行初始化,data中的$$state对应的是定义的state。state是resetStoreVM接受的第二个参数,也就是this._modules.root.state。在最初this._modules.root.state只代表根部module的state,并没有形成一个嵌套的结构,那么state tree的形成是在子模块执行installModule函数的时候,会通过 Vue.set(parentState, moduleName, module.state),往root.state中,以模块的key作为key,模块的state作为value进行添加。也就是说最终的$$state: state对应的state是一个树状结构的state,这样当我们就可以通过state.modulesA.count拿到modulesA模块中的count。如果我们对state进行直接修改,比如this.$store.state.moudlesA.count = 4,那么并不会成功的修改state,因为state的set函数他并不会去做相应的state的修改,而是会在开发模式下报出一个警告。

// src/store.js
export class Store {
  constructor (options = {}) {
    ...
    this._modules = new ModuleCollection(options)
    ...
    const state = this._modules.root.state

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

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)
    ...
  }
}
// src/store.js
function installModule (store, rootState, path, module, hot) {
  ...
  // set state
  if (!isRoot && !hot) {
    // 拿到父级的state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 拿到模块名称
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      ...
      // 去设置当前的state到父级state中
      Vue.set(parentState, moduleName, module.state)
    })
  }
  ...
}

2.getters

假如我们想访问modulesA的getters中的computedConut,官方文档给出的api是使用this.$store.getters['modulesA/computedConut']。getter的初始化同样是在resetStoreVM函数中,首先会定义一个空的computed对象,然后遍历store._wrappedGetters(在执行installModule函数的时候会执行registerGetter函数,在store._wrappedGetters挂载我们定义的getter函数),把getters函数作为值,getter函数的key作为computed的key,注册在computed,在遍历的时候还会通过Object.defineProperty定义了在访问store.getters.xxx的访问,最终会访问到store._vm[key],也就是会访问到store的vm实例上,为什么这么做,是因为接着,他会把computed作为vm实例的computed,这样通过访问this.$store.getters.xxx就会被代理到了store._vm[key]也就是我们经过vue实例computed处理过的具体的getters函数。

// src/store.js
function resetStoreVM (store, state, hot) {
  ...
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  ...
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  ...
}

3.commit

假如我们想调用modulesA的mutations中的add函数,来修改我们的state,那么需要执行this.$store.commit('modulesA/add')。store中的commit函数首先会通过unifyObjectStyle函数解析我们传入的三个参数,也就是文档中支持两种传入方式

  • commit(type: string, payload?: any, options?: Object)
  • commit(mutation: Object, options?: Object)

最终拿到三个参数,第一个是type也就是我们需要调用的mutations的名称,第二个参数是,payload,也就是我们需要传入所调用mutations的参数,第三个参数是options(在actions中提交commit可以传入root:true,让context访问到根部module)。接着commit函数会通过this._mutations[type](在registerMutation函数中通过)拿到对应的mutations函数,然后在_withCommit函数的包裹下,遍历执行(因为mutations在注册中会注册为一个数组),并把payload作为参数传入。为什么用_withCommit函数进行包裹,_withCommit函数帮我们做了这样一件事他首先把全局的_committing置为了true,在执行完其中的函数,在把他置为false,在函数resetStoreVM的执行中,如果传的strict是true,则会执行enableStrictMode函数,enableStrictMode函数的目的是通过vm的 $watch方法对this._data.$$state进行了监听,当修改的时候,如果他判断_committing为false,则会报错。也就是说如果通过mutations进行了state的修改,那么是不会报错的,如果我们擅自进行了修改,则会报错。传入root为true可以调用到根部commit的原因是在installModule函数中执行makeLocalContext函数时,他定义了namespaced的module下的commit会接受第三个参数,如果第三个参数(options)中有root为true,那么在调用commit的时候,传入的type就是原始定义的type,而不是和namespace拼接之后的type。

// src/store.js
  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) {
      ...
    }
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    ...
  }
// src/store.js
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
// src/store.js
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

4.dispatch

假如我们想调用modulesA的actions中的addCount函数,来提交提个mutation函数,那么需要执行this.$store.dispatch('modulesA/addCount')。store中的dispatch函数和commit函数相似,首先通过unifyObjectStyle对传入的参数进行解析,拿到type和payload参数。同样也会去this._actions[type]中拿到对应的actions中注册的函数。和commit不同的是,他并不会直接执行,而是会先判断判断拿到的_actions[type]的length,如果是1则会执行,如果不是1,则会执行 Promise.all(entry.map(handler => handler(payload))),这是因为actions在注册的时候会通过registerAction函数进行注册,registerAction函数中会判断传入的actions是否是一个promise如果不是promise,则会通过res = Promise.resolve(res),把他变成一个promise,dispatch函数最终会返回一个promise,其中的reslove的执行时机,正是执行entrypromise的then的时候。也就是说,我们调用dispatch最终会返回一个promise,这个promise触发then的时机,是对应的所有actions执行完的时候。其实dispatch是一个异步的函数,他可以接受一些异步的方法,最终提交mutation来修改state,dispatch很好的帮助我们规范了提交mutations的方式。

// src/store.js
 dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    ...
    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)
      })
    })
  }
// src/store.js
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)
    }
    if (store._devtoolHook) {
      ...
    } else {
      return res
    }
  })
}