四月 21, 2020

Vuex 代码精读

Vuex 代码精读

序言

时隔多日,终于又有时间和精力来阅读源码了,这回挑的是和 redux 有些类似的 vuex,本来以为两者都是状态管理工具,大部分逻辑可能是重合的,但是没想到两者大相径庭,从代码哲学上就有分歧,具体实现更是相差甚远。
后面或许会另起一篇来具体描述一下两者的差异。

目录结构

src
├─ module                       
│    ├─ module-collection.js    // 模块树相关代码
│    └─ module.js               // 单个模块相关代码
├─ plugins
│    ├─ devtool.js              // devtool 插件
│    └─ logger.js               // log 插件
├─ helpers.js                   // 给开发者的各类工具函数(mapMutations...
├─ index.esm.js                 // 入口文件
├─ index.js                     // 入口文件
├─ mixin.js                     // 将 vuex 注入 vue 全局中
├─ store.js                     // 主代码入口
└─ util.js                      // 工具函数

前期提要

关键词复习

  • state: 状态
  • getter: store 的 computed 集合,用于快速获取一些状态及状态的简单变形
  • mutation: 改变状态的唯一方法(仅支持同步
  • action: 通过操作 mutation 来实现状态的更新(支持异步
  • module: 状态的集合

如何 debug Vuex 源码

以 Vuex 官方示例项目为示例,我们需要先在 examples/webpack.config.js 添加 devtool: 'source-map',然后将示例项目跑起来,打开控制台就可以愉快地 debug 啦。

简单描述一下 vuex 的状态更新思路

Vuex 整体采用了 OOP 的哲学来实现(Redux 是 FG),总共有三个 Class,分别为 StoreModuleCollectionModule而对于开发者而言,状态的更新过程简单来说,就是我们使用了 Store 的内部方法然后改变了 Store 的内部属性。(够简略了吧🤣,Redux 其实就是个闭包)

其中使得状态具有响应能力的实现方式就是实例化一个 Vue,然后将 state 注入 data,将 getter 注入 computed。

代码解读

既然是 OOP,那就用 OOP 的思路来讲解。

Store

上面是官方示例项目 shopping-cart 初始化后的 Store

commit

commit (_type, _payload, _options) {
    // 1: 处理传入参数
    // 兼容两种 commit 传参方式
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    
    // 2: 提取对应的 mutation 集合
    // _mutations 以键值对的形式存储着一个 type 对应的 mutation 集合
    const entry = this._mutations[type]
    
    // 3: 数据校验
    // 如果不存在 mutation 集合则抛出错误结束
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    
    // 4: 遍历调用 mutation 集合,同时改变 committing 状态
    // _withCommit 的作用是在运行函数的同时,调整 committing 状态
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

    // 5: 调用订阅函数
    // 一般用于通知 plugin 响应已数据变化,比如 logger 的对应函数会在这里被调用
    this._subscribers
      .slice() // 浅拷贝以防止无法迭代
      .forEach(sub => sub(mutation, this.state))

    // 6: 提示 silent 已被移除
    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

dispatch (_type, _payload) {
    // 1: 处理传入参数
    // 兼容两种 commit 传参方式
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }

    // 2: 提取对应的 action 集合
    // _actions 以键值对的形式存储着一个 type 对应的 action 集合
    const entry = this._actions[type]

    // 3: 数据校验
    // 如果不存在 mutation 集合则抛出错误结束
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    // 4: 调用 action 之前需要调用的监听函数(调用 subscriber.before 集合)
    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 (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    // 5: 遍历调用 action 集合,如果有多个则使用 Promise.all 合成一个 Promise
    // action 在被注册时,如果不是异步函数,则会被强制输出为 Promise
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    // 4: 调用 action 之后需要调用的监听函数(调用 subscriber.after 集合)
    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
    })
}

getter

function resetStoreVM (store, state, hot) {
  // 初始化时为 undefined
  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 = {}
  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) // 为什么不用 fn.bind(store, 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 = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

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

  if (oldVm) {
    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())
  }
}

(更新中……)