导言
在阅读本文之前, 建议去阅读Reference部分的头两个: 组合式API常见问答 与 阮一峰React Hooks入门教程。
第一篇是Vue官方的组合式API问答,解释了为什么Vue官方从Vue 3开始更推荐composition API的写法,同时也是入门hooks概念,因为Vue composition API就是借鉴了React Hooks的思想开发而来的。即使之前你完全没有写过Vue 3也值得看一下。此篇为必读。
第二篇是阮一峰老师的React Hooks入门教程,有助于对React hooks扫盲,但如果没有React基础可以先跳过,等看完本文后再来熟悉一下就好。
本文重在基本概念介绍,为接下来精读官方文档作铺垫,这也是下一篇文章要写的内容。
基本概念
为什么Vue 3开始推荐composition API
以下部分由ChatGPT回答:
Vue 3 引入的 Composition API 是一种新的编写组件的方式,相比于 Vue 2 中的 Options API,它提供了更好的逻辑复用和代码组织能力。
以下是 Composition API 的主要优点:
-
逻辑复用和代码组织: 在 Options API 中,我们需要按照选项(如
data
、methods
、computed
等)来组织代码,这在处理复杂组件时可能会导致单个选项代码量过大,难以管理和维护。而 Composition API 允许我们按照逻辑功能来组织代码,使得代码更易于理解和维护。 - 类型推断: Composition API 有更好的 TypeScript 支持和类型推断,这对于大型项目和团队合作来说非常重要。
- 更好的性能: Composition API 由于基于函数,因此可以更好地进行 tree shaking,减少最终的打包体积。
- 更自由的代码组织: Composition API 不再限制你必须在特定的生命周期钩子中进行操作,你可以在 setup 函数中自由地组织代码。
尽管 Composition API 提供了很多优点,但这并不意味着你必须放弃 Options API。两者可以在同一个项目中共存,你可以根据实际需要选择最适合的方式。而且,对于简单的组件,Options API 仍然是一个非常好的选择,因为它的学习曲线更平缓,代码更直观。
函数式编程需要知道的几个概念
不论是Vue 2,还是16.8之前版本的React,推荐写法都是面向对象式的组件。但2019年,16.8版本React Hooks稳定版发布后,hooks逐渐成为React的标准写法。随后2020年Vue 3的发布也将composition API作为推荐写法。至于使用Hook的好处,文章组合式API常见问答已经说的很明白,此处按下不表,但你要知道的是,hook思想或者说composition API的思想,都是函数式编程的思想。本小节对函数式编程当中的几个基本概念进行介绍。
定义及性质
函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算是该语言最重要的基础。而且λ演算的函数可以接受函数作为输入的参数和输出的返回值。 3
上面一段定义似乎有些难懂。如果让AI用更通俗的语言解释一下:
函数式编程就像是在做数学题,你有一些数学函数,你把一些值放进去,然后得到一个结果。这个过程中,你不需要去关心这个函数是怎么运行的,你只需要关心输入和输出。这就是所谓的"将电脑运算视为函数运算"。
"避免使用程序状态以及易变对象",这个可以理解为在函数式编程中,你不会去改变已经存在的东西,比如变量的值。你只会创建新的值。这就像是在做数学题时,你不会去改变一个已经存在的数,你只会通过运算得到一个新的数。
"λ演算是该语言最重要的基础",λ演算是一种数学理论,它是函数式编程的理论基础。你可以把它理解为一种规则或者方法,告诉我们如何用函数来做计算。
"函数可以接受函数作为输入的参数和输出的返回值",这个意思是在函数式编程中,函数不仅可以接受普通的值(比如数字,字符串等)作为参数,还可以接受其他函数作为参数。同样,函数的返回值也可以是一个函数。这就像是在数学中,一个函数可以接受另一个函数作为参数,比如复合函数。
如果依然有些懵,那么只需要了解如下性质 4 :
- 函数是第一等公民(first class)。这条性质是指指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。这条是最为重要的性质
- 只有表达式 (expression), 而没有语句 (statement)。"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
-
没有边际作用 / 副作用 (side effects)。大部分情况下
side effects
的翻译都是副作用,但本人觉得这个翻译虽然“信”,但没有达到“达雅”的水平,因为它很容易让人联想到破坏性的“负作用”,因此本人将其译为“边际作用”。日后文章中“边际作用”或“副作用”为同一词。边际作用指的是函数运算以外的结果,如:修改了外部变量,每次输入输出结果不一致等等...这个概念在刚接触时较为难理解,下一小节单独解释。与“边际作用”对应的概念是“纯函数”。 - 引用透明,指的是任何时候只要参数相同,函数运算所得到的返回值总是相同的。这也是纯函数的概念。
阮一峰老师的文章中还提到了不修改状态这一条性质 4,它实质上与没有边际作用是同一性质,所以没有单独列出。
纯函数与边际作用
上一部分简单介绍了函数式编程的定义与性质。如果你的感受是“根本做不到”,那说明你的感受是正确的,因为现在你我使用的编程语言都不是纯粹的函数式编程,也完全不可能不涉及到修改系统变量等操作。纯粹的函数式编程语言如haskell
, lisp
等语言现在相对比较小众,这里不是讨论的范围。即使像Python
, JavaScript
等无法以纯函数式编程思想编程的语言,我们也可以借鉴其思想。
其中,最重要的思想是纯函数与边际作用。
纯函数的定义与上一小节中引用透明有相同之处,即任何时候只要参数相同,函数运算所得到的返回值总是相同的。但比引用透明更进一步的是,不能有任何边际作用,即修改了外部变量等与本次运算无关的行为。
下面举几个例子:
def my_sqrt(x: int) -> int:
return x * x
上面这个函数中, 只要输出1,必然得到1,只要输出2,必然得到4。无论输入多少次1,多少次2,得出的结果都是一样的。这就叫纯函数。
再举一个非纯函数的例子:
import random
def get_a_number(x: int):
# x 乘以一个10到19的随机整数
return x * random.randint(10, 20)
上面这个函数,只要输入的不是0,你无法保证上一次的输出结果与本次输出结果一样。比如上次 输入10是,输出10 * 10,本次输出的是10 * 12,参数相同,运算两次得出了不同的结果,那么它就不是纯函数。
再举一例:
a = 0
def my_sqrt(x: int) -> int:
global a
a += 1
return x ** 2
虽然上述函数可以保证相同的输入总能得到相同的输出,但每运算一次,a便累加1,函数运算过程修改了外部变量的值,这就叫有副作用(有边际作用)。
由此便得出了边际作用的定义:函数在执行过程中,除了返回函数值之外,还影响或改变了外部的状态。换句话说,如果一个函数除了产生输出之外,还与外部世界有其他的交互,那么我们就说这个函数有副作用。常见的边际作用包括:
1. 改变全局变量或数据结构
如果一个函数改变了一个全局变量或者一个外部的数据结构,那么这就是一个副作用。例如上面的代码就是这样。
2. 写入数据库或文件
如果一个函数在执行过程中,写入了数据库或者文件,那么这也是一个副作用。因为这改变了外部的状态。
3. 打印输出或记录日志
如果一个函数在执行过程中,打印了输出或者记录了日志,那么这也是一个副作用。虽然这种副作用可能看起来无害,但是它改变了外部世界的状态(例如,改变了控制台的输出或者日志文件的内容)。
4. 网络请求
如果一个函数在执行过程中,发出了一个网络请求(例如,向一个API发送了一个请求),那么这也是一个副作用。因为这改变了网络的状态。
那么,什么是hooks呢
在hooks正式发布之前,React和Vue就存在函数式组件。鉴于假设看官可能仍然是对Vue更熟悉的,那么就用Vue中的函数式组件来说5:
<template functional>
<div>
<h1>{{ props.title }}</h1>
</div>
</template>
<script>
export default {
name: 'FunOne',
props: {
title: [String],
},
}
</script>
根据官方文档,我们可以知道函数式组件是无状态、无实例6的。换句话说,它只接收props
并渲染它们,只起渲染作用,不处理事件,不接受响应式数据,没有生命周期函数,它只是一个函数。
同理,16.8之前的React也只起渲染作用,没有生命周期方法,也无法使用state
。如下:
interface Props {
title: string;
}
function FuncComponent({ title }: Props) {
return <h1>{title}</h1>
}
随着16.8版本的发布,在函数式组件中使用state
等成为了可能,所以可以认为16.8是对React固有的函数式组件增强,函数式组件拥有了处理事件、状态等的能力。现在我们一提react hooks
默认指的是16.8+版本的react。随后, Vue跟进了这一进程,2020年发布的Vue 3就更提倡大家使用思想类似的composition API
,于2022年Vue 3.2版本<script setup>
语法糖发布后,composition API
成为了默认写法。
下面分别用Vue 3和React实现一个累加:
<script setup lang="ts">
// Vue 3
import { ref } from 'vue';
const showValue = ref(0);
function addFunc() {
showValue.value++;
}
</script>
<template>
<div>
<p>{{ addValue }}</p>
<input type="button" value="+1" @click="addFunc" />
</div>
</template>
// React hooks
import React, { useState } from 'react';
function AddOne() {
const [showValue, setShowValue] = useState(0);
function addFunc() {
setShowValue(showValue + 1);
}
return (
<div>
<p>{showValue}</p>
<input value="+1" type="button" onClick={addFunc} />
</div>
);
}
可以认为,hooks其实就是针对函数式组件的增强,让函数式组件具有了处理state
及事件等的能力。但是,对于React
来说,函数式组件仍然无生命周期的概念。虽然有各种各样用useEffect
来处理生命周期事件的方法,但本人认为认为,不应该把生命周期的概念考虑到函数式组件中来,而应该用更贴近函数式编程的思想考虑React。
Hooks的思想除了为React带来了函数式组件的增强以外,更重要的是带来了一种思想:
UI = f(state)
上面的公式是说,UI是由state
运算而得来。这个公式在刚接触React Hooks时可能不那么容易理解,但随着深入了解React之后,你会有恍然大悟的一刻。在开悟之前,只需要记下来这条公式。
读到这里,你可能又有疑问:公式我不理解我先记下来,可生命周期直接不考虑了会让人很疑惑?这个问题的答案,还是请你回想上一部分提到的纯函数与边际作用两个概念,再带入实际场景中思考:
- 按钮A点击之后改变了B的展示状态,而B被隐藏时要结束计时器C的计时;
- 填写完成表单后要发请求到后端
上面仅举了两个例子,仔细想想,不难发现这两个例子都是边际作用的体现。在React当中有useEffect
专门用于处理这些边际作用,而在Vue 3之后,只需要使用watch
函数基本上就可以处理所有响应式变量变化导致的边际作用。
总之,hooks从使用角度来看,它是对函数式组件的增强;从思维方式来看,它带来了全新的函数式编程思考方式UI = f(state)
。
总结
本文以Vue 3推荐composition API为开始,讲到函数式编程思想,最后结合前两部分内容引出hooks基本概念的简介,并指出当下的React hooks与Vue 3 composition API均使用了函数式编程思想。
最后,本文最重要的公式是UI = f(state)
,这也是后续理解React的基础。