大家好,我卡颂。
长期使用React
的同学应该知道,React
中存在两种组件:
-
Class Component
,类组件 -
Function Component
,函数组件
既然提到类 和函数,那么很自然的,我们会进一步思考:
-
类组件和
OOP
(面向对象编程)有关系么? -
函数组件和
FP
(函数式编程)有关系么?
毕竟,如果类组件和OOP
有关,那么OOP
中的思想(继承、封装、多态...)也能指导类组件的业务开发(函数组件与FP
的关系同理)。换言之,我们可以直接用这些编程范式的最佳实践指导React
项目开发。
那么,函数组件 和函数式编程究竟是什么关系呢?本文会围绕这个话题展开讲解。
欢迎围观朋友圈、加入人类高质量前端交流群,带飞
编程范式与DSL
首先,我们应该明确,框架语法 本质是一种DSL
(领域相关语言),他是为了某个特定领域的开发量身定制的。
比如,React
作为一款针对view开发 的DSL
,虽然不同的view
使用的框架不同,比如:
-
对于
web
,框架为ReactDOM
-
对于小程序,框架为
Taro
-
对于原生开发,字节内部有个叫
React Lynx
的框架
但这些框架都大体遵循同一套DSL
(React
语法),这套DSL
并不属于某一种编程范式,而应该被视为不同编程范式中,更符合view开发的语言特性的集合。
所以,作为React DSL
的一部分,函数组件可以体现OOP
的思想,类组件也能体现FP
的思想。只要这些思想有利于view开发 ,就可以纳入DSL
的语法中。
比如,下面的函数组件Header
,是由WelcomeMessage
与LogoutButton
组件组合而成,这是OOP
中的组合优于继承思想:
js
function Header(props) {
return (
<div>
<WelcomeMessage name={props.name} />
<LogoutButton onClick={props.onLogout} />
</div>
);
}
再比如,下面的类组件Cpn
中,要改变状态count
,并不是通过突变(类似this.state.count++
),而是调用this.setState
,传入不可变数据:
js
class Cpn extends React.Component {
// ...
onClick() {
const count = this.state.count;
this.setState({count: count + 1});
}
render() {
// ...
}
}
使用不可变数据 属于FP
中的思想。
所以,当我们要深入了解某个React
特性时,应该以如下顺序递进的思考:
-
React
的开发理念是什么? -
为了实现这套理念,吸收了哪些编程范式中的思想
-
这些思想如何在
React
中落地
如果我们用上述思考过程研究函数组件与函数式编程的关系,会发现:
-
函数组件属于落地的产物(上述思考的第三步)
-
函数式编程属于编程范式(上述思考的第二步)
这就是两者的关系 ------ 函数组件属于多种编程范式(主要是OOP
与FP
)在React
中最终的落地产物,其中借鉴了一部分FP
的思想。
我们不应该将函数组件单纯视为FP
在React
中的具象体现。
那么,函数组件究竟是如何演进而来的呢?
函数组件的演进
让我们按照上述三步演进顺序思考。首先,React
的开发理念践行了如下公式(即:UI
是数据快照经过函数映射而来):
js
UI = fn(snapshot)
要落地这个理念,有两个要素需要实现:
-
数据快照
-
函数映射
在这里,FP
中不可变数据 更适合作为数据快照 的载体,所以React
中状态是不可变的,因为状态的本质是快照。
而函数映射 的载体则没有特殊要求。在React
中,每次触发更新,所有组件都会重新render
,render
的过程就是函数映射 的过程,输入是props
与state
,输出是JSX
。
与React
相对的,Vue
中组件则更符合OOP
的理念,考虑如下App
组件:
js
const App = {
setup(initialProps) {
const count = reactive({count: 0})
const add = () => { count.value++ }
return {count, add}
}
template: "...省略"
}
组件的setup
方法只会在初始化时执行一次,后续触发更新时操作的都是同一个闭包中的数据。这里面的闭包就是OOP
思想中的实例。
既然React
对函数映射的载体没有特殊要求,那么类组件、函数组件都是可以的。
那为什么函数组件最终替代了类组件成为React
开发的主流呢?很多同学认为函数组件的Hooks可以更好的复用逻辑这一点,是函数组件优于类组件的主要原因。
但实际上,基于装饰器的类开发模式早已被验证是优秀的逻辑复用模式,类组件配合TS
装饰器的模式是行得通的。
主要原因还是 ------ 函数组件能够更好的落地UI = fn(snapshot)
这一理念。
刚才说过,公式中的snapshot
是快照 的含义。在React
中,快照主要包括三类数据:
-
state
-
props
-
context
对于同一个组件,根据公式UI = fn(snapshot)
,相同的快照输入应该获得相同输出(JSX
)。
但状态更新也可能触发副作用 ,比如请求数据、操作DOM
...
在类组件中,这些副作用 逻辑被分散在各个生命周期钩子函数中,React
无法掌控。
而在函数组件中:
-
副作用受限在
useEffect
中。每次render
,React
都会保证上次的副作用效果已经被清除(通过useEffect
回调的返回值函数) -
ref
的传播也需要借由forwardRef
,这进一步限制了ref
可能的影响范围 -
数据请求的副作用被交给
Suspense
处理,考虑下面组件:
js
function UserList({id}) {
// 异步请求数据
const data = use(fetchUser(id));
// ...
}
使用时:
html
<Suspense fallback={<div>加载中...</div>}>
<UserList id={1}/>
</Suspense>
总而言之,使用函数组件时,所有副作用都处于一种受到管控 的状态,可以尽可能保证每次更新时相同的快照输入,获得相同的JSX输出 ,所以函数组件在React
中才会发扬光大。
同时,这也契合了FP
中的纯函数思想。
总结
函数组件 并不是函数式编程 在React
中的具体实现,而是React
的设计理念UI = fn(snapshot)
落地的最好载体。
在React
中,还吸收了其他编程范式中的优秀思想。FP
只是其中影响React
最深的一种。毕竟,一切落地都是为了践行最初的设计理念。