当前位置: 首页 > Web前端 > HTML

以 Vuex 为引,一窥状态管理全貌

时间:2023-03-28 01:24:21 HTML

以Vuex为入门一窥状态管理Vuex的用法和API都不难,官网的介绍也简洁明了。得益于此,可以非常轻松地将Vuex快速集成到您的项目中。但是由于使用灵活,很多同学在Vuex的设计和使用上有些迷茫。其实在使用之前,我们不妨停下来思考几个问题:什么是状态管理?我为什么要使用Vuex?组件内部状态和Vuex状态是如何分配的?使用Vuex有哪些潜在的问题?如果您对这些问题感到模棱两可,那么恭喜您,这篇文章可能就是您所需要的。请和我一起,从源头开始,以Vuex为例,一起揭开状态管理的奥秘。大纲预览本文介绍的内容包括以下几个方面:状态和组件的诞生是否需要状态管理?单数据源状态更新方式异步更新?StateModularityModularSlot接下来就是state和components的诞生自三大框架诞生以来,它们的两个共同能力已经全面冲击了Jquery。这两个能力是:数据驱动视图组件化数据驱动视图,让我们进入了只能靠DOM更新页面的时代。我们不再需要在每次页面更新时通过层层查找和修改DOM的属性和内容。这些事情都可以通过操纵数据来实现。当然,在我们前端看来,数据基本上可以理解为存储各种数据类型的变量。在数据驱动的概念出现之后,一些变量也被赋予了特殊的含义。首先是普通变量,和JQ时代没什么区别,只是用来存储数据的。此外,还有一类变量,它具有响应式的作用。这些变量绑定到视图。当变量发生变化时,绑定这些变量的视图也会触发相应的更新。我称这种类型的变量为状态变量。所谓数据驱动视图,严格来说就是状态变量在驱动视图。随着Vue和React的普及,前端开发者的重心逐渐从操作DOM转移到操作数据,状态变量成为核心。状态变量,现在大家好像更喜欢叫它状态。我们经常谈论状态和状态管理。实际上,这个状态指的是状态变量。下面所说的状态,也是指状态变量。状态有了,组件也来了。在JQ时代,一个前端页面就是一个html,没有“组件”的概念。对于页面中的公共部分,实现优雅复用并不算太难。好在三大框架都带来了非常成熟的组件设计,可以很方便的抽取一个DOM片段作为组件,并且组件可以在内部维护自己的状态,独立性更高。组件的一个重要特征是这些内部状态与外部是隔离的。父组件无法访问子组件的内部状态,但子组件可以访问父组件传递过来的状态(Props),并根据变化自动响应。这个特性可以理解为状态是模块化的。这样做的好处是不需要考虑当前设置的状态会影响其他组件。当然,完全隔离组件状态是不现实的。必须有多个组件共享状态的需求。这种情况下的解决方案是将状态提取到离这些组件最近的父组件,并通过Props向下传递。上面的共享状态方案通常是没有问题的,也是官方推荐的最佳实践。但是如果你的页面很复杂,你会发现还是力不从心。例如:组件层级太深,状态需要共享。这时候状态就必须一层层传递。子组件更新一个状态,可能有多个父组件,兄弟组件共享,实现难度大。这种情况下,继续使用“提取状态到父组件”的方法,你会发现很复杂。并且随着组件数量的增加,嵌套层次的加深,复杂度也越来越高。由于关联状态多,传输复杂,容易出现某个组件莫名其妙更新,或者某个组件没有更新等问题,异常排查也会比较困难。鉴于此,我们需要一个更优雅的方案来处理这种复杂情况下的状态。需要状态管理?上一节我们提到,随着页面的复杂化,我们在跨组件共享状态的实现上遇到了棘手的问题。那么有解决办法吗?当然有,在社区领导的努力下,方案不止一个。但是这些解决方案都有一个共同的名字,就是我们两年前讨论得非常激烈的一个名字——状态管理。状态管理,其实可以理解为全局状态管理。这里的状态不同于组件的内部状态。它独立于组件维护,然后以某种方式与需要状态的组件相关联。每个状态管理都有自己的实现方案。Vue有Vuex,React有Redux,Mobx,当然还有其他。但是他们都解决了同一个问题,就是跨组件状态共享的问题。记得前两年,因为“状态管理”这个概念的流行,它似乎已经成为了应用开发中不可或缺的一部分。以Vue为例,创建项目必然会引入Vuex进行状态管理。但是很多人不知道为什么、什么时候以及如何使用状态管理。他们只是盲目跟风,所以滥用状态管理的例子比比皆是。看到这里,应该知道状态管理不是必须的了。为什么会出现,解决什么问题上面已经基本说明了。如果还是不明白,暂停一下,从头再读一遍。不要认为一个技术方案的背景不重要。如果你不明白它出现是为了解决什么问题,那么你就不能真正发挥它的作用。Redux的作者有一句名言:如果你不知道你是否需要Redux(状态管理),你就不需要它。好了,如果你正在使用状态管理,或者需要使用状态管理来帮你解决问题,那我们继续往下看。VuexVue在国内应用广泛,尤其是中小型团队,所以大多数人最先接触到的状态管理方案应该是Vuex。那么Vuex是如何解决跨组件状态共享问题的呢?让我们一起探索。创建store上面我们提到,对于一般的组件共享状态,官方建议是“将状态提取到最近的父组件”。Vuex更进一步,将所有状态提取到根组件,以便任何组件都可以访问它。也许你会问:这不是把国家暴露给全世界吗?岂不是完全消除了模块化的优势?其实并不是。Vuex这样做的主要目的是让所有组件都可以访问这些状态,完全避免子组件的状态无法访问的情况。Vuex将所有的状态数据放在一个对象上,遵循单一数据源的原则。但这并不意味着状态是堆叠的。Vuex在这个单一的状态树上实现了自己的模块化方案。别着急,我们一步一步来,先看看如何使用Vuex。Vuex作为一个Vue插件存在。首先,使用npm安装:$npminstall--savevuex安装完成后,我们新建一个src/store文件夹,将所有Vuex相关的代码放在这里。新建一个index.js,写入如下代码。这段代码的主要作用是使用Vue.use方法加载Vuex插件,然后导出配置好的Vuex.Store实例。importVuefrom'vue'importVuexfrom'vuex'//安装插件Vue.use(Vuex)exportdefaultnewVuex.Store({state:{},mutations:{},actions:{},modules:{}})上面导出的实例通常称为store。一个store包含了存储的状态(state)和修改状态的函数(mutation)等,所有的状态和相关的操作都在这里定义。最后一步是在入口文件中将上面导出的store实例挂载到Vue中:importstorefrom'./store'newVue({el:'#app',store:store})注意:这一步挂载不是必要的。挂载这一步的作用只是为了方便在.vue组件中通过this.$store访问我们导出的store实例。如果没有挂载,直接导入使用也是一样的。单一数据源(state)在上一步中,我们使用构造函数Vuex.Store创建了一个store实例。至少每个人都知道如何使用Vuex。这一步,我们来看看Vuex.Store构造函数的具体配置。首先是状态配置,它的值是一个用来存储状态的对象。Vuex使用单状态树的原则,将所有的状态都放在这个对象上,方便后续的状态定位和调试。例如,我们有一个初始状态app_version来表示版本,如下所示:$store.state.app_versionBut这不是唯一的获取方式,也可以这样:importstorefrom'@/store'//@表示src目录store.state.app_version为什么要强调这一点?因为很多朋友认为Vuex只能通过this.$store操作。在非组件中,比如你想在request函数中设置某个Vuex状态,你不知道该怎么做。其实有有更优雅的方式获取组件中的state,比如mapState函数,可以让你获取更多的State变得更简单import{mapState}from'vuex'exportdefault{computed:{...//其他计算属性...mapState({version:state=>state.app_version})}}状态更新方法(突变)Vuex中的状态与组件中的状态不同,c不能直接修改为state.app_version='xx'。Vuex规定修改状态的唯一方式就是提交一个mutation。Mutation是一个函数,第一个参数是state,它的作用是改变state的状态。下面定义了一个名为increment的mutation来更新函数中count的状态:newVuex.Store({state:{count:1},mutations:{increment(state,count){//改变状态state.count+=count}}})然后在.vue组件中触发increment:this.$store.commit('increment',2)这样绑定count的view就会自动更新。同步更新虽然mutation是更新状态的唯一方式,但它其实有一个限制:必须是同步更新。为什么一定要同步更新?因为在开发过程中,我们经常跟踪状态变化。一种常见的方法是在浏览器控制台进行调试。在mutation中使用异步更新状态,虽然也会让状态更新正常,但是会导致开发者工具有时无法追踪到状态变化,调试起来会非常困难。再者,Vuex对mutation的定位是改变状态,只改变状态,不参与其他事情。所谓专人专干,这也有助于我们避免将状态变化和自己的业务逻辑混在一起,同时也规范了函数功能。那么如果你真的需要异步更新怎么办?异步更新异步更新状态是一个很常见的场景。比如接口请求返回的数据需要存储,就是异步更新。Vuex提供了异步更新状态的操作。与突变不同,动作不直接更新状态,而是通过触发突变间接更新状态。因此,即使使用action也不违反“修改状态的唯一方法是提交突变”的原则。Action允许在真正更新状态之前进行一些副作用操作,比如上面提到的异步,还有数据处理,根据条件提交不同的mutation等等。看一个例子:newVuex.Store({state:{count:1},mutations:{add(state){state.count++},reduce(state){state.count--}},actions:{increment(context,data){axios.get('**').then(res=>{if(data.iscan){context.commit('add')}else{context.commit('reduce')}})}}})在组件中触发动作:this.$store.dispatch('increment',{iscan:true})这些是动作的使用方法。其实action的主要作用就是请求接口,获取需要的数据,然后触发mutation修改状态。其实这一步也可以用组件来实现。我已经看到了一些解决方案。常见的是在组件中写一个请求方法。当请求成功时,直接通过this.$store.commit方法触发mutation更新state,完全不使用action。行动是可选的吗?不是的,具体场景下确实需要action,下一篇再说。状态模块化(module)前面说过,Vuex是一棵单状态树,所有的状态都存储在一个对象上。同时,Vuex有自己的模块化方案,可以避免状态堆叠在一起变得臃肿。Vuex允许我们将store分成模块,每个模块都有自己的state、mutation、action。状态虽然注册在根组件中,但是支持模块切分,相当于实现了一个与页面组件同级的“状态组件”。为了区分,我们将划分出来的模块称为子模块,将全局模块称为全局模块。我们看一下基本用法:newVuex.Store({modules:{user:{state:{uname:'ruims'},mutation:{setName(state,name){state.name=name}}}}})上面定义了用户模块,它包含一个状态和一个突变。组件中的使用方法如下://访问状态this.$store.state.user.uname//更新状态this.$store.commit('setName')大家都发现访问状态submodule是通过this.$store.state.[modulename]这样访问的,触发mutation和globalmodule是一样的,没有区别。action和mutation的原理是一样的,就不细说了。上面在namespace中提到,submodule触发的mutation和action与globalmodule是一致的,所以假设globalmodule和submodule中都有一个名为setName的mutation。在组件中触发,会执行哪个mutation?测试后,将执行。官方的说法是:对于多个模块响应同一个突变或动作。其实官方的兼容,我从来没有遇到过实际的应用场景,但是因为同名突变,误触发造成了很多麻烦。可能官方也意识到了这个问题,index后来针对mutation和action做了一个module的解决方案。这个解决方案就是命名空间。命名空间也很简单。在子模块中添加一个namespaced:true配置开启它,如:newVuex.Store({modules:{user:{namespaced:true,state:{}}}})开启命名空间之后,触发mutation变成:this.$store.commit('user/setName'),可以看到提交参数由'[mutation]'变成了'[modulename]/[mutation]'。ModularSlots上面我们介绍了Vuex的模块化方案,将单个状态树存储分成多个模块,每个模块负责存储和更新模块的状态。模块化是必须的,但是这种模块方案用起来总感觉有点别扭。比如整体设计是先把store分成模块,模块包括state、mutation、action。那么按照正常的理解,访问user模块下的state应该是这样的:this.$store.user.state.uname但是实际的API是这样的:this.$store.state.user.uname这个API好像在state中,它们也被划分为模块。没看过源码,但从体验上看,这就别扭了。除了state,mutation和action默认注册在全局设计中,也很别扭。首先,官方说响应同一个突变或动作的多个模块,这个功能目前还没有找到应用场景。并且在没有配置命名空间的情况下,名称必须是唯一的,否则会导致误触发。其次,使用命名空间后,triggermutation是这样的:this.$store.commit('user/setName')这明明是单独处理参数的,为什么不呢:this.$store.user.commit('setName')总体感觉Vuex模块化不够彻底。为什么要抱怨上面提到的插槽,不是为了抱怨。主要原因还是还有优化的空间。例如,this.$store.commit函数可以触发任何改变状态的突变。如果一个组件比较复杂,需要对多个子模块的状态进行操作,那么很难快速找出当前组件操作了哪些子模块,当然也不容易指定权限。我希望的是,比如A组件中使用了b和c这两个子模块的状态,不允许操作其他子模块,那么可以先导入要使用的模块,例如:import{a,b}fromthis.$storeexportdefault{methods:{test(){alert(a.state.uname)//访问状态a.commit('setName')//修改状态}}}这样,按模块导入,查询和使用更清晰。下一步我们详细介绍了状态管理的背景和Vuex的使用,并分享了我们对官方API的思考。相信看到这里,你已经对状态管理和Vuex有了更深入的了解和理解。但是,在这篇文章中,我们只介绍了Vuex的方案,其他的状态管理方案,以及我们上面的抱怨。能否找到更好的实现方式,等待我们去尝试。在下一篇文章中,我们将继续深挖状态管理,比较Vuex和React,Fluter在状态管理实现上的差异,然后在Vue上集成Mobx来打造我们优雅的应用。以往本专栏会长期输出前端工程和架构方向的文章,现刊发如下:前端架构师的git技能你有多好?前端架构师的神技,统一代码风格的三种方式如果喜欢我的文章,请点赞支持!也欢迎关注我的专栏。免责声明:本文为原创,如需转载请微信联系ruidoc获得授权。