React 函数组件

没什么特殊情况的话,一般还是用函数组件


大部分内容其实是和类组件作出的对比

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from 'react';
import './App.css';

const App = props => {
const [n, setN] = useState(0)
const add = () => {
setN(n => n + 1)
}
return (
<div>
{n}
<button onClick={add}>+1</button>
</div>
)
}

export default App;

好了,一个函数式组件的基本形完成了,比类组件简短很多吧

基本概念

props

与类组件基本相同,react 会向组件提供 props 参数

只不过类组件是使用构造器来接收并初始化,函数组件是直接通过传入参数来取得

state

对于私有变量,类组件的方案是在构造器中声明

而在函数组件中,要使用 useState 来模拟 state 的效果

useState 函数返回两个值,按顺序分别是目标变量的值和对于该变量的 setter

同时还接受一个传入参数,作为目标变量的初始值

可以通过解构赋值获得变量和 setter

1
2
3
const [n, setN] = useState(0)
// n: 取决于传入值,此处是 number
// setN: React.Dispatch<React.SetStateAction<number>> 此处的 number 同上

setter

对于从 useState 中取得的 setter,一般可以直接写一个值,例如

1
setN(n + 1)

但是有复杂逻辑时,不免太过单薄,所以一般建议在 setter 中传入一个 function

setter 会向这个函数抛出一个参数,这个参数是操作数当前的值

这个函数应当返回一个值,作为操作数的新值

返回值

函数组件要求 return 一段 JSX 语段,起到类组件中 render 方法的效果

每当组件刷新时,都会再次执行函数组件中的语句

生命周期

好,现在发现一个很重要的问题——函数组件没有生命周期钩子函数!

但是 react 作者怎么可能没有想到这个呢?早就给你安排了模拟了

useEffect

首先来了解一下 useEffect 函数,函数原型如下

1
useEffect(fn[,target])

第一个参数是监听的回调函数

第二个参数可不填,表示监听组件中的所有项目,或填入一个数组,数组中的每个元素都是监听的对象

一个用例如下

1
2
3
useEffect(()=>{
console.log('render run')
}, [n])

则每当 n 变化时,都会输出 render run

模拟 constructor

构造器不需要模拟,在函数组件中,return 前的代码都可以认为是构造器

模拟 componentDidMount

根据上述定义,显然我们可以选择填入一个空数组,表示不需要监听任何变量

1
2
3
useEffect(()=>{
console.log('constructor')
}, [])

好了,现在只有函数组件初始化的时候会执行这段输出

模拟 componentWillUnmount

上面说到 useEffect 的第一个参数应该是一个函数,我们可以通过返回值来控制组件消亡前的动作

1
2
3
4
5
6
useEffect(()=>{
console.log('constructor')
return ()=>{
console.log('组件即将消亡')
}
})

如上,return 一个函数即可,该函数将在组件即将消亡时被调用

模拟 componentDidUpdate

在如下这个例子中

1
2
3
useEffect(()=>{
console.log('render run')
}, [n])

我们发现它会在 n 变动时打印输出,但在初始化的时候也打印了输出

如果要求不太高的话,其实已经可以就这样满足了

但我们怎么可以这么容易满足呢?我就是要让它在初始化的时候不打印,在更新的时候才打印!

那么容易想到使用一个控制变量来判断是不是第一次渲染

1
2
3
4
5
6
7
8
9
10
11
let flag = false

useEffect(() => {
if (flag) {
console.log('n changed')
}
else {
console.log('first render')
flag = true
}
}, [n])

但是一运行,发现一直都只能打印 first render,怎么回事??

因为直接创建的变量并不会绑定到函数组件上,我们必须通过 useState 函数来创建才可以

那么可以得到如下改版

1
2
3
4
5
6
7
8
9
10
11
const [flag, setFlag] = useState(false)

useEffect(() => {
if (flag) {
console.log('n changed')
}
else {
console.log('first render')
setFlag(flag => true)
}
}, [n])

现在可以正常实现我们的功能了,但是代码好丑

于是我们把这个代码段包装成一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const useUpdate = dep => {
const [flag, setFlag] = useState(false)

useEffect(() => {
if (flag) {
console.log('n changed')
}
else {
console.log('first render')
setFlag(flag => true)
}
}, [dep])
}

剥离与组件中变量的依赖,改为只依赖传入的参数

然后在函数组件中通过 useUpdate(n) 进行调用

注意,此处的函数名必须满足 \use.+\ 的格式,否则 react 会报错,认为这不是一个 react 组件或函数

但是还是不够灵活——我想传入我自己的处理函数,怎么办呢?

那么我们往这个函数中传入自己的 function

1
2
3
4
5
6
7
8
9
10
11
12
13
const useUpdate = (fn, dep) => {
const [flag, setFlag] = useState(false)

useEffect(() => {
if (flag) {
fn()
}
else {
console.log('first render')
setFlag(flag => true)
}
}, [dep])
}

在函数组件中通过 useUpdate(fn, n) 进行调用即可

现在就还挺好看了,用法也和原生 useEffect 挺像

现在把这段逻辑抽离到单独的文件中,然后引入它

1
2
3
4
5
6
7
8
9
10
11
12
13
// useUpdate.js
import { useEffect, useState } from 'react';

const useUpdate = (fn, dep) => {
const [flag, setFlag] = useState(false);

useEffect(() => {
if (flag) fn();
else setFlag(flag => true);
}, [fn, dep, flag]);
};

export default useUpdate;

之后就可以在任意文件中引入它了

注意,此处一般建议将 fn 和 flag 都放入监听数组中,否则 react 担心 fn 和 flag 一旦变化,会导致执行结果脱离预期,所以会引发警告(虽然警告一般都不重要)

总结

以上就是 react 函数组件的主要内容

结合类组件的内容,可以看出,react 在绝大多数地方都是推荐使用函数而不是直接赋值,并且大部分时候是通过组合各种功能来实现新功能,而不是依赖继承

如果用不习惯的话,vue 也不错,2333


感谢阅读

--It's the end.Thanks for your read.--