Vuex源码阅读(2):实现原理

如果直接从源码的入口文件开始解读,并不能很好的理解消化,因为不知道 Vuex 的核心实现原理,会使读者很茫然,并不知道某段代码是做什么的。所以在这里,我首先对 Vuex 的实现原理进行解读。

Vuex 实现的核心其实是利用了 Vue 实例的响应式特性。我们在代码中通过 new Vuex.Store({}) 生成 Store 实例。 这个 Store 实例会被从上而下(父级到子级)的赋值到每个 Vue 实例对象的 $store 属性上,每个 Vue 实例都可以通过 this.$store 拿到这个对象,具体如何实现赋值到每个 Vue 实例的 $store 属性上的, 这个会在下篇文章介绍。在这里,你只需要知道每个 Vue 实例的 $store 属性都指向这个 Store 实例,而这个 Store 实例中有一个 _vm 属性,它是一个 Vue 实例,这个 Vue 实例是理解 Vuex 的关键之处,接下来请以 _vm 为中心来阅读这篇文章。

Vuex源码阅读(2):实现原理_第1张图片

1,Vuex 对 state 的处理

先从 state 说起。

Vuex 对 state 的处理首先是对各个模块的 state 对象根据其模块的嵌套规则组合成相互嵌套的对象,例如有下面所示的配置对象:

{
  state: {
    name: 'main module'
  },
  mutations: {},
  getters: {}
  modules: {
    foo: {
      state: {
        name: 'foo module'
      },
      mutations: {},
      getters: {}
    },
    bar :{
      state: {
        name: 'bar module'
      },
      mutations: {},
      getters: {}
    }
  }
}
         ||
        转换成
         ||
         \/
{
  name: 'main module',
  foo: {
    name: 'foo module'
  },
  foo: {
    name: 'bar module'
  }
}

然后在创建 _vm 的时候进行如下的操作:

// store 是 Store 的实例对象
store._vm = new Vue({
  data: {
    // state 就是上面转换完成的 state 嵌套对象
    // 这样我们所编写的 state 就具有响应式的特性了 
    $$state: state
  },
  computed
})

我们平时从 Vuex 中获取 state 值是通过 this.$store.state.xxx 拿数据的,现在再来看看 $store 实例对象的 state 属性指向什么,相关代码如下:

class Store {
  get state () {
    return this._vm._data.$$state
  }
}

从上面的代码可知:$store.state 指向的就是 $store._vm.data.$$state,和上面创建 $store 中的 $$state 正好对应。

总结:Vuex 将我们编写的配置对象中的 state 转换成以 state 嵌套的对象,然后使用它作为 data.$$state 的值创建 Vue 实例, 然后我们在自己的业务代码中获取的 $store.state 也是从这个 Vue 实例中取的,这个 _vm(Vue实例) 成为了业务代码和创建 Store 实例中的 state 之间的桥梁, 将两者链接起来,并且其是具有响应式特性的。

2,对 getters 的理解

2-1,我们所写的 getter 函数被注册到哪里了?

这主要看 store.js 中的 registerGetter 函数。

function registerGetter (store, type, rawGetter, local) {
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

通过上面的代码可知,我们写的 getter 函数被包装了一层,并被放到了 store 的 _wrappedGetters 属性中。key 值是 type, 这个 type 是命名空间和 getter 函数名的组合,例如命名空间是 'foo/bar',getter 的函数名是 'getFullName', 那么 type 的值就是 'foo/bar/getFullName',这只是处理的第一步。

接下来看看这些函数是如何被放置到 _vm 中的,看下面的代码:

// store.js
function resetStoreVM (store, state, hot) {
  // 给 Store 实例设置 getters 对象
  store.getters = {}
  // 获取我们在 registerGetter 函数中设置的 _wrappedGetters,就像下面这个样子。
  // _wrappedGetters:
  //   evenOrOdd: ƒ wrappedGetter(store)
  //   fooGet: ƒ wrappedGetter(store)
  //   joo/jooGet: ƒ wrappedGetter(store)
  //   joo/op/getFullName: ƒ wrappedGetter(store)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 对 wrappedGetters 进行遍历
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    // 这一段很有意思,我们给 store.getters 设置属性,key 是 getter 的路径加上 getter 名称,例如:joo/op/c1Get。
    // 然后,设置的 get 从 _vm 中取值。这使得我们可以通过 this.$store.getters.xxx 取得 getter 值,并且是响应式的。
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
}

我们先从 store._wrappedGetters 拿到包含了 getters 的对象。对该对象进行遍历,遍历的回调函数做了两件事,一件事是将 getters 放置到 computed 对象中, 另一件事是将 getters 设置进 store.getters 中,这个 store.getters 就是我们在业务代码中使用 this.$store.getters 拿到的 getters, 这里比较关键的地方是其设置的 get 是 store._vm[key],也就是说我们在业务代码中使用 this.$store.getters.xxxx 拿的 getter 实际上是指向 _vm 中的 computed。 从下面的代码可知,遍历回调函数中被赋值的 computed 对象被用来实例化 _vm了,和上面的正好对应。

2-2,registerGetter 函数中,Vuex 给 rawGetter 函数指定的参数指向哪里?

先列出相关的代码:

function registerGetter (store, type, rawGetter, local) {
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

到这里,还有一个关键之处没有说明,Vuex 给 rawGetter 函数指定的参数指向哪里?
这里比较好理解的是 store.state 和 store.getters,从上面的解读可知,store.state 和 store.getters 分别指向 _vm 中的 data.$$state 和 _vm 中的 computed。

这里比较奇怪的是 local 是什么?先列出与其有关的代码:

// store.js/makeLocalContext
Object.defineProperties(local, {
  getters: {
    get: noNamespace
      // 在全局作用域中,
      ? () => store.getters
      : () => makeLocalGetters(store, namespace)
  },
  state: {
    // store.state 是响应式的,因为他是从 this._vm.data.$$state 中取值的
    get: () => getNestedState(store.state, path)
  }
})

// 获取指定路径的模块的 state
function getNestedState (state, path) {
  return path.reduce((state, key) => state[key], state)
}

其实 local.state 和 local.getters 最终还是指向 _vm 中的 data.$$state 和 _vm 中的 computed,只不过在这里进行了命名空间的处理,什么叫命名空间的处理? 我们以 local.state 为例,例如,我们假设有如下的 state 对象,这个对象会被放置到 _vm._data 中。

{
  name: 'main module',
  foo: {
    name: "foo module",
    tar: {
      name: "foo/tar module"  
    }
  },
  bar: {
    name: "bar module"
  }
}

另外假设 tar 模块有如下的 getter:

printName(state, getters, rootState, rootGetters) {
  rootState.name // main module
  state.name // foo/tar module
}

从上面的输出可知,getter 的 state 其实指向的是 this.$store._vm._data.$$state.foo.tar,而 rootState 指向的是 this.$store._vm._data.$$state, 所谓的命名空间的处理就是将 local.state 指向 this.$store._vm._data.$$state 的更深层,指向该 getter 所在模块的 state(这句话是精髓),具体怎么实现的看 getNestedState 函数。 例如在本例中,printName 所在的模块是 foo/tar,它的 state 就指向 this.$store._vm._data.$$state.foo.tar。

3,对 mutation 的理解

3-1,我们所写的 mutation 函数被注册到哪里了?

看下面的代码:

function registerMutation (store, type, handler, local) {
  // 首先判断store._mutations是否存在指定的 type,如果不存在的话给空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 向 store._mutations[type] 数组中添加包装后的 mutation 函数
  entry.push(function wrappedMutationHandler (payload) {
    // 包一层,commit 函数调用执行 wrappedMutationHandler 时只需要传入payload
    // 执行时让this指向store,参数为当前module上下文的state和用户额外添加的payload
    handler.call(store, local.state, payload)
  })
}

从上面的代码可知,mutation 函数被包了一层,然后被 push 到了 store._mutations[type] 数组中,这个 type 是命名空间加上 mutation 函数名。

3-2,commit 函数是如何触发 mutation 函数的?

相关代码如下:

commit (_type, _payload, _options) {
  // 获取 type 所对应的 mutation 函数数组
  const entry = this._mutations[type]
  
  // 遍历执行添加到 entry 数组中的包装函数
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
}

commit 函数的实现很简单,使用 type 从 this._mutations 中获取指定的函数数组,然后再遍历执行函数数组中的函数

3-3,mutation 函数的 state 参数指向什么?

我们在平时一般会这样书写 mutation 函数:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

那么 increment 函数的 state 参数指向的是什么呢?

在这里,需要说明一点,虽然这个 mutation 函数是我们自己编写的,但是该函数的执行却是 Vuex 内部的操作,所以说这个 state 指向什么, 要看 Vuex 在执行这个函数的时候注入的是什么,看下面的代码。

function registerMutation (store, type, handler, local) {
  // 首先判断store._mutations是否存在指定的 type,如果不存在的话给空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 向 store._mutations[type] 数组中添加包装后的 mutation 函数
  entry.push(function wrappedMutationHandler (payload) {
    // 包一层,commit 函数调用执行 wrappedMutationHandler 时只需要传入payload
    // 执行时让this指向store,参数为当前module上下文的state和用户额外添加的payload
    handler.call(store, local.state, payload)
  })
}

这个 handler 就是我们所写的 mutation 函数,Vuex 在执行这个函数的时候使用的是 local.state,这个 local.state 在上面介绍过, 其实它指向 this.$store._vm._data.$$state 或者更深层的 state 对象。也就是说,我们编写的 state.count++,实际上是执行 this.$store._vm._data.$$state.count++,最终操作的对象还是 _vm 中的数据。

通过上面一系列的介绍,我们知道,当我们通过 this.$store.state.xxx 和 this.$store.getters.xxx 获取数据的时候,实际上是从 _vm 中获取数据, 因为 _vm 是 Vue 实例,所以我们拿到的数据都是响应式的。然后当我们通过 commit 改变数据的时候,其实改变的是 _vm._data.$$state 中的数据。 当 _vm._data.$$state 中数据改变的时候,我们使用了其中数据的页面也会响应式的更新。至此,就完成了 Vuex 中央状态管理仓库的功能。

4,对 Action 的理解

Action 比较简单,和上面的理解思路一样,只不过多了一些 Promise 的处理。

4-1,我们所写的 action 函数被注册到哪里了?

相关代码和注释如下:

function registerAction (store, type, handler, local) {
  // 首先判断 store._actions 是否存在指定的 type,如果不存在的话给空数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // 和 registerMutation 一样,向 store._actions 中 push 包装过的函数
  entry.push(function wrappedActionHandler (payload) {
    // 这里对应 Vuex 官网的:https://vuex.vuejs.org/zh/guide/modules.html 中的 "在带命名空间的模块内访问全局内容(Global Assets)"部分
    // action 函数的第一个参数是包含多个属性的对象,具体实现如下所示:
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,

      getters: local.getters,
      state: local.state,

      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 判断 action 函数的返回值是不是 promise,如果不是的话,将其包装成 resolved 状态的 promise,确保其返回值是 promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
  })
}

4-2,dispatch 函数是如何触发执行 action 函数的?

相关代码和注释如下:

dispatch (_type, _payload) {
    // 获取相应 type 的 action 函数数组
    const entry = this._actions[type]
    
    const result = entry.length > 1
      // 如果 action 函数有多个的话,使用 Promise.all 进行处理
      ? Promise.all(entry.map(handler => handler(payload)))
      // 只有一个的话,直接执行就行了,由于在 registerAction 函数中,注册的每个函数的返回值都进行了 promise 的包装,函数的返回值一定是 promise
      : entry[0](payload)

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

4-3,action 函数的参数指向的是什么?

相关代码和注释如下:

function registerAction (store, type, handler, local) {
  // 首先判断 store._actions 是否存在指定的 type,如果不存在的话给空数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // 和 registerMutation 一样,向 store._actions 中 push 包装过的函数
  entry.push(function wrappedActionHandler (payload) {
    // 这里对应 Vuex 官网的:https://vuex.vuejs.org/zh/guide/modules.html 中的 "在带命名空间的模块内访问全局内容(Global Assets)"部分
    // action 函数的第一个参数是包含多个属性的对象,具体实现如下所示:
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,

      getters: local.getters,
      state: local.state,

      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 判断 action 函数的返回值是不是 promise,如果不是的话,将其包装成 resolved 状态的 promise,确保其返回值是 promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
  })
}

handler 就是我们写的 action 函数,我们可以看到,参数还是蛮多的,不过好几个已经在前面讲过了,这里简要讲下 local.dispatch 和 local.commit,相关代码如下:

const local = {
  dispatch: noNamespace ?
  // 如果是全局命名空间的话,直接使用 store 中的 dispatch 函数
  store.dispatch :
  (_type, _payload, _options) => {
    const args = unifyObjectStyle(_type, _payload, _options)
    // 获取载荷形式的 type、payload 和 options
    const { payload, options } = args
    let { type } = args
    // 如果 options 没有传递或者 options.root 为 false 的话,
    // 说明这个 action 需要 dispatch 某一具体的命名空间(而不是全局命名空间)
    if (!options || !options.root) {
      // 往 type 前面拼接上命名空间
      type = namespace + type
      if (__DEV__ && !store._actions[type]) {
        // 如果没有这个 action 的话,输出报错信息
        console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
        return
      }
    }
    // 使用 store 中的 dispatch 函数进行最后的处理。
    return store.dispatch(type, payload)
  },

  // 和 dispatch 思路一样。
  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)
  }
}

其实 local.dispatch 和 local.commit 最终还是指向 store.dispatch 和 store.commit。只不过在命名空间存在的时候,进行了一些额外的处理, 处理的主要操作就是 type = namespace + type。为什么要进行这样的处理呢?这样处理有什么作用呢?这里以 commit 为例,假设有如下的 mutation:

let store = new Vuex.Store({
  state,
  getters,
  actions,
  mutations,

  modules: {
    foo: {
      namespaced: true,
      modules: {
        bar: {
          namespaced: true,
          state: {
            count: 0,
            mutations: {
              increment(state) {
                state.count++
              }
            },
            actions: {
              incrementAction ({dispatch, commit, getters, state, rootGetters, rootState}) {
                // 看这里
                commit('increment')
              }
            }
          }
        }
      }
    }
  }
})

这个 increment mutation 函数所在的命名空间是 "foo/bar/",该函数保存在 store._mutations['foo/bar/increment'] 数组中。 当你执行 commit('increment') 的时候,其内部会执行 type = "foo/bar/" + "increment"; store.commit(type); store.commit() 函数就会执行 store._mutations['foo/bar/increment'] 数组中的函数。如果 Vuex 不进行 type = namespace + type 的处理的话,你必须在 23 行执行 commit("foo/bar/increment"),这非常的不方便,在库的设计上也不合理。 也许你已经感觉到了,通过执行 type = namespace + type,使 Vuex 有了一种 "局部 mutations" 的概念,就是当你 commit 当前模块的 mutation 函数的时候, 你直接 commit 这个 mutation 的函数名就可以了,不用管这个模块的命名空间,这种库的设计非常的好,很值得我们借鉴和学习。

你可能感兴趣的:(vuex源码阅读系列,javascript,vue,源码,前端)