当前位置: 首页 > Web前端 > vue.js

关于“前端身份”相关的问题,如何思考更全面

时间:2023-03-31 14:45:37 vue.js

大家好,我是Kason。最近看到一个写的很好的知乎回答。Hooks被高估了吗?前端应该跟React还是跟JS、TS?-beeplin的回答。在这个回答的基础上,我想提出一个问题——如何更全面地思考前端身份相关的问题?今天,我们尝试从多个抽象层次来回答这个问题。欢迎加入人类优质前端框架群。飞车问题的起源有相当一部分前端从业者是从学习前端框架的使用开始的。也就是说,在他们的知识体系中,最底层是如何使用前端框架,其他的业务知识都是建立在其之上的。在此基础上回答有关前端状态的问题并不容易。比如你问组长:为什么在项目中使用Redux而不是Mobx?为什么使用Hooks而不是ClassComponent?很多时候得到的是一个既定的事实(就是这样,没有为什么),而不是分析的结果。分析这类问题,需要了解一些较低抽象层次的知识。几乎所有主流前端框架的实现原理都在践行UI=f(state)这个公式。通俗地说——UI是状态的映射。这应该是前端状态会出现的最低抽象层次,所以我们从这个层次开始。前端框架的实现原理限于篇幅。这里我们以最常见的React和Vue为例。在实现UI是状态映射的过程中,两个方向是不同的。React不关心状态如何变化。每当调用更新状态的方法(例如this.setState或useStatedispatch...)时,整个应用程序都会被diff。所以在React中,传递给更新状态的方法的是状态的快照,换句话说,是不可变数据。Vue关心状态如何变化。每当更新状态时,与状态相关联的组件就会被区分。所以在Vue中,直接改变状态的是值。换句话说,状态是可变数据。这种底层实现的差异在单独使用框架时不会有太大的区别,但是会影响上层库(比如状态管理库)的实现。现在我们知道使用前端框架,我们可以将状态映射到UI。那么如何管理对应的映射关系呢?换句话说,您如何将状态与其关联的UI联系起来?让我们看看更高层次的抽象。如何封装组件前端开发一般将组件作为状态和UI的松散耦合单元。至此我们可以发现,如果只使用前端框架,只能将组件视为前端框架中既定的设计。但是如果从更底层的抽象(前端框架的实现原理)出发,你会发现组件是框架实现原理中解决UI到状态映射的方式。那么组件如何实现,它的载体是什么?从软件工程的角度来看,有两个方向可以探索:面向对象编程函数式编程面向对象编程的特点包括:继承、封装和多态。封装的特性使得面向对象编程自然而然地成为了组件的首选实现方式。组件的本质是一个松耦合的单元,封装了状态和UI。React的ClassComponent和Vue的OptionsAPI是类似的实现。但毕竟组件的本质是松耦合的状态和UI单元。在考虑复用性的时候,不仅要考虑逻辑的复用(逻辑是指运行状态的业务代码),还要考虑UI的复用。所以面向对象编程的另外两个特性不适用于组件。框架根据自身特点,在类面向对象编程组件的实现上扩展了可重用性:React通过HOC,renderPropsVue2通过mixin经过长期的实践,框架逐渐发现在类面向对象编程中的封装组件实现带来的好处不足以抵消可重用性的缺点。于是React引入了Hooks,以函数作为组件封装的载体,借用函数式编程的理念来提高复用性。类似的还有Vue3中的CompositionAPI。无论是ClassComponent还是FunctionComponent,OptionsAPI还是CompositionAPI,它们的本质都是松耦合的状态和UI单元。当组件数量增加,逻辑变得复杂时,一种常见的解耦方法是从组件中提取可重用的逻辑,并将它们放在单独的Model层中。UI直接调用Model层的方法。Model层的管理也称为状态管理。状态的管理比组件中状态和UI的耦合更高层次的抽象。状态管理问题状态管理要考虑的最基本的问题是——如何尽可能匹配框架实现原则?比如我们要设计一个UserModel,如果写成类的形式:classUser{name:String;构造函数(名称:字符串){this.name=name;}changeName(name:string){returnthis.name=name;}}你只需要将这个Model实例包装成一个响应式对象,就可以轻松访问Vue3:import{reactive}from'vue'setup(){constuser=reactive(newUser('KaSong')asUser;return()=>(user.changeName('XiaoMing')}>{user.name})}就是这么方便,就像本文开头提到的——在Vue的实现原理,状态是可变数据,这与UserModel的用法是一致的,同一个UserModel连接到React会比较困难,因为React原生支持不可变数据类型的状态。访问React,我们可以设计和不可变数据一样的UserModel,写成reducer的形式:constuserModel={name:'KaSong'};constuserReducer=(state,action)=>{switch(action.type){case"changeName":constname=action.payload;返回{...state,name}}};functionApp(){const[user,dispatch]=useReducer(userReducer,userModel);constchangeName=(name)=>{dispatch({type:"changeName",payload:name});};return(changeName('XiaoMing')}>{user.name});}如果必须访问变量类型状态,可以为React提供类似响应式更新的能力在访问它之前到Vue。比如借用Mobx提供的响应能力:import{makeAutoObservable}from"mobx"functioncreateUser(name){returnmakeAutoObservable(newUser(name));}模型,都带来了从组件中提取逻辑的能力。对于上面的例子:可变类型state将状态和逻辑分离为User,不可变类型state将状态和逻辑分离为userModel和userReducer,最终暴露出来。只有changeName方法被提供给UI。当业务进一步复杂化,Model本身需要更完整的架构时,这时候就是更高层次的抽象。在这个层面上,它已经脱离了前端框架的范畴,上升到了纯粹的状态管理,比如给mobx带来结构化数据的mobx-state-tree。此时,框架实现原理对Model的影响已经在更高的抽象中抹去了。比如Redux-toolkit是React技术栈的解决方案,Vuex是Vue技术栈的解决方案,但是它们在用法上是相似的。的。这是因为Redux和Vuex的概念是从Flux借来的。尽管React和Vue在实现原理上存在差异,但这些差异被状态管理方案所平滑。更高的抽象在这之上,状态有没有更高的抽象?答案是肯定的。对于常规的状态管理方案,根据不同的用途,可以划分出更多的细分,例如:对于表单状态,汇聚到表单状态管理库;对于服务器缓存,汇聚到服务器状态管理库(ReactQuery,SWR)使用完整的框架汇聚前后端模型,例如Remix和Next.js。总结一下,回到我们一开始提到的问题:为什么在项目中使用Redux而不是Mobx?为什么使用Hooks而不是ClassComponent?现在我们可以清楚地知道这两个问题的相同点和不同点:相同点:都与状态相关不同点:状态相关的问题属于不同的抽象层次,需要什么样的知识来回答这些问题?只需要知道问题涉及的状态的抽象层次,以及低于这个层次的抽象层次对应的知识即可。比如回答:为什么在项目中使用Redux而不使用Mobx?考虑到目前的抽象层次Redux和Mobx都属于Model的实现,前者带来了一套类似Flux的状态管理理念,后者为React带来了响应式更新能力。设计Model时哪种类型更适合我的项目?或者我不关心这两种类型,那么我是否应该使用更高抽象的解决方案(例如MST,ReduxToolkit)来消除这些差异?考虑将ClassComponent或FunctionComponent用于较低级别的抽象级别项目?当Redux、Mobx和它们组合使用时,哪种组合更能协调UI和逻辑的松耦合?考虑到较低抽象层次的实现原则,React决定其原生和不可变类型状态更加兼容。Redux更适合不可变数据,Mobx更适合可变数据。我的项目是否需要考虑这些差异?当理解了需要在不同抽象层次上考虑的问题时,任何广泛的、与状态相关的问题都可以转化为具体的、多层次的抽象问题。从不同的抽象层次思考,可以更全面地回答问题。