大家好,我是Kason。最近看到一个写的很好的知乎回答。Hooks被高估了吗?前端应该跟React还是跟JS、TS?-beeplin的回答[1]。在这个回答的基础上,我想提出一个问题——如何更全面地思考与“前端身份”相关的问题?今天,我们尝试从多个抽象层次来回答这个问题。问题的根源相当一部分前端从业者是从“学习前端框架的使用”开始的。也就是说,在他们的知识体系中,最底层是“如何使用前端框架”,其他的业务知识都是建立在这个基础上的。在此基础上回答“前端身份”相关的问题并不容易。比如你问组长:为什么在项目中使用Redux而不是Mobx?为什么使用Hooks而不是ClassComponent?很多时候得到的是一个既定的事实(就是这样,没有为什么),而不是分析的结果。分析这类问题,需要了解一些较低抽象层次的知识。几乎所有主流前端框架的实现原理都在践行UI=f(state)这个公式。用外行的话来说——“用户界面是状态的映射”。这应该是会出现“前端状态”的最低抽象层次,所以我们从这个层次开始。前端框架的实现原理限于篇幅。这里我们以最常见的React和Vue为例。在实现“UI是状态的映射”的过程中,两者的方向是不同的。React不关心状态如何变化。每当调用更新状态的方法(例如this.setState或useStatedispatch...)时,整个应用程序将被diff。所以在React中,传递给“更新状态的方法”的是“状态快照”,换句话说,是“不可变数据”。Vue关心状态如何变化。每当状态更新时,“与状态关联的组件”将被区分。所以在Vue中,直接改变状态的是值。换句话说,状态是“可变数据”。这种底层实现的差异在单独使用框架时不会有太大的区别,但是会影响上层库(比如状态管理库)的实现。现在我们知道使用前端框架,我们可以将状态映射到UI。那么如何管理对应的映射关系呢?或者说,如何用“与他相关的UI”来约束state?让我们看看更高层次的抽象。如何封装组件前端开发一般将“组件”作为“松散耦合的状态和UI单元”。至此我们可以发现,如果只使用前端框架,只能把组件看做是“前端框架中既定的设计”。但是如果从更底层的抽象(前端框架的实现原理)出发,就会发现组件是解决框架实现原理中“UI-to-statemapping”的方式。那么组件如何实现,它的载体是什么?从软件工程的角度来看,有两个方向可以探索:面向对象编程函数式编程“面向对象编程”的特点包括:继承、封装和多态其中,“封装”的特点使得“面向对象编程”成为组件自然而然的首选实现方式,毕竟组件的本质是“将状态和UI封装在一起的松耦合单元”。React的ClassComponent和Vue的OptionsAPI是类似的实现。但毕竟组件的本质是“松耦合的状态和UI单元”。use”。所以“面向对象编程”的另外两个特性不适用于组件。框架根据自身特点,在“类面向对象编程”组件的实现上扩展了可重用性:通过HOC实现React,renderPropsVue2throughmixin经过长期的实践,框架们逐渐发现“类对象编程组件”在“实现”上“封装”的好处不足以抵消“可重用性”的缺点。于是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的原理,state是“可变数据”,这与UserModel的用法是一致的,同一个UserModel连接到React比较困难,因为React原生支持“immutableData”类型的状态。访问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));}至此无论是“可变类型状态”还是“不可变”“类型状态”的Model带来了“逻辑与组件分离”的能力,从逻辑上分离到userModel和userReducer,最终暴露给UI的只有changeName方法,当业务越来越复杂时,Model本身需要一个更完整的架构,此时是更高的抽象层次,在这个层次上,已经脱离了前端框架的范围,上升到了纯状态管理,比如mobx-state-tree,它为mobx带来了结构化数据。此时框架实现原理对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的时候,我的project选择哪种类型比较合适?或者我不关心这两种类型,那么我是否应该使用更高抽象的解决方案(例如MST,ReduxToolkit)来消除这些差异?考虑将ClassComponent或FunctionComponent用于较低级别的抽象级别项目?当Redux、Mobx和它们组合使用时,哪种组合更能协调UI和逻辑的松耦合?考虑到React在较低抽象层次上的实现原理,确定其原生与“不可变类型状态”更加兼容。Redux更适合“不可变数据”,Mobx更适合“可变数据”。我的项目是否需要考虑这些差异?当理解了需要在不同抽象层次上考虑的问题时,任何广泛的、与状态相关的问题都可以转化为具体的、多层次的抽象问题。从不同的抽象层次思考,可以更全面地回答问题。参考文献[1]Hooks被高估了吗?前端应该跟React还是跟JS、TS?-beeplin的回答:https://www.zhihu.com/question/468249924/answer/1968728853。