Vuex 4源码学习笔记 - 通过dispatch一步步来掌握Vuex整个数据流(五)

在上一篇笔记中:Vuex 4源码学习笔记 - Store 构造函数都干了什么(四)

我们通过查看Store 构造函数的源代码可以看到主要做了三件事情:

  • 初始化一些内部变量以外
  • 执行installModule来初始化module以及子module,
  • 执行resetStoreState函数通过Vue3的reactive使store的状态实现“响应式”)。

今天我们我们通过官方的购物车示例来查看Vuex内部的工作流程,示例代码在examples/composition/shopping-cart文件夹内

Vuex配置代码如下:

import { createStore, createLogger } from 'vuex'
import cart from './modules/cart'
import products from './modules/products'

const debug = process.env.NODE_ENV !== 'production'

export default createStore({
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

Vuex组件module中各模块state配置代码部分:

/**
cart.js
*/
const state = {
  items: [],
  checkoutStatus: null
}
/**
products.js
*/
const state = {
  all: []
}

页面加载成功后可以看到state和getters

image-20211123172114819

state和getters都是按照配置中module path的规则来划分的

然后我们看在ProductList.vue组件中,通过store.dispatch方法来调用vuex中actions,其他不重要的代码省略了。

<script>
import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()

    //... 

    store.dispatch('products/getAllProducts')

    //...
  }
}
</script>

这是products.js中的actions

const actions = {
  getAllProducts ({ commit }) {
    shop.getProducts(products => {
      commit('setProducts', products)
    })
  }
}

第一步:store.dispatch将走到Store 构造函数内的this.dispatch函数

this.dispatch重写了Store类的原型prototype上的dispatch方法,根据优先级规则,会优先找实例上的属性方法,再去找prototype原型上的属性方法。

/* 将dispatch与commit调用的this绑定为store对象本身*/
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}

第二步:dispatch.call将走到Store 的原型方法dispatch

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

在dispatch函数内:

  • 第一,首先通过unifyObjectStyle函数来抹平成统一化的参数,因为actions和mutation都支持两种参数方式调用
// check object-style dispatch
// 抹平数据格式,dispatch调用可能是以载荷形式分发,也可能是以对象形式分发
const {
  type,
  payload
} = unifyObjectStyle(_type, _payload)

// 建立action对象
const action = { type, payload }

两种传递参数形式:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
  • 第二,然后在this._actions中通过type来找到对应的actions函数数组,然后通过if做了判断
// 在this._actions中寻找对应的action数组
const entry = this._actions[type]
if (!entry) {
  if (__DEV__) {
    console.error(`[vuex] unknown action type: ${type}`)
  }
  return
}
  • 第三,之后的try/catch内的代码主要是用来执行action订阅器数组中的函数,比如在logger.js的插件中,我们就通过store.subscribeAction方法来订阅了action,那么在action运行时,就会执行我们订阅器函数。action订阅的状态主要有三种,分别为:before(action执行之前)、after(action执行之后)、error(action执行失败)。
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)
  }
}
  • 第四,然后就是运行actions函数,如果是多个actions使用Promise.all来处理,如果是单个就直接运行
const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  • 第五,最后,action会返回一个promise,如果成功或者失败都会调用action订阅器,我们可以在插件中订阅它。
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)
  })
})
  • 第六步,在上面第四步调用的action函数为我们昨天看到的installModule函数中registerAction函数包装而成。

下面的wrappedActionHandler函数就是经过包装的函数,通过call设置handler函数内的this指向为store对象,然后传入一个带有当前模块的dispatch、commit、getters、state和根模块rootGetters、rootState的对象。

接着如果函数返回的不是Promise,用Promise.resolve进行了包装,因为上面的action函数需要promise调用。

function registerAction (store, type, handler, local) {
  // 取出对应type的actions-handler集合
  const entry = store._actions[type] || (store._actions[type] = [])
  // 存储新的封装过的action-handler
  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)
    // action需要支持promise进行链式调用,这里进行兼容处理
    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
    }
  })
}
  • 第七步:上面的handler.callhandler就是getAllProducts函数,函数的参数就是我们上面的对象,所以可以通过解构出commit供使用,在这个action中我们调用shop.getProducts这个异步的操作来获取数据,在获取完成数据之后,通过commit来调用mutation来更改数据。
const actions = {
  getAllProducts ({ commit }) {
    shop.getProducts(products => {
      commit('setProducts', products)
    })
  }
}
  • 第八步:上面的commit调用mutation会走到products模块的commit方法中,因为它是一个子模块,主要通过type = namespace + type,将它的namespace和type进行拼接,拼接完成后调用store上的commit方法。
/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 * 生成局部的dispatch, commit, getters 和 state
 * 如果没有命名空间,就使用根命名空间
 */
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    //...

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args // 这时type为:setProducts

      if (!options || !options.root) {
        type = namespace + type // 这时type为:products/setProducts
        if (__DEV__ && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  //...

  return local
}

第九步:和action相同,commit方法同理

/* 将dispatch与commit调用的this绑定为store对象本身*/
const store = this
const { dispatch, commit } = this
//...
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

第十步:走到commit方法

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

第十一步:和action相同,先抹平数据

// check object-style commit
// 抹平数据格式,mutation调用可能是以载荷形式分发,也可能是以对象形式分发
const {
  type,
  payload,
  options
} = unifyObjectStyle(_type, _payload, _options) 

第十二步:在this._mutations中找到mutation函数的数组,然后是if判断,然后通过this._withCommit来运行每个mutation,

// 建立mutation对象
const mutation = { type, payload }
// 根据type在this._mutations中寻找对应的mutation数组
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)
  })
})

_withCommit是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。实现代码如下。

_withCommit (fn) {
  // 保存之前的提交状态
  const committing = this._committing
  // 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告
  this._committing = true
  // 执行state的修改操作
  fn()
  // 修改完成,还原本次修改之前的状态
  this._committing = committing
}

第十三步:与action相同,mutation也有个订阅器,可以在插件中进行订阅

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

第十四步:上面_withCommit中运行的函数就是我们包装后的wrappedMutationHandler函数,和action差不多,也是绑定this,然后传入state,和payload

function registerMutation (store, type, handler, local) {
  // 取出对应type的mutations-handler集合
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // commit实际调用的不是我们传入的handler,而是经过封装的
  entry.push(function wrappedMutationHandler (payload) {
    // 调用handler并将state传入
    handler.call(store, local.state, payload)
  })
}

第十五步:上面的handler.call的handler就是下面的mutations中的setProducts函数,在setProducts函数中我们修改了state中的all字段的值。

// mutations
const mutations = {
  setProducts (state, products) {
    state.all = products
  },
  //...
}

到这里state的值其实已经被修改了,那么页面是如何做到自动变更的呢?

这就要找到Store构造函数中的resetStoreState函数调用

export function resetStoreState (store, state, hot) {
  //...

  /* 这里使用Vue3的reactive来将state实现为响应式对象 */
  store._state = reactive({
    data: state
  })

  //...
}

重点在这里,Vuex将state通过Vue3的reactive函数将数据设置为响应式数据,所以在mutation中我们修改了state,由于其为reactive的数据,所以依赖它的页面或者组件也会自动变更。

今天我们通过dispatch的调用,一步步的看到Vuex的内部工作流程,这也是看源码的最好的方式,只要捋清楚大概主流程后,再去看那些细枝末节就容易多了。

一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值