首发于不止前端

Vuex源码解析四及对比Redux

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

plugin

plugin其实是对subscribe的封装,store里提供了subscribesubscribeAction来订阅commitdispatch。不过plugin只针对subscribe,为的就是只考虑同步。

 const myPlugin = store => {
   // 当 store 初始化后调用
   store.subscribe((mutation, state) => {
     // 每次 mutation 之后调用
     // mutation 的格式为 { type, payload }
   })
 }

plugin只在同步情况下被使用到,由于异步dispatch最终也是会调用commit的,所以也会间接调用了plguin,没有必要为subscribeAction再做一个封装了。

Vuex里提供了两个内置的plugin——devtoollogger,来看这个logger

 let prevState = deepCopy(store.state)
 
 store.subscribe((mutation, state) => {
   if (typeof logger === 'undefined') {
     return
   }
   const nextState = deepCopy(state)
 
   if (filter(mutation, prevState, nextState)) {
     const time = new Date()
     const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
     const formattedMutation = mutationTransformer(mutation)
     const message = `mutation ${mutation.type}${formattedTime}`
     const startMessage = collapsed
       ? logger.groupCollapsed
       : logger.group
 
     // render
     try {
       startMessage.call(logger, message)
     } catch (e) {
       console.log(message)
     }
 
     logger.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
     logger.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
     logger.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
 
     try {
       logger.groupEnd()
     } catch (e) {
       logger.log('—— log end ——')
     }
  }
 
   prevState = nextState
 })

除开打印日志的代码外,我们看到用闭包prevState变量来hold住之前的状态,每次commit完成会调用subscribe注册的回调,即进入(mutation, state) => {},在这个方法中能访问到prevState变量,在结束的时候,用最新的stateprevState给替换掉。

仿照这种方式,实现一个回滚操作并不是难事,下面是我自己的一个实现,当然写的更好看一点的话可以封装一下,不让stateHistory暴露在外面,当然devtools插件里也有这个功能。

 const state = {
   count: 0
 };
 
 let stateHistory = [state.count]; //简化起见,只针对count的回滚
 
 const mutations = {
   randomIncrement(state) {
     state.count += Math.ceil(Math.random() * 10);
   },
   increment(state) {
     state.count++;
   },
   decrement(state) {
     state.count--;
   },
   ctrlz(state) {
     if (stateHistory.length > 1) {
       state.count = stateHistory[stateHistory.length - 2]; //另外可以考虑store.replaceState
       stateHistory.length--;
     }
   }
 };
 
 export default new Vuex.Store({
   strict: true,
   state,
   mutations,
   plugins: [
     store => {
       store.subscribe((mutation, state) => {
         if (mutation.type !== "ctrlz") {
           stateHistory.push(state.count);
         }
       });
     }
   ]
 });

mapXxx

Vuex上还暴露出了一系列mapXxx的辅助方法,类似乎react-reduxconnect干的活,代码比较简单,不再详述,不过有个辅助方法还是挺实用的

 function normalizeMap (map) {
   if (!isValidMap(map)) {
     return []
   }
   return Array.isArray(map)
     ? map.map(key => ({ key, val: key }))
     : Object.keys(map).map(key => ({ key, val: map[key] }))
 }

可以处理数组或者对象。


有个指定namespace的用法可能是盲点

 computed: {
   ...mapState('some/nested/module', {
     a: state => state.a,
     b: state => state.b
   })
 },
 methods: {
   ...mapActions('some/nested/module', [
     'foo', // -> this.foo()
     'bar' // -> this.bar()
   ])
 }

它使用了normalizeNamespace方法来做适配,如果第一个参数不是string,那么就是map函数,如果是string,那么就是namespace

 function normalizeNamespace (fn) {
   return (namespace, map) => {
     if (typeof namespace !== 'string') {
       map = namespace
       namespace = ''
     } else if (namespace.charAt(namespace.length - 1) !== '/') {
       namespace += '/'
     }
     return fn(namespace, map)
   }
 }

对比Redux

这一节一直是我想写又不敢写的,水平有限,能力一般,江山父老能容我,不使人间造孽钱。

很多的不同或者对比其实背后体现出VueReact思想、生态之间的区别。

API使用

在这一点上Vuex显得相对简单一些,Redux由于中间件的因素导致整体理解难度稍高一些。

项目结构

分歧点在于状态管理的代码如何存在,在Vuex官网上推荐独立集中存放

 store
     ├── index.js          # 我们组装模块并导出 store 的地方
     ├── actions.js        # 根级别的 action
     ├── mutations.js      # 根级别的 mutation
     └── modules
         ├── cart.js       # 购物车模块
         └── products.js   # 产品模块

Redux官方文档上讲的有些含糊,不过业界很多人都认为该把状态管理代码分在不同的业务目录下,最后再集中。

明确Action概念

Redux中,明确提出了Action概念,用以封装typepayload,而Vuex里并没有。

纯函数

Redux明确规定修改状态的Reducer是一个纯函数,而Vuex里面因为有响应式加持,所以没有纯函数的概念在内。

直接修改状态

Redux完全限死了修改状态只能通过Reducer,所以直接修改的话,改是该了,不会触发subscribe监听导致不会引起视图更新。而Vuex由于响应式的存在,直接修改后也是能引起视图更新的,只是无论怎样,这种做法都不值得推荐。

命名空间

这玩意是用来解决命名冲突的,Vuex里明确有这个配置项,一旦设为true,不会再有因为同一个名字的commit而冲突,都带上module名字的前缀的。而Redux没有对此种情况声明负责,而一些别的库比如dva很自觉的加上前缀了,附一张dva的框架图,之前对它的印象不错。

更新视图

Vuex因为有Vue加持(内部会生成一个Vue对象,从这个角度看,Vuex像是Bus,或者接近于Mobx),能够跟踪数据的变化并通知到视图。而Redux在自身上并没有针对通知视图变化给出解决方案,而仅仅是开放了监听的口子供调用。真正解决这个问题的是在react-redux里。

凡事都有两面性,Vuex就被限定在了Vue圈子里,而Redux变成了一个百搭,可以和任何技术、框架一起合作使用。

生态圈

Vuex延续了Vue家族的特点,它属于官方开发的状态管理工具,功能比较完备。而Redux仅仅是最基本的架子,得要带着中间件、视图更新解决方案库等弟兄们一起上。

Redux这样能玩出花来,比如容器组件、显示组件的划分就是这么来的,而Vuex秉承着怎么简单怎么来的思想,简化使用者的思维。

表单

Redux本身肯定没有专门针对表单提出解决方案,Vuex在官方文档中对表单的用法提出了建议。

扩展

Redux主要是提供了中间件的机制来扩展自身,而Vuex提供的是插件机制。两者所处的位置不同。中间件是在提交变化前后,有点像AOP切面的位置。而Vuex的插件本质上还是监听变化,是对subscribe的封装。

编辑于 03-09

文章被以下专栏收录