对于“前端状态”相关问题,如何思考比较全面

开发 前端
有相当比例的前端从业者入行是从「学习前端框架的使用」开始的。换言之,在他们的知识体系中,最底层是「前端框架如何使用」,其他业务知识都是构建于此之上。

大家好,我卡颂。

最近看到个写得很不错的知乎回答Hooks是否过誉了?前端应该跟着React走还是跟着JS、TS走?- beeplin的回答[1]

在这个回答的基础上,我想引申出一个问题 —— 对于「前端状态」相关问题,如何思考比较全面?

今天,我们试着从多个抽象层级的角度回答这个问题。

问题的起源

有相当比例的前端从业者入行是从「学习前端框架的使用」开始的。换言之,在他们的知识体系中,最底层是「前端框架如何使用」,其他业务知识都是构建于此之上。

要以此为基础回答「前端状态」相关问题,并不容易。就比如你问组长:

  • 为什么项目中用Redux而不用Mobx?
  • 为什么要用Hooks而不用ClassComponent?

很多时候得到的是一个既定的事实(就是这样,没有为什么),而不是分析后的结果。

要分析这类问题,我们需要知道一些更低抽象层级的知识。

几乎所有主流前端框架的实现原理,都在践行UI = f(state)这个公式,通俗的说 —— 「UI是对状态的映射」。

这应该是「前端状态」会出现的最低抽象层级了,所以我们从这个层级出发。

前端框架的实现原理

限于篇幅有限,这里我们以最常见的React与Vue举例。

在实现「UI是对状态的映射」过程中,两者的方向不同。

React​并不关心状态如何变化。每当调用更新状态的方法(比如this.setState​,或者useState dispatch​...),就会对整个应用进行diff。

所以在React中,传递给「更新状态的方法」的,是「状态的快照」,换言之,是个「不可变的数据」。

Vue​关心状态如何变化。每当更新状态时,都会对「与状态关联的组件」进行diff。

所以在Vue中,是直接改变状态的值。换言之,状态是个「可变的数据」。

这种底层实现的区别在单独使用框架时不会有很大区别,但是会影响上层库的实现(比如状态管理库)。

现在我们知道,通过前端框架,我们可以将状态映射到UI。那么如何管理好对应的映射关系呢?

换言之,如何将状态与「和他相关的UI」约束在一起?

我们再往更高一级抽象看。

如何封装组件

前端开发普遍采用「组件」作为「状态与UI的松散耦合单元」。

到这里我们可以发现,如果仅仅会使用前端框架,那么只能将组件看作是「前端框架中既定的设计」。

但如果从更低一层抽象(前端框架的实现原理)出发,就能发现 —— 组件是为了解决框架实现原理中「UI到状态的映射」的途径。

那么组件该如何实现,他的载体是什么呢?从软件工程的角度出发,有两个方向可以探索:

  • 面向对象编程
  • 函数式编程

「面向对象编程」的特点包括:

  • 继承
  • 封装
  • 多态

其中「封装」这一特点使得「面向对象编程」很自然成为组件的首选实现方式,毕竟组件的本质就是「将状态与UI封装在一起的松散耦合单元」。

React的ClassComponent,Vue的Options API都是类似实现。

但毕竟组件的本质是「状态与UI的松散耦合单元」,在考虑复用性时,不仅要考虑「逻辑的复用」(逻辑是指操作状态的业务代码),还要考虑「UI的复用」。所以「面向对象编程」的另两个特性并不适用于组件。

框架们根据自身特点,在「类面向对象编程」的组件实现上,拓展了复用性:

  • React​通过HOC、renderProps
  • Vue2​通过mixin

经过长期实践,框架们逐渐发现 —— 「类面向对象编程的组件实现」中「封装」带来的好处不足以抵消「复用性」上的劣势。

于是React​引入了Hooks​,以函数作为组件封装的载体,借用「函数式编程」的理念提高复用性。类似的还有Vue3​中的Composition API。

不管是ClassComponent​还是FunctionComponent​、Options API​还是Composition API,他们的本质都是「状态与UI的松散耦合单元」。

当组件数量增多,逻辑变复杂时,一种常见的解耦方式是 —— 将可复用的逻辑从组件中抽离出来,放到单独的Model​层。UI​直接调用Model层的方法。

对Model层的管理,也就是所谓的「状态管理」。

对状态的管理,是比组件中「状态与UI的耦合」更高一级的抽象。

状态管理问题

​状态管理」要考虑的最基本的问题是 —— 如何与框架实现原理尽可能契合?

比如,我们要设计一个User Model​,如果用class的形式书写:

class User {
name: String;
constructor(name: string) {
this.name = name;
}
changeName(name: string) {
return this.name = name;
}
}

只需要将这个Model的实例包装为响应式对象,就能很方便的接入Vue3:

import { reactive } from 'vue'

setup() {
const user = reactive(new User('KaSong') as User;
return () (
<button onClick={() => user.changeName('XiaoMing')}>
{user.name}
</button>
)
}

之所以这么方便,诚如本文开篇提到的 —— Vue​的实现原理中,状态是「可变的数据」,这与User Model的用法是契合的。

同样的User Model​要接入React​则比较困难,因为React原生支持的是「不可变数据」类型的状态。

要接入React​,我们可以将同样的User Model​设计为不可变数据,采用reducer的形式书写:

const userModel = {
name: 'KaSong'
};

const userReducer = (state, action) => {
switch (action.type) {
case "changeName":
const name = action.payload;
return {...state, name}
}
};

function App() {
const [user, dispatch] = useReducer(userReducer, userModel);

const changeName = (name) => {
dispatch({type: "changeName", payload: name});
};

return (
<button onClick={() => changeName('XiaoMing')}>
{user.name}
</button>
);
}

如果一定要接入「可变类型状态」,可以为React​提供类似Vue​的「响应式更新」能力后再接入。比如借用Mobx提供的响应式能力:

import { makeAutoObservable } from "mobx"

function createUser(name) {
return makeAutoObservable(new User(name));
}

到目前为止,不管是「可变类型状态」还是「不可变类型状态」的Model,都带来了「从组件中抽离逻辑」的能力,对于上例来说:

  • 「可变类型状态」将状态与逻辑抽离到User中
  • 「不可变类型状态」将状态与逻辑抽离到userModel与userReducer
  • 最终暴露给UI的都仅仅是changeName方法

当业务进一步复杂,Model本身需要更完善的架构,此时又是更高一级的抽象。

到这一层时已经脱离前端框架的范畴,上升到纯状态的管理,比如为mobx​带来结构化数据的mobx-state-tree。

此时框架实现原理对Model​的影响已经在更高的抽象中被抹去了,比如Redux-toolkit​是React​技术栈的解决方案,Vuex​是Vue技术栈的解决方案,但他们在使用方式上是类似的。

这是因为Redux与Vuex的理念都借鉴自Flux,即使React与Vue在实现原理上有区别,但这些区别都被状态管理方案抹平了。

更高的抽象

在此之上,对于状态还有没有更高的抽象呢?答案是肯定的。

对于常规的状态管理方案,根据用途不同,可以划分出更多细分领域,比如:

  • 对于表单状态,收敛到表单状态管理库中。
  • 对于服务端缓存,收敛到服务端状态管理库中(React Query、SWR)。
  • 用完整的框架收敛前后端Model,比如Remix、Next.js。

总结

回到我们开篇提到的问题:

  • 为什么项目中用Redux而不用Mobx?
  • 为什么要用Hooks而不用ClassComponent?

现在我们已经能清晰的知道这两个问题的相同点与不同点:

  • 相同点:都与状态相关。
  • 不同点:属于不同抽象层级的状态相关问题。

要回答这些问题需要哪些知识呢?只需要知道问题涉及的「状态的抽象层级」,以及「比该层级更低的抽象层级」对应的知识即可。

比如回答:为什么项目中用Redux​而不用Mobx?

考虑当前抽象层级

Redux与Mobx​都属于Model​的实现,前者带来一套「类Flux的状态管理理念」,后者为React​带来「响应式更新」能力,在设计Model时我的项目更适合哪种类型?

或者两种类型我都不在乎,那么要不要使用更高抽象的解决方案(比如MST、Redux Toolkit)抹平这些差异?

考虑低一级抽象层级

项目用的ClassComponent还是FunctionComponent?Redux、Mobx与他们结合使用时哪个组合更能协调好UI与逻辑的松散耦合?

考虑再低一级抽象层级

React​的实现原理决定了他原生与「不可变类型状态」更亲和。Redux​更契合「不可变数据」,Mobx更契合「可变数据」。我的项目需要考虑这些差异么?

当了解不同抽象层级需要考虑的问题后,任何宽泛的、状态相关问题都能转化成具体的、多抽象层级问题。

从不同抽象层级出发思考,就能更全面的回答问题。

参考资料

[1]Hooks是否过誉了?前端应该跟着React走还是跟着JS、TS走?- beeplin的回答:https://www.zhihu.com/question/468249924/answer/1968728853。

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2017-07-10 14:53:35

前端开发MVVM模式有限状态机

2021-04-29 09:31:05

前端开发技术

2019-12-16 08:00:00

ReactAngularVue

2009-12-03 18:09:51

Visual Stud

2010-09-10 15:18:28

SOAP协议

2009-06-25 09:50:32

JSF

2021-05-12 06:28:09

AI人工智能

2020-12-23 07:56:40

前端UICSS

2009-07-15 16:39:51

AWT和Swing

2009-06-26 14:37:10

EJB和Spring

2009-07-14 16:30:41

Swing与SWT

2009-06-24 16:16:30

JSF和Tapestr

2011-08-01 10:37:29

软件项目管理

2009-12-22 15:08:46

ADO控件

2020-07-07 07:00:00

RustGo语言编程语言

2009-08-18 10:24:03

Java开发工具

2009-08-11 14:57:11

比较C#和Java

2009-10-28 13:27:11

2009-12-28 17:01:31

2024-05-22 10:03:59

点赞
收藏

51CTO技术栈公众号