,各种前端状态管理的工具库层出不穷。在开始一个新项目时,人们不禁要问,我应该使用哪一个?其实每个都能达到我的目的。我们想要的是对系统中的状态进行管理,让代码易于维护和扩展,尽可能的降低系统的复杂度。使用Vue的同学可能更愿意相信它的官方生态,直接上vuex/pinia,没有太多的纠结。由于平时React用的比较多,所以就拿目前广泛使用的Redux和Mobx的工具库作为例子,研究了一下,记录下自己的一些八卦。注:以下不会涉及各个库的具体用法,多是讨论各自的设计理念和推荐模式(patterns),提前说明,以免耽误大家的时间。Redux和Mobx或多或少都借鉴了Flux的概念。比如大家经常听到的“单向数据流”的原理,最早是被Flux带入前端领域的,所以先说说Flux。FluxFlux是facebook团队引入的一个架构概念,并给出了代码实现。Flux为何诞生?Facebook一开始使用传统的MVC范式进行系统开发,但随着业务逻辑越来越复杂,代码中添加新功能的难度也越来越大。很多状态耦合在一起,状态的处理也耦合在一起,FB团队抱怨MVC最多的两点:Controller的集中化不利于扩展。核心是Controller需要处理Model变化的大量复杂逻辑。对模型的更改可能来自各个方向。可能是开发者自己想要对Model进行更改,可能是View上的回调想要对Model进行更改,也可能是一个Model的更改触发了另一个Model的更改。我们可以大致总结出基于MVC的数据流有三种类型:Controller->Model->ViewController->Model->View->Model->View...(loop)Controller->Model1->Model2->View1->view2...而这三个数据流在实际业务中很可能会交织在一起。为了改善MVC在复杂应用中的上述缺陷,降低系统的整体复杂度,FB团队推出了Flux架构,结合React重构了他们的代码,这就是Flux架构诞生的原因。Flux具体是什么?Flux由几部分组成:StoreActionViewDispatcherStore是存放Model的地方,View和MVC的View一样,Action可以理解为操作Model的行为,Dispatcher可以理解为寻找Store的分发执行对应Action,执行操作By。Dispatcher是Flux的核心,统一了Model的运行。在Flux中,Dispatcher和View可以看作是Model唯一的输入输出。那么与MVC相比有哪些变化或者好处呢?首先,数据流是统一的。谁要操作模型,就必须通过调度器。同时,dispatcher配合Action,相当于同意了Model的几个有限且独立的操作。相对于MVC中集中Controller中运行大量复杂的Model逻辑,Flux中将其抽象分离为Actions。所以state在整个系统中的流向是:Action->dispatcher->Model->View->Action->dispatcher->Model->View...这就是所谓的“单向数据流””。与MVC中多个数据流的交叉相比,单向数据流显着降低了系统的复杂度。并且由于Action为Model指定了有限的操作,开发者可以根据Action的输入知道会发生什么变化,提高了代码的可预测性。基于MVC范式的代码,项目的长期维护者可能知道各个地方的状态变化会触发什么操作,但是如果有新人加入团队,要想弄清楚就需要费一番功夫了出去。如果是基于Flux架构,开发者只需要跟踪一个Action是如何在整个系统中流动的,就知道系统其余部分是如何工作的。因为Flux的数据流是单向的、一致的。比如你可以知道如何通过Action改变state,然后搜索改变的state用在什么地方就知道这里的view会重新渲染。同理,搜索dispatch的action在哪里,你就会知道谁要对状态进行更改。.另外,你也可以结合纯函数的概念来体验Dispatcher的设计。这在Redux的实现中尤为明显。找到Action对应的Store后,只负责根据Action处理Model的变化逻辑,不会改变Input参数或外部变量,同一个输入总是对应同一个Store变化。意思是如果你在后面任意一个时间点做一个Action,得到的变化状态和之前得到的是一致的。这就是所谓“时间旅行”功能的原理。“时间旅行”的本质是记录每一次数据修改。只要每次修改都是无状态的,理论上我们可以通过修改记录来恢复之前任何时候的数据。.结合纯函数的设计至少可以带来两个好处:方便开发者调试。我们可以通过“时间旅行”来重现之前任意时间点的状态,我们可以统一在dispatcher中添加日志,看看什么时候发生了什么变化。基于纯函数的代码更容易写单测ReduxRedux是基于Flux架构理念的函数式实现,并做了一些优化,所以Redux具备Flux架构的所有优点。上图是Redux官网给出的Redux工作原理gif动图。Redux的核心组成简单展示一下:其中Reducer是FluxDispatcher的纯函数式实现,找到Action对应的Model,返回一个变化的对象给Redux,Redux应用Store上的变化。根据上面两张图,可以很明显的感知到Redux的数据流是单向的:action->middleware->reducer->store->view->action->middleware->reducer->store->view...ExplainRedux三大基本原则Redux官方表示,Redux可以用三个基本原则来描述:“单一数据源”、“只读状态”、“使用纯函数进行修改”。“单一数据源”绝对比分散的数据源更有优势,除非各个数据源之间没有联系。但是只要有多个数据源连接,仍然需要通过一定的操作将各个数据源连接起来,这无疑增加了复杂度。“只读状态”表示不允许直接修改Model。必须创建一个action,交给reducer处理,保证Model只有一个输入。这是实践单向数据流的基本要求。“使用纯函数进行修改”是指用户编写的reducer必须是纯函数,方便开发者调试,也便于编写单元测试。此外,另一个需要开发者编写纯函数reducer的特性是Redux提倡的Immutable特性。不可变与可变什么是不可变的,什么是可变的?Immutable的意思是“不可变的”。在编程中,不可变数据指的是一种一旦创建就无法更改的数据结构。它的概念是:当改变时,产生一个与原对象完全一样的新对象,指向不同的内存地址,互不影响。可变的意思是“可变的”。与Immutable相反,MutableData指的是创建后可以直接改变的数据结构。对于JS来说,所有原始类型(Undefined、Null、Boolean、Number、BigInt、String、Symbol)都是Immutable的,但是引用类型的值是Mutable的。举两个例子直观感受一下:例子一:leta={x:1};让b=a;b.x=6;a.x//6Example2:functiondoSomething(x){/*在这里改变x会影响str和obj吗?*/};varstr='astring';varobj={an:'object'};doSomething(str);//基本类型是不可变的,函数得到一个副本doSomething(obj);//对象通过引用传递,在函数doAnotherThing(str,obj)中是可变的;//`str`没有改变,但是`obj`可能已经改变了。js中其实有几种方法可以让值不可变。为了不跑题,大家可以去维基百科进一步阅读。需要注意的是,无论是writeable:false还是Object.freeze还是const,修改/冻结/声明的属性/值只在第一层生效。如果属性/值嵌套了引用类型的值,则需要递归修改/冻结/声明才能达到整体不可变的目的。Immutable和MutableMutable的优缺点其实更明显。开发者可以直接更改Mutable的数据,但是要考虑更改后的副作用。优点:操作方便。缺点:开发者需要知道更改Mutable数据后会产生哪些副作用,以及对其他使用该数据的地方有何影响。Immutable其实和Mutable是相反的,但是它可以在Immutable特性的基础上做一些额外的功能。优点:避免数据变化带来的副作用(多线程语言中所谓的“线程安全”)JS虽然没有多线程的概念,但是有竞态条件的概念。JS中引用类型的值是通过引用传递的。在一个复杂的应用程序中,多个变量指向同一个内存地址。如果多个代码块同时更改引用,就会出现竞争条件。你需要关心这个对象会被哪一方修改,你对它的修改是否会影响其他代码的运行。使用ImmutableData不会导致这个问题——因为每次更新状态时,都会生成一个新对象。状态可追溯由于每次修改都会创建一个新的对象,对象不会被修改,可以保存修改的记录,应用的状态变得可控可追溯。ReduxDevTool和Git这两个可以实现“时间旅行”的工具,都是基于Immutable的哲学。缺点也是相对于Mutable方法:额外的CPU和内存开销需要额外的操作来修改值。虽然生态中有Immutable.js和Immer.js等库可以帮助我们更方便的操作Immutable变化,但这两个缺点也是无法避免的,尽量优化就好。至于用Immutable还是Mutable的方案来写代码,个人觉得还是要具体情况具体说。显然Redux尊重Immutable,并基于此提供了时间穿梭的功能。React推荐开发者使用Immutable,因为React的UI=fn(states)模型中,React对状态使用shallowMerge。如果可变状态不改变引用,React会认为不需要diff,自然不会重新渲染。但是Mobx提倡Mutable,开发者体验非常好。MobxMobx是一个响应式的状态管理库,提倡自动收集依赖和执行副作用。推荐开发者使用Mutable直接改变状态。该框架帮助我们管理(导出)每个Mutable的副作用并实现最佳处理。从上图可以看出,Mobx也实现了单向数据流的概念:Action->State->ComputedValue->Reaction(likerender)->Action...这里引入的新概念是ComputedValue和反应。Mobx有多个商店。ComputedValue可用于连接来自多个数据源的数据,ComputedValue也可用于从同一数据源的多个数据中派生出一个新数据。Reaction是state的所有副作用,可以是render方法,也可以是Mobx自带的autorun,when等。Mobx想要实现的其实是开发者可以自由管理状态,让修改状态的行为简单直接,剩下的交给Mobx。为了实现这个目标,Mobx需要在内部做更多的事情。它的作者MichelWeststrate在推文中解释了Mobx的设计原则,但是有点过于详细了。不熟悉Mobx底层机制的同学可能看不懂。理解。下面,我将根据这条推文,结合对源码的探索,细化一下。有兴趣的可以阅读原文。Mobx源码自荐分析:Mobx源码及设计思路。文章较长,建议找时间静静地一口气看完。对状态变化做出反应总是比对状态变化采取行动更好。其实这和Vue的响应式交付的理念是一样的,都是数据驱动的。再分析一下这句话,“reacting”的意思是state和sideeffects的绑定关系由framework(library)帮你完成,state的变化自动通知给sideeffects,不需要用户手动处理(开发商)。“Takeaction”是在用户知道状态变化时,手动通知副作用更新。至少有一个操作是用户必须做的:在sideeffect中手动订阅状态变化,这至少带来两个缺陷:订阅的冗余性无法保证,可能订阅多了也可能订阅少了,导致应用程序失败。不符合预期的情况。会让业务代码变得更脏,并且不容易组织最小且一致的订阅集。以render为例,如果render中有条件语句:render(){if(dependsonA){returncomponent1;返回取决于B?组件2:组件3;}第一,如果交给用户手动订阅,那肯定只依赖A和B的状态一起订阅。如果订阅较少,则不会发生预期的重新呈现。交给框架处理如何?当然依赖A和B一起订阅也没有错,但是假设依赖A和B在初始化的时候是有值的,我们是不是有必要让render订阅到的状态靠B?不需要,为什么?想一想,如果此时依赖B的状态发生变化,重新渲染的效果会不会不一样?因此,在初始化时订阅所有状态是多余的。如果应用程序很复杂,有很多状态,那么不必要的内存分配就会更多,这会降低性能。因此,Mobx实现了一种在运行时处理依赖关系的机制,确保将副作用绑定到最小且一致的订阅集。有关源代码,请参阅“getter中有什么?”一章。和Mobx源代码和设计思想中的“处理依赖关系”。导数计算(副作用的执行)的合理性用人的话说就是:消除丢失的计算和多余的计算。丢失计算:Mobx的策略是引入状态机的概念来管理依赖和推导,让数学逻辑可以保证不会丢失任何计算。冗余计算:对于非计算属性状态,引入事务的概念,保证同一批次状态的所有同步变化,对应的状态推导只计算一次。对于计算属性,当计算属性作为推导使用时,当其依赖发生变化时,不会立即重新计算计算属性,而是会等到计算属性本身作为状态绑定推导再次被使用时重新计算其值计算的属性。并且计算相同的值会阻止分叉继续处理。ReduxvsMobx上面分析过,Redux是一个强调对代码进行思考的状态管理库,而Mobx恰恰相反。该框架帮助我们做更多的事情,并且易于使用。总结一下区别:Redux要求开发人员根据其模式编写代码,而Mobx更自由,更易于使用。相对而言,基于Redux开发的系统更加健壮。如果你用了Mobx但是没有很好的管理状态,会让系统更难维护(咦,这个怎么不渲染?哎!这个怎么渲染了那么多次??(转义)。Redux结合了functional和Immutablefeatures提供时间穿越功能,更方便开发者调试和回溯状态。mobx提供了一个全局监控钩子来监控每一个状态的变化和副作用的触发。我们直接打开Log调试,但是肯定不是那么方便相较于Redux,Redux提倡单一Store管理状态,降低状态管理的复杂度。Mobx没有为开发者设置限制,开发者可以以任何形式管理状态,如果多个Store提供ComputedValue作为多个Store数据之间的链接。Mobxframework会帮我们实现最优渲染(sideeffectexecution),而Redux需要我们自己写各种selector或者使用memo来手动优化...以上欢迎大家合理指正并根据证据进行补充。参考:HackerWay:RethinkingWebAppDevelopmentatFacebookBecomingfullyreactive:anin-depthexplanationofMobXImmutableobject
