Vuex源码浅析

308 阅读4分钟

个人阅读vuex的一些笔记、文章的最后会抛弃 vuex 一些繁琐的检查,撸一个简易版的 vuex。

初始化

vuex 的初始化是以插件的形式导入vue。使用Vue.use(plugin)导入插件,需要插件提供一个 install 的方法。 Vue.use导入插件

从install 方法开始

// mixin.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

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

我们只看version > 2的逻辑,这里为什么可以使用this.$store拿到 store 对象?就是由于执行了vuexInit方法,这里有一个判断options.parent && options.parent.$store,这里用到了声明周期的技巧,如果不执行beforeCreate 这个逻辑 this.$store 仅仅在根的Vue有,但是由于 Vue 的生命周期 beforeCreate 是从父到子,就可以把 store 一层一层往下传。

初始化

首先我们不涉及modules。

export class Store {
  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    // note: new Store必须在Vue.use(Vuex)之前
    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }

    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._wrappedGetters = Object.create(null)
    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))
  }

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

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

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

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

  subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }

  subscribeAction (fn) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers)
  }

  watch (getter, cb, options) {
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }

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

new Vuex.Store 做了什么?首先Vue.use 要先于 new Vuex.Store否则没办法拿 到Vue 对后面的数据进行 observable ,因为Vuex 就是一个小型的Vue所以你能对state进行computed watch。 new Vue.Store首先初始化了store 的dispatch commit 最主要的它执行了 resetStoreVM 对state的数据进行observable, 对getters的数据进行computed。

// 精简后的代码
function resetStoreVM (store, state) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // note: 初始化getters
  /*
    note:
    比如我们传入的getters: {
        name(state) { return 123 }
    }
    我们需要做一层柯里化,因为Vue默认的computed 下面的是没有state这个参数的。
    所以需要把getters里面的函数包裹一层, 相当于执行以下:
    const old = getters.name;
    getters.name = ((innerStore) => {
        return old(store);
    })(store)
  */
  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 enviroment.
    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
  const silent = Vue.config.silent
  Vue.config.silent = true
  // note: getters 的本质就是计算属性
  // note: 访问state 的本质就是访问store._vm.$$state
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }
}

dispatch 与 commit。

dispatch 与 commit 的区别在于,dispatch是异步调用。

commit (_type, _payload, _options) {
    // check object-style commit
    // 
    // note: 检查判断
    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
    }
    // note: 这里会做一层正在committing的封装以区分是否是通过commit,改变state的值
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // note: 监听器hook的调用
    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'
      )
    }
  }

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

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    // note: 订阅actions的before的调用
    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

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

这里commit, dispatch只是把需要的参数封装进去,并执行一些订阅。这里有一个问题:在严格模式中 Vuex 是如何区别 commit 改变 state,还是外部手动改变 state ?

这里在执行commit的时候有一个 _withCommit 实际上这个就是做一层flag的切换:

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

这里如果对 state 改变的时候 _committing 的值设为 true。在上述的resetStoreVM 会执行 enableStrictMode:

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

这里会执行一个 watch,因为设置的sync,所以一旦 state 执行了 setter 这个函数就会被立马调用,调用的时候检查一下 _committing 如果是false就代表着 本次 setter 的触发不是使用commit,就抛出警告。

subscribe

subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
}

subscribeAction (fn) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers)
}
function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

这里分别把对 commit, dispatch 的监听器放到 _subscribers, _actionSubscribers。除此之外还做了一层校验防止重复订阅 genericSubscribe,这里会返回一个函数这个函数如果重复执行一次的话就能取消该订阅。

一个简易的 Vuex

var Biux = (function() {
    let Vue
    const install = function(vue) {
      if (!Vue) {
        Vue = vue;
      }
      Vue.mixin({
        beforeCreate() {
          const options = this.$options
          if (options.store) {
            // 根Vue
            this.$store = options.store
          }
          if (!this.$store && this.$parent) {
            this.$store = this.$parent.$store
          }
        }
      })
    }
    
    function resetStoreVM(store, options) {
      const { getters = {} } = options
      store.getters = {}
      // 对getters的值进行computed
      const formatGetters = Object.keys(getters).reduce((collection, key) => {
        collection[key] = function() {
          return getters[key](store.state)
        }
        // 代理到store getters
        Object.defineProperty(store.getters, key, {
          get() {
            return store._vm[key]
          }
        })
        return collection
      }, {})
      store._vm = new Vue({
        data() {
          return {
            $$state: options.state || {}
          }
        },
        computed: formatGetters
      })
      // 监听state并对非commit改变State抛错
      store._vm.$watch(function() {
        return this._data.$$state
      }, {
        handler() {
          if (!store._committing) {
            console.error('[biux error] please change state by commit')
          }
        },
        deep: true,
        sync: true
      })
    }
    
    class Store {
      constructor(options) {
        const { state = {} } = options
        this._subscribes = []
        this._actionSubscribers = []
        // commit标识符
        this._committing = false
        this._actions = options.actions || {}
        this._mutations = options.mutations || {}
        resetStoreVM(this, options)
        // bind this
        const { commit, dispatch } = this
        this.commit = commit.bind(this)
        this.dispatch = dispatch.bind(this)
      }
    
      get state() {
        return this._vm._data.$$state;
      }
    
      commit(type, payload) {
        const mutation = this._mutations[type];
        if (mutation) {
          this.withCommit(() => mutation(this.state, payload))
          this._subscribes.forEach((fn) => {
            typeof fn === 'function' && fn({ type, payload }, this.state)
          })
        } else {
          console.warn(`[biux error] mutation ${type} not declare`);
        }
      }
    
      dispatch(type, payload) {
        const action = this._actions[type]
        if (action) {
          // 执行before订阅
          this._actionSubscribers.forEach(({ before }) => {
            before({ type, payload }, this.state)
          })
          Promise.all([action(this, payload)]).then(() => {
            // 执行after订阅
            this._actionSubscribers.forEach(({ after }) => {
              after && after({ type, payload }, this.state)
            })
          }).catch((e) => {
            console.warn(e.message);
          })
        } else {
          console.warn(`[biux error] action ${type} not declare`);
        }
      }
    
      // 订阅commit
      subscribe(fn) {
        this._subscribes.push(fn)
      }
    
      // 订阅action
      subscribeAction(fn) {
        if (typeof fn === 'function') {
          fn = { before: fn }
        }
        this._actionSubscribers.push(fn)
      }
    
      // 正在commit的flag
      withCommit(fn) {
        this._committing = true;
        console.log(this._committing)
        fn()
        this._committing = false;
      }
    }
    return {
        install,
        Store
    }
})();

一个简单的counter view