ZengTianShengZ / My-Blog

📓read notes

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Vuex 源码解析

ZengTianShengZ opened this issue · comments

Vuex 源码解析

本篇意在理清楚 Vuex 的工作原理,相应的会忽略一些技术细节,更多源码请移步到 github vux

1、目录结构

image

Vuex 的源码其实并不多,短小精悍,主要的工作都在 store.js 下完成

2、入口文件 index.js

分析源码先从入口文件入手,理清来龙去脉。

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

入口文件有整个状态管理的 Store 对象,安装 Vue.js 插件必须提供的 install 方法,版本号以及辅助函数 mapStatemapMutationsmapGettersmapActions

3、Vuex 插件的安装 install

Vuex 的使用要从插件的安装说起,安装 Vuex 插件做了哪些事情呢。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)  // 会调用  Vuex 提供的 install 方法

安装 Vuex 插件,插件提供的 install 方法会被调用,方法如下:

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

可看出插件主要执行了 applyMixin(Vue) ,如下:

// applyMixin

export default function (Vue) {
  Vue.mixin({ beforeCreate: vuexInit })
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

简要分析下, install 里面调用了 applyMixin 方法,applyMixin 方法里头执行了 Vue.mixin ,订阅了 beforeCreate 钩子,beforeCreate 则会在 Vue 初始化前调用执行。beforeCreate 对应执行了 vuexInit , 如下:

 this.$store = options.store
 this.$store = options.parent.$store

即为当前 Vue 实例(this)的 $store 属性挂上 store 实例。如果当前Vue 实例没有 store 实例则从父组件的 Vue 实例继承过来。

这样做是干嘛用的呢,是为了我们在任何一个 Vue 组件或子孙组件中都可以方便的使用

this.$store 

去对 store 做任何操作

4、 Store 对象初始化

了解了 store 如何挂载到 vue 实例,下面来看下 store 做了哪些操作

export class Store {
  constructor (options = {}) {
    const {
      plugins = [],
      strict = false
    } = options
    /****** _committing 严格模式下的标志位 ******/
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    /****** module收集器 ******/
    this._modules = new ModuleCollection(options)    
    this._modulesNamespaceMap = 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
    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)
    // apply plugins
    plugins.forEach(plugin => plugin(this))
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }

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

   set state (v) {
      if (process.env.NODE_ENV !== 'production') {
         assert(false, `use store.replaceState() to explicit replace store state.`)
       }
    }

    commit (_type, _payload, _options) {
        // ...
     }

     dispatch (_type, _payload) {
        // ...
      }
}

从 Store 的构造函数我们可以看到几个熟悉的属性和方法,state, _mutations, _actions, this.dispatch, this.commit ,我们先抛开其他细节,来简单的回顾下我们是怎么使用 Vuex 的:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

store.commit('increment')
console.log(store.state.count) // -> 1

我们往 Store 传入一个 option 对象(包含 state,mutations属性 ),接着我们可以使用 store 实例 进行 store.commit 更新 statestore.state 获取 statestore.commit('increment') 到底做了什么事呢,下面我们主要分析下 store.commit

5、store.commit

更多的技术细节我们需要通过分析 store.commit 到底做了哪些操作:

//   store.commit
  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) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {    // ②
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

commit 方法接收一些参数,比如我们例子中传入的 type ,根据 type 获取到可执行的数组 entry

const entry = this._mutations[type]    // ①

最后数组 entry_withCommit 的包裹下遍历执行

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

分析到这里那么问题就拓展到了 store.commit('increment') 是怎么根据 type: increment 获取到 entry 的,执行完 entry 又是怎么更新了 state 的。

5.1、 module 的收集

要分析上面的问题我们还需再重新回到上面提到的 Store 的构造方法,构造方法有这么一步操作

installModule(this, state, [], this._modules.root)

其中:

function installModule (store, rootState, path, module, hot) {
  const local = module.context = makeLocalContext(store, namespace, path)  
  // module forEachMutation 方法能找到自己对应的 mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    // 注册 mutation
    registerMutation(store, namespacedType, mutation, local) 
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    // 注册 action
    registerAction(store, type, handler, local) 
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    // 注册 getter
    registerGetter(store, namespacedType, getter, local)
  })
  
  // 递归注册  module.module.module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

可见 installModule 里头做了一些注册相关的初始化操作,如 registerMutation ,还有后续我们会提到的 registerActionregisterGetter

关键先来看下 registerMutation

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

到这里应该就比较清晰整个 store.commit('increment') 的流程了。

// store
store.commit('increment') 
==>
// commit
const entry = this._mutations[type]
==>
// installModule
registerMutation(store, namespacedType, mutation, local) 
==>
// registerMutation
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
  handler.call(store, local.state, payload)
})

store.commit('increment') 需要通过 type_mutations 拿到可执行的 entry 遍历执行,_mutations 是通过 registerMutation 注册初始化的,_mutations 根据 type 初始化为为数组 store._mutations[type] = [] , 再往数组中 push 进去 mutationmutation 其实就是最最开始 往 Store 传入的初始化 options 配置了

简单梳理下数据结构:

  // options 
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    },
  }

Store 实例对象

image

mutations 下的方法会被收集到 Store_mutations 对象上,根据对应的 type 构造出 entry 数组,当
store.commit('increment') 时就会根据 type 找到 _mutations 对象对应的 _mutations 数组,对应执行。

5.2、 更新 state

上面只分析了 store.commit('increment') 执行的过程,漏掉了最重要的一步,commit 完最后是怎么更新 state 的呢。

还记得我们有提到的 _withCommit 方法吗:

  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()  // 方法内部更新了 state
    this._committing = committing
  }

执行 _withCommit 方法会遍历执行 entry 方法。

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

最终是执行了这个 handler(payload)handler 对应的就是处理 options 对应的 mutation方法了,如例子中的

  mutations: {
    increment (state) {  // 处理为 handler 方法
      state.count++  // 更新了 state 
    }
  }

最终的 state 更新就是在 mutations 对应 type 函数执行了更新。

至此我们完成了 store.commit('increment') 执行过程的分析

6、 state 状态的获取

store.state.count  // -> 1

源码:

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

  set state (v) {
    if (process.env.NODE_ENV !== 'production') { // 禁止直接更新 state 状态
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

state 的获取也很简单,从 Store_vm 返回 _data 状态就是了,但禁止了直接更新 state 的状态,原因是稍后提到的 Vuex 的状态跟踪。

这里还有一个不明白的点是为什么 state 的获取是从 _vm 属性来的呢,原来 Vuex 初始化的时候有这么一步

resetStoreVM(store, state, hot)

其中:

function resetStoreVM (store, state, hot) {
  // ... 
  
  // Vuex依赖Vue核心实现数据的“响应式化”。
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // ...
}

Storestate 挂载到了一个 Vue 实例上,利用 Vue 的双向绑定的原理来实现 Store state 的更新从而触发视图的更新。

7、store.dispatch

dispatchcommit 的原理差不多,如果你通过上面理解了 commit 的过程,那分析 dispatch 的过程就轻松多了。

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 result.then(res => {
    try {
      this._actionSubscribers
        .filter(sub => sub.after)
        .forEach(sub => sub.after(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in after action subscribers: `)
        console.error(e)
      }
    }
    return res
  })
}

同样的,根据 type 获取到对应 _actions 属性下的 entry,不过和 commit 不同的是,这里获取到的 entry 放在了 Promise 下执行,这也就是 store.dispatch 触发的 action 能执行异步函数的原因。

  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

   return result.then(res => {})

store.dispatch 触发了 actionaction 更新 state 需要通过 commit ,具体过程就上面分析 Store.commit 的过程了,不在赘述。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

8、状态跟踪

Store 初始化时还有这么一段代码:

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
   devtoolPlugin(this)
}

其中:

const target = typeof window !== 'undefined'
  ? window
  : typeof global !== 'undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  store.subscribe((mutation, state) => { // 状态订阅
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}

devtoolPlugin 干嘛用的呢,它做了个状态的订阅,每当状态更新时会触发 store.subscribe ,从而做一次 vuex:mutationemit,配合控制台我们就能方便的跟踪到每次状态更新的数据情况了。

  store.subscribe((mutation, state) => { // 状态订阅
    devtoolHook.emit('vuex:mutation', mutation, state)
  })

image

利用控制台还可做 时间旅行,回退到任意时间节点的状态。

小结

我们从源码入手,分析了 Vuex 插件的安装过程,storecommit 过程,如何获取 storestate,以及 storeaction 过程,最后我们提了下 VuexVuex 配合控制台可方便做状态管理。中间隐藏了一些技术细节,想深入研究的同学可查看源码继续分析。