vuex 源码分析(6)—— dispatch

1,206 阅读1分钟

前言

在阅读 dispatch 源码之前,希望同学们能花几分钟时间温习一下它的使用方式,以便快速理解。

源码解读

相关方法

  • unifyObjectStyle 检测参数类型并进行相应调整。详细介绍(提示:跳转后,向下滚动)。

dispatch 源码

dispatch (_type, _payload) {
    // 检测参数类型并进行相应调整
    const {
      type, // 触发的类型 ,例如:store.dispatch('increment') 
      payload // 提交的载荷即参数,例如:store.dispatch('increment', 10)
    } = unifyObjectStyle(_type, _payload)

    const action = {
      type,
      payload
    }
    // this._actions,store实例定义的用于存储action的对象
    const entry = this._actions[type]
    // entry不存在就阻止运行
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      // this._actionSubscribers,用于存储调用store.subscribeAction时,传入的函数
  
      // 使用slice进行浅复制,是为防止订阅者同步调用取消订阅而造成的迭代器失效。
      
      // filter将符合条件(包含before属性)的元素筛选出来,组成一个新数组,然后遍历
      // 此数组中的元素并调用before。这个筛选条件与store实例方法:subscribeAction
      // 有关(稍后会介绍它)。
      this._actionSubscribers
        .slice()
        .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)
      }
    }
    
    // 使用Promise.all的原因:一个store.dispatch在不同模块中可以触发多个action函数。
    // 在这种情况下,只有当所有触发函数完成后,返回的Promise才会执行。
    const result = entry.length > 1 ?
      Promise.all(entry.map(handler => handler(payload))) : entry[0](payload)

    return new Promise((resolve, reject) => {
      result.then(res => {
        try {
          // filter将符合条件(包含after属性)的元素筛选出来,组成一个新数组,然后遍历
          // 此数组中的元素并调用after。这个筛选条件与store实例方法:subscribeAction有关。
          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 {
          // filter将符合条件(包含error属性)的元素筛选出来,组成一个新数组,然后遍历
          // 此数组中的元素并调用error。这个筛选条件与store实例方法:subscribeAction有关。
          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)
      })
    })
}

为了更好的理解源码中对this._actionSubscribers的操作,我们需要了解一下Vuex.Store的实例方法:subscribeAction

学习这个方法时,我们可以看到对其作用的定义:

它是用来订阅store的action。handler会在每个action分发的时候调用并接收action描述和当前的store的state这两个参数

若是,对这段话不甚清楚的话,可以多看几遍官方的举例:

store.subscribeAction((action, state) => {
  console.log(action.type)
  console.log(action.payload)
})
  1. handler指的就是传入subscribeAction方法的函数。
  2. 能接收action描述和当前的store的state这两个参数,是因为在对this._actionSubscribers进行遍历时就已传入。

光靠文字描述,还不能够太清晰。所以,我们就结合案例及其源码来看一下吧。

subscribeAction 案例

所用案例,是vuex项目中提供的counter测试案例(目录位置:examples/counter)。

调用store实例方法 dispatch 和 subscribeAction:

例1.jpg

例2.jpg

参数打印:

例3.jpg

图中,我们不仅打印了传入的参数,也打印了this._actionSubscribers。初始时,它是个空数组, 一旦调用store.subscribeAction,这个数组中就会被放入一个函数,就是传入subscribeAction的handler。

subscribeAction 源码

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

如果fn是个函数,就将一个对象赋值给subs,此对象,以before为键,fn为值。若是,你有仔细阅读vuex中有关subscribeAction的教程的话,想必你已明白,这么做的原由,也就是下面这句话:

从 3.1.0 起,subscribeAction 也可以指定订阅处理函数的被调用时机应该在一个 action 分发之前还是之后 (默认行为是之前)。

这是相应的代码部分,引用自官方文档。大家可以自行调试一下。

store.subscribeAction({
  before: (action, state) => {
    console.log(`before action ${action.type}`)
  },
  after: (action, state) => {
    console.log(`after action ${action.type}`)
  }
})

关于 genericSubscribe 方法,我在上一篇文章中已有介绍,这里不再重复。同时,关于在this._actionSubscribers上调用slice方法的原因,和上一篇文章谈到的对this._subscribers调用slice方法的原因是一样的(要仔细阅读一遍哟,一通百通嘛)。

一个store.dispatch在不同模块中可以触发多个action函数

还记得下面这段代码吗?这是dispatch源码中的另一重要知识点。

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

在我们学习vuex关于Actions的教程时,你是否注意的最后一句话: 一个store.dispatch在不同模块中可以触发多个action函数。在这种情况下,只有当所有触发函数完成后,返回的Promise才会执行。

如你所想,这句话的对应实现,就是上面这段代码,其中关键就是Promise.all的使用。为更加清晰地理解上面的语句,我们举例说明一下。

代码部分

位于vuex目录中的位置:examples/counter/store.js,在根模块和moduleB模块中注册一样的 action:increment,为了简便,仅贴出了需要改动的部分,其它部分都一样。

// 根模块和moduleB模块都注册一样的 increment
const actions = {
  increment: ({ commit }) => commit('increment'),
}

const moduleB = {
  // namespaced: true,
  actions: {
    increment: ({ commit }) => commit('increment')
  }
}

调用

位于vuex目录中的位置:examples/counter/Counter.vue,直接在mounted生命周期函数中调用,方便测试。

mounted () {
    // 在这里调用,仅是为了便于测试
    this.$store.dispatch('increment')
},

打印参数

位于vuex目录中的位置:src/store.js,找到dispatch源码部分打印参数。

1623422567(1).jpg

结果

1623422869(1).jpg

store.dispatch().then()

大家到知道,调用store。dispatch分发action时,可以使用then,就是下面代码展示的那样。

store.dispatch('actionA').then(() => {
  // ...
})

至于原因,若是大家已仔细阅读dispatch源码,那么便知道,它最后返回了一个Promise对象。

结束语

最近有点忙,在解读完commit源码后,一直想把dispatch源码也解读了,现在终于补上了。同时,若是小伙伴们看到了这篇文章并发现了有解读错误的地方,还请不吝赐教,大家一起共同进步加掉发。