阅读完vuex源码我学到了什么

353 阅读6分钟

大家好,我是百慕大三角。最近阅读了vuex源码,在这里分享一下收获吧。这是本人第一次独立阅读源码,有什么理解错误的地方,希望大家能谅解并指正出来。十分感谢!

正确的阅读姿势

可能有些朋友要说了,阅读源码能有什么正确的姿势?不就是把github上的开源项目 clone 到本地去阅读、调试吗?我在尝试去阅读vuex源码前也是这样天真认为的(= 。=)

但其实在阅读源码方面还是有很多技巧的,以下这些都能很好的帮助到我们:

  1. git log --reserse先看初始提交
  2. git tag检测出重要版本
  3. 善用 git worktree
  4. 关注注释中的TODOFIXME
  5. 学会使用debug

那么我们可以在 github 上看到 vuex 中有好几个分支,那么我们就可以先 clone 0.3.0、0.4.0、1.0 分支的源码去阅读,去理解最核心的思想。tips:我们最好 fork 项目到自己的仓库,方便在源码上写注释做笔记。

image.png

1.0 分支的 vuex 到底是如何实现的?

在 clone 上述的几个分支后,发现这几个分支的差异并不是很大,这里就看下1.0分支的 vuex 是如何实现的。我们可以看到这个版本的 vuex 目录结构非常简单:

|-- src
|    |-- plugins
|        |-- devtools.js
|        |-- logger.js  //
|    |-- index.js
|    |-- override.js
|    |-- utils.js  // 一些通用的方法
|-- package.json
|-- ...

我们可以看到用 index.js和 override.js 就实现了vuex的核心功能, inedx.js导出 Store 类和供 Vue.use() 使用的install 方法,override.js 实现了通过 this.$store 访问 state

override.js

我们先从比较简单的override.js文件看起:

// index.js 中导出的 install 方法
function install (_Vue) {
  if (Vue) {
    console.warn(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  override(Vue)
}

/**
 * override.js
 * 在 Vue2.0 以上的版本中主要是通过 Vue.mixin 实现在 beforeCreate 生命周期中执行 vuexInit 方法
 * 在 Vue1.0 版本中用的是 _init ,由于本人没用过vue1.0版本,在这里也就不赘述了
 */
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })

function vuexInit () {
    const options = this.$options
    const { store, vuex } = options
    // store injection
    if (store) {
      this.$store = store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
    ...
}

我们可以看到 install 方法对 vuex 多次使用的检测。

然后获取options的 store, 如果有 store 就将赋值给 this.$store ,也就是挂载到根组件实例上。 如果 options 中没有 store 且父组件有 store 属性,就将父组件的 store 也挂载在子组件的 $store 上,实现了 state 的向下流动。

vuexInit 后面的代码就不赘述了,主要目的是支持以下代码中的写法,这种写法在vue2.0版本中已经被摒除了

export default {
  vuex: {
    getters: {
      count: state => state.count
    },
    actions: actions
  }
}

index.js

那么我们回到index.js 文件中,去看 Store 类内部是如何实现的:

class Store {
    constructor ({
    state = {},
    mutations = {},
    modules = {},
    plugins = [],
    strict = false
  } = {}) {
      this._rootMutations = this._mutations = mutations
      this._modules = modules
      const dispatch = this.dispatch
      // bind dispatch to self
      this.dispatch = (...args) => {
          dispatch.apply(this, args)
      }
      this._vm = new Vue({
          data: {
              state
          }
      })
      this._setupModuleState(state, modules)
      this._setupModuleMutations(modules)
      get state () {
          return this._vm.state
      }
      set state (v) {
          throw new Error('[vuex] Use store.replaceState() to explicit replace store state.')
      }
  }
}
dispatch (type, ...payload) {
    let silent = false
    let isObjectStyleDispatch = false
    // 兼容使用对象操作的参数
    if (typeof type === 'object' && type.type && arguments.length === 1) {
      isObjectStyleDispatch = true
      payload = type
      if (type.silent) silent = true
      type = type.type
    }

    if (typeof type !== 'string') {
      throw new Error(
        `[vuex] Expects string as the type, but found ${typeof type}.`
      )
    }

    const handler = this._mutations[type]
    const state = this.state
    if (handler) {
      this._dispatching = true
      // apply the mutation
      if (Array.isArray(handler)) {
        handler.forEach(h => {
          isObjectStyleDispatch
            ? h(state, payload)
            : h(state, ...payload)
        })
      } else {
        isObjectStyleDispatch
          ? handler(state, payload)
          : handler(state, ...payload)
      }
      this._dispatching = false
      if (!silent) {
        const mutation = isObjectStyleDispatch
          ? payload
          : { type, payload }
        this._subscribers.forEach(sub => sub(mutation, state))
      }
    } else {
      console.warn(`[vuex] Unknown mutation: ${type}`)
    }
  }
  
    /**
   * Attach sub state tree of each module to the root tree.
   *
   * @param {Object} state
   * @param {Object} modules
   */

  _setupModuleState (state, modules) {
    if (!isObject(modules)) return

    Object.keys(modules).forEach(key => {
      const module = modules[key]

      // set this module's state
      Vue.set(state, key, module.state || {})

      // retrieve nested modules
      this._setupModuleState(state[key], module.modules)
    })
  }
  
    /**
   * Bind mutations for each module to its sub tree and
   * merge them all into one final mutations map.
   *
   * @param {Object} updatedModules
   */

  _setupModuleMutations (updatedModules) {
    const modules = this._modules
    Object.keys(updatedModules).forEach(key => {
      modules[key] = updatedModules[key]
    })
    const updatedMutations = this._createModuleMutations(modules, [])
    this._mutations = mergeObjects([this._rootMutations, ...updatedMutations])
  }

  /**
   * Helper method for _setupModuleMutations.
   * The method retrieve nested sub modules and
   * bind each mutations to its sub tree recursively.
   *
   * @param {Object} modules
   * @param {Array<String>} nestedKeys
   * @return {Array<Object>}
   */

  _createModuleMutations (modules, nestedKeys) {
    if (!isObject(modules)) return []

    return Object.keys(modules).map(key => {
      const module = modules[key]
      const newNestedKeys = nestedKeys.concat(key)

      // retrieve nested modules
      const nestedMutations = this._createModuleMutations(module.modules, newNestedKeys)

      if (!module || !module.mutations) {
        return mergeObjects(nestedMutations)
      }

      // bind mutations to sub state tree
      const mutations = {}
      Object.keys(module.mutations).forEach(name => {
        const original = module.mutations[name]
        mutations[name] = (state, ...args) => {
          original(getNestedState(state, newNestedKeys), ...args)
        }
      })

      // merge mutations of this module and nested modules
      return mergeObjects([
        mutations,
        ...nestedMutations
      ])
    })
  }
  

new Store 先是初始化了this._rootMuattions、this._modules等变量,然后通过 new Vue 将 state 代理到 this._vm.state 中。

实现 dispatch 方法(也就是正式版本中的 commit 方法),再通过 this._setupModuleState(state, modules) 和 this._setupModuleMutations(modules) 递归实现 module 上的state 树合并到 store 的 root state树上,module上的 mutations 合并到 store的 mutations 上。

下图就是new Vuex.Store()的大概流程

Vuex Source Code Process Version1.0.png

小结:我们可以看到1.0分支的vuex还是比较简陋的,在 Store 类中并没有 vuex3.0+ 版本的 actions 的实现、module 也没有命名空间、没有实用的辅助函数等等,但是整体流程是十分清晰,简单易懂的。

dev版本的vuex(3.6.2)是如何实现的

废话不多说,我们直接看目录结构:

|-- src
|    |-- module
|        |-- module.js
|        |-- module-collection.js
|    |-- plugins
|        |-- devtools.js
|        |-- logger.js 
|    |-- helpers.js
|    |-- index.cjs.js
|    |-- index.js 
|    |-- index.mjs
|    |-- minxin.js
|    |-- store.js
|    |-- util.js
|-- package.json
|-- ...

index.js

我们先从index.js文件看起

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

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

export {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

index 使用了 export default 和 export 两种方式导出方法。提供了多元化的引入方式

mixin.js

我们知道 install 方法也是定义在 store.js 的,

// store.js
import applyMixin from './mixin'

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

mixin.js 就跟1.0版本中 override.js 作用是一样的,都是为了能在 Vue 的上下文环境中使用this.$store ,实现方式也是大同小异,就不多说了

// 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
    // vue 1.0 版本的向后兼容
    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
    // 跟1.0版本有区别的是,传入的 store 可以是 function ,只要返回的值是 store 对象
    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
    }
  }
}

store.js

现在我们来看最核心的 store.js

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

    if (__DEV__) {
      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._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue() // 使用 $watch 监听 state 的变化
    this._makeLocalGettersCache = Object.create(null)

    // 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
    // 初始化根 module 同时递归的注册所有子 module

    installModule(this, state, [], this._modules.root)
    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    // 初始化 store 实例,同时将 _wrappedgetter 注册为 computed 属性 
    resetStoreVM(this, state)

    // apply plugins
    plugins.forEach(plugin => plugin(this))

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

在new Store()开始阶段,主要对设置初始变量的值,我们着重看this._modules = new ModuleCollection(options)方法,

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    // 注册根 module 
    console.log('rawRootModule', rawRootModule)
    this.register([], rawRootModule, false)
  }
  
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      // 获取当前 module 的 parent 的 store
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    // 注册嵌套 modules 
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
 }

这些代码的主要功能是将传入的 options 转换成一个 module 对象并赋值给 this.module

image.png

此时整个 Store 的属性为:

{
    _actions: {}
    _mutations: {},
    _module: ModuleCollection,
    _modulesNamespaceMap: {},
    _wrappedGetters: [],
    ...
}

installModule 方法

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

  // register in namespace map
  if (module.namespaced) {
    // 如果 module 之前被注册过就会报错
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    // 
    store._modulesNamespaceMap[namespace] = module
  }

  // 非 root module 设置 state 
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      Vue.set(parentState, moduleName, module.state)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)
  // 遍历注册 mutations
  module.forEachMutation((mutation, key) => {
    // 以 shopping-cart 为例子就是 cart/${key}
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // 遍历注册 actions
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 遍历注册 gettter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // 遍历注册 module._children 里的模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

function registerMutation (store, type, handler, local) {
  // 如果 store._mutations[type] 没有为null, 就初始化 this._mutations[type] 并赋值 []
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

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) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

在 registerAction 的时候做了一些处理,一是对 action 方法的第一个参数的格式化,二是判断 action 方法是否返回的是一个 Promise 对象,如果不是就给该 actions 方法包上一层Promise

此时整个 Store 的属性为:

{
    _actions: {
        'actionType': fn
        'moduleName/actionType': fn,
        ...
    }
    _mutations: {
        'mutationType': fn,
        'moduleName/mutationType': fn
    },
    _module: ModuleCollection,
    _modulesNamespaceMap: {
        'moduleName/': Module,
        ...
    },
    _wrappedGetters: {
        'moduleName/getter': fn,
        ...
    }
    ...
}

可以看到 _module 和 _module._children 里的module 都被合并到了 root module里

resetStoreVM 方法

// 可以通过 this.$store.state 访问到 this._vm._data.$$state
get state () {
  return this._vm._data.$$state
}

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍历 wrapperGetters,并用computed 对象保存,使得可以通过 this.$store.getters.xxxx 访问到该 getter
  // this.$store.getters.xxxx 访问到 this._vm.xxxx
  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
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 设置新的 store._vm 初始化,以及 getters 作为 computed 属性
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    // 禁止除 mutations handler 外修改 state
    enableStrictMode(store)
  }

  if (oldVm) {
    // 解除旧 vm 的引用,以及销毁旧的 vue 对象
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

commit 方法

commit (_type, _payload, _options) {
    /**
     * 检查 object 形式调用的 commit, mutation 也可以通过下面这种方式调用
     * mutation({
     *  type: 'increment',
     *  amount: 10
     * })
     */
    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)
      })
    })

    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))

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

dispatch 方法

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 (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers
        .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (__DEV__) {
        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 new Promise((resolve, reject) => {
      result.then(res => {
        try {
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }

我们可以看到所有改变 state 状态的都是通过 store._withCommit 方法:

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

在 vuex 中为了让状态的每次改变都很明确且可追踪,vuex 状态的所有的改变都必须在 store 的 mutations handler 中调用。

我们可以看到每次传入的修改 state 的方法被执行前都会将_committing 置为true,执行完成后再将_committing 恢复成原位。外部修改state虽然能生效,但是 _committing 变量是不会变化的,这时只要监听 state 的变化时判断 _committing 变量就能知道修改的合法性了。

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

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

到这里 vuex 的基本实现我们都大概清楚了,这些mapState, mapMutations, mapGetters, mapActions辅助函数的实现有兴趣的朋友可以自行翻看了解。

其他能学习到的知识点

利用 typeof 的安全防范机制

typeof 去检查一个没有声明过的变量结果会返回undefined而不是直接报错, 所以可以通过 typeof 去检查是否有其他模块声明的全局变量,在 devtool.js 我可以看到有以下的写法

// web 环境下 window 是全局变量,node 环境下的全局变量是 global
const target = typeof window !== 'undefined'
  ? window
  : typeof global !== 'undefined'
    ? global
    : {}

如何防止 this 丢失

在这里我们先复习一下this的定义:

this是在执行上下文(执行环境)创建时确定的一个在执行过程中不可更改的变量。
this只在函数调用阶段确定,也就是执行上下文创建阶段进行赋值,保存在环境变量中。
实际上,this的最终指向是那个调用它的对象

在1.0分支的vuex中,我们能看到源码是这样去实现 dispatch 的:

const dispatch = this.dispatch
// bind dispatch to self
this.dispatch = (...args) => {
    dispatch.apply(this, args)
}

如果我们不通过显示绑定(apply、call、bind)将 dispatch 方法的 this 绑定到 store 对象上的话,那么像下面例子中这样使用dispatch就会有问题:

let func = this.$store.dispatch
func(type, payload) // 严格模式下会指向undefined。非严格模式下,web浏览器环境指向window
// component.vue
export default {
    mounted() {
        let { dispatch } = this.$store
        dispath(); // this 指向该组件
    }
}

在vuex源码中还有很多地方使用到了该技巧,如下面等等

entry.push(function wrappedMutationHandler (payload) { 
    handler.call(store, local.state, payload) 
})

设计模式:发布-订阅者模式

在devtool.js中,我们可以看到有以下的代码:

const hook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__
  
hook.emit('vuex:init', store)
hook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
})

devtool插件中应该用到了发布-订阅者模式,通过on去订阅事件,emit通知变化。Vue中的EventBus就是一个很典型的订阅发布者模式,在这里我们也简单实现以下:

class Vue {
    constructor() {
        this.subs = {}
    }
    $on(type, fn)() {
        if(!this.subs[type]){
            this.subs[type] = []
        }
        this.subs[type].push(fn)
    }
    $emit(type, ...args) {
        if(this.subs[type]){
            this.subs[type].forEach((fn) => {
                fn(...args)
            })
        }
    }
    $off(type, fn) {
        let fns = this.subs[type]
        let index = fns.indexOf(fn)
        if(index !== -1){
            fns.splice(index, 1)
        }
    }
    $once(type, fn) {
        let wrapper = (...args) => {
            fn(...args)
            this.$off(type, wrapper)
        }
        this.$on(type, wrapper)
    }
}