首发于不止前端

Vuex源码解析三

Anli Li:Vuex源码解析一zhuanlan.zhihu.com图标Anli Li:Vuex源码解析二zhuanlan.zhihu.com图标Anli Li:Vuex源码解析三zhuanlan.zhihu.com图标Anli Li:Vuex源码解析四及对比Reduxzhuanlan.zhihu.com图标

mutation/commit

从刚才的module解析里其实对mutation已经略有些了了解,现在更详细的走一遍commit的过程。

我们知道commit其实是一个同步的过程,现在我们把断点打在commit语句前,一步步地走下去。

首先来到

 this.commit = function boundCommit(type, payload, options) {
   return commit.call(store, type, payload, options);
 };

相当于在实例上又定义了一个commit方法,把prototype上的commit方法跟自己(对象实例)绑定起来,初看好像是脱裤子放屁,但实际上为的是把this.commit方法执行时的this死死摁在了store实例上,试想此时即便我是用下面的代码调用commit,也不会有this上的任何问题。

 this.$store.commit.call({}, 'increment')

因为方法体里,commit是被绑定在了store上,外层也即是定义的this.commit上的this根本无所谓了。其实这个例子还不够极端,因为还是从this.$store上调用了commit方法,真实情况是,调用commit方法的地方可能更复杂,比如在dispatch里,this有可能不知道被绑在什么对象上了,但希望是commit里的this永远要指向store实例,现在使用这种方式杜绝了一百万种可能,并且我们可以实现在mutation里调用另一个mutation的操作,因为既然mutation里的this肯定是store,那么调用this.commit就能发起另一个commit了。

 const mutations = {
   increment(state) {
     this.commit('decrement');
     state.count++;
   },
   decrement(state) {
     state.count--;
   }
 };


进入到commit方法里第一步是把数据归一处理一下

 const {
   type,
   payload,
   options
 } = unifyObjectStyle(_type, _payload, _options)

我们知道commit里可以传入多种风格的数据,比如

 store.commit('increment', 10)
 store.commit('increment', {
   amount: 10
 })
 store.commit({
   type: 'increment',
   amount: 10
 })

它们都在unifyObjectStyle方法里把typepayloadoptions给抽出来,抽出来之后,按照先commitsubscribe的次序都执行一遍即可。

 this._withCommit(() => {
   entry.forEach(function commitIterator (handler) {
     handler(payload)
   })
 })
 
 this._subscribers
   .slice() // 浅拷贝以避免在subscriber同步调用unsubscribe使得iterator迭代失效
   .forEach(sub => sub(mutation, this.state))

其中_withCommit里是有标记的

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

这个标记就是Vuex用来判断是否是直接修改Vuexstate的,见上面enableStrictMode源码,一个细节是enableStrictMode调用的时机,初始化state做完后才调用的,这样只会针对state修改才去检查。

为什么要缓存、恢复上一次的_committing而不是直接设置为false,因为有可能存在嵌套的情况,commit里再调commit,那么在第二层commit上直接设置为false,就把第一层的实际数据给错刷了。


做一下实验,首先把strict配置给打开,然后直接修改this.$store.state.count = 100,触发后发现数据改变还是生效的,只不过在console里打印了错误信息。

enableStrictMode里使用Vuewatch监听数据变化,它是同步的,不是异步,等于是在_withCommitfn()调用里做完是否直接修改state的检查后,才恢复this._committing的。


有了namespace之后,在module内分发commit默认是在module范围之内,但是Vuex也是开了个口子,可以向state根部分发,比如

 // foo模块
 dispatch('someOtherAction') // -> 'foo/someOtherAction'
 dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
 commit('someMutation') // -> 'foo/someMutation'
 commit('someMutation', null, { root: true }) // -> 'someMutation'

先看为什么能限制在module范围之内,秘密在makeLocalContext方法里

 const local = {
     commit: noNamespace ? store.commit : (_type, _payload, _options) => {
       const args = unifyObjectStyle(_type, _payload, _options)
       const { payload, options } = args
       let { type } = args
 
       if (!options || !options.root) {
         type = namespace + type
         if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
           console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
           return
         }
       }
 
       store.commit(type, payload, options)
     }
  }

不带namespace的时候,用store上的commit,如果有了namespace,会自动在type上加上namespace前缀,避免冲突。另外留了个后门就是option.roottrue的时候,不会加上前缀,那么还是发送给了根部的module上的mutation


接下来看看如果在mutation里异步修改state会怎样?好奇总是不会有错。

修改成异步

 const mutations = {
   increment(state) {
     setTimeout(() => {
       state.count++
     }, 3000)
   },
   decrement(state) {
     state.count--;
   }
 };

点击按钮后三秒,数据增加1,但是console里还是打印了state在外部修改的错误,相当于_withCommit里的fn同步执行的时候还没有修改state,等到修改了之后this._committing早就被恢复为false了,那么修改state所触发的watch当然就认为你是在外部自己修改了state的值。官方文档上也强调了不要在mutation里异步修改数据,保证它是同步的,方便问题的跟踪与定位。

action/dispatch

终于来到了异步的环节,Redux里异步处理往往伴随着中间件,我认为它本质上是一种更高抽象的思维,你其实也可以在自己的业务代码里的业务回调方法里触发同步更新的机制,但这样子就充斥了各种重复的代码,所以向上进行更高层次的抽象,把异步的工作交给中间件来做,业务代码保持干净,最终只要保证同步更新的机制没问题就行,Vuex里也是一样的思路。

添加一个action用作测试

 const actions = {
   asyncIncrement(context) {
     setTimeout(() => {
       context.commit("increment");
     }, 3000);
   }
 };

初始化完成之后,不出意外地action被收集完成了

store里,dispatch也是跟commit一样,被封了一层来绑定storethis

 this.dispatch = function boundDispatch (type, payload) {
   return dispatch.call(store, type, payload)
 }

执行的时候,先执行actionSubscriptionbefore部分

 this._actionSubscribers
     .slice()
     .filter(sub => sub.before)
     .forEach(sub => sub.before(action, this.state))

订阅是通过store,比如

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

然后执行action的本体

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

注意,action在准备module的时候已经全包装为Promise了(严格的说其实是包装为返回Promise的方法),所以这里才能用Promise.all

 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)  // 包装为Promise对象
     }
     if (store._devtoolHook) {
       return res.catch(err => {
         store._devtoolHook.emit('vuex:error', err)
         throw err
       })
     } else {
       return res
     }
   })
 }

在全部执行完成后,再会执行订阅的after部分,会返回一个Promise的哦,就是说你仍然还有机会在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
 })

关注一下回调里面传入的context是什么,官方文档里明确说了context不是store

答案就在刚才的registerAction方法里,注意local指的是本module,而store指的是全局module或者说是根部的module。

 let res = handler.call(store, {
   dispatch: local.dispatch,
   commit: local.commit,
   getters: local.getters,
   state: local.state,
   rootGetters: store.getters,
   rootState: store.state
 }, payload)
 
 //local对象的出处
 const local = module.context = makeLocalContext(store, namespace, path)

断点停下来看一看,丝丝入扣

action里也是通过调用commit来同步修改state的数据,就是说任何情况下,state的修改都是由commit来发起的。

编辑于 03-09

文章被以下专栏收录