抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

React18 函数组件精简笔记

新的React官网文档

0. 介绍

函数组件

在React16开始就有了函数式的组件写法,而到了React18更是完善了函数组件的各项功能,几乎可以和类组件平替,但在写法和运行方式上有这许多的不同之处。

基本概念:函数组件其实就是一个返回视图(JSX)的函数,函数里的状态又由各种React-hooks钩子函数维护

1. 创建函数组件

创建一个函数组件,非常简单!

1
2
3
4
5
6
// src/App.js
import React from "react";

export default function App() {
return <div>App</div>
}
1
2
3
4
// src/index.js
import App from "./App"

root.render(<App />)

可以说声明一个函数当函数返回的是JSX视图时,这个函数就是一个组件。

2. 声明组件状态-useState

在函数组件里声明组件状态需要使用useState来声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import useState form "react"

function App () {
// 返回的值一个是设置的默认值,一个是更改值的方法,参数为默认值
const [count, setCount] = useState(0)
//
return <div>
{count}
<button onClick={()=>{
// 在更改值的时候和原来的setState类似,只是现在只需在参数里直接写更新的值
setCount(count + 1)
}}>点击加一</button>
</div>
}

在函数组件中,useState方法可以被调用多次用于声明多个组件状态

3. 响应函数useEffect(重点)

useEffect的主要作用在于监听值的变化而做执行自定义操作,和vue里的watch监听器很像。

官方的解释:useEffect是一个 React Hook,可让您将组件与外部系统同步

1
useEffect(setup, dependencies?)

useEffect函数在正确的时机执行正确的副作用代码

什么是副作用代码?

组件的职责是根据 Props 和 State 计算用户界面所需要的状态数据并渲染用户界面,其他的和渲染用户界面没有关系的代码都属于副作用代码。

比如 Ajax Request、手动修改 DOM、本地存储、控制台输出、定时器等。

在官方文档里的解释晦涩难懂,但将它进行比喻后,就更好理解其作用是什么了

useEffect在函数组件里它有着特殊的执行过程,先看代码

1
2
3
4
5
6
7
8
9
10
11
import useEffect from "react";

function App () {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count]);
return <button onClick={()=>{
setCount(count+1)}
}>{count}</button>
}

useEffect的执行过程:

  1. 组件挂载后执行一次参数1 的函数
  2. 监听的状态更新后执行一次参数1的函数

useEffect在函数组件里一般情况下有五种情况:

无效果写法

1.什么也不写仅填一个函数

1
2
3
useEffect(()=>{
...
})

这样的useEffect钩子函数的调用不会触发任何效果,和直接写在函数体里的效果一样

模拟生命周期函数

2.componentDidMount组件挂载完成时

1
2
3
useEffect(()=>{
console.log("组件完成挂载")
},[])

3.componentWillUnmount组件即将卸载时

1
2
3
4
5
useEffect(()=>{
return ()=>{
console.log("组件即将卸载")
}
},[])

以上两个情况可以写成一个useEffect函数

1
2
3
4
5
6
useEffect(()=>{
console.log("组件完成挂载")
return ()=>{
console.log("组件即将卸载")
}
})

监听器

4.监听状态的变化执行一些操作

1
2
3
4
5
const [count,setCount] = useState(0)

useEffect(()=>{
console.log(count)
},[count])

5.执行响应函数前再执行一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const [count,setCount] = setState(0)

useEffect(()=>{
// 再执行此函数之前会先执行return的函数
let timer = setTimeout(()=>{
setCount(count + 5)
},2000)
console.log(count)
// 当状态更改时会先执行return的这个函数
return ()=>{
clearInterval(timer)
}
},[count])

完整的执行顺序:

1
2
3
4
5
6
7
8
9
useEffect(()=>{
// 1. 组件挂载时执行
// 3. 监听的状态更新时(组件状态更新时触发)
return ()=>{
// 2. 监听的状态更新时(组件状态更新时先触发)
// 4. 组件卸载时
}
// 组件状态
},[count])

详细完整执行解析

一共五种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// (1)只要组件状态发生更新, 不管是谁更新, 它都执行
useEffect(() => {})

// (2)组件挂载完成之后执行一次
useEffect(() => {}, [])

// (3)监听状态
// 1. 组件挂载完成之后执行一次
// 2. number 状态每次发生变化执行一次
useEffect(() => {}, [number]);

// (4)模拟挂载卸载声明周期函数
// 返回的函数在组件卸载之前执行一次
useEffect(() => {
// 组件挂载完成之后执行一次
return () => {
// 组件卸载之前执行一次
}
}, [])

// (5)监听状态,且有一个先执行的函数
useEffect(() => {
// 1. 组件挂载完成之后执行一次
// 2. 组件状态 number 每次发生变化执行一次
return () => {
// 1. 组件卸载之前执行一次
// 2. 组件状态 number 每次发生变化执行一次
};
// 当 number 发生变化以后先执行清理函数, 再执行副作用函数
}, [number]);


// useEffect 第一个参数函数不能是异步函数 以下代码的写法是错误的
useEffect(async () => {})

// 如需要在参数1的函数进行异步操作可以参考以下写法
useEffect(() => {
(async function () {
await ...
})();
});

// useEffect 的作用是确保副作用代码在正确的时机得到执行

注意点

  1. useEffect可以声明多个
  2. useEffect中可以监听的值可以是多个
  3. useEffect的5中种情况中有1种是无效的,其余4种按使用情况灵活使用
  4. useEffect的参数1不能是异步函数,其返回值也不能是异步函数(约定)
  5. 使用useEffect时只能在函数组件的顶层作用域声明(也不能嵌套在if-else、for等作用域里)。

4. 获取DOM对象的钩子函数useRef

通过useRef方法可以在函数式组件中获取DOM对象,其实也可以创建一个函数组件重新渲染而不被影响的值,就像组件的状态

通过useRef绑定dom对象

1
2
3
4
5
6
7
import React, { useRef } from "react";

function App() {
const username = useRef();
return <input ref={username} onChange={() => console.log(username.current)} />;
}
export default App;

还可以通过useRef批量获取DOM对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useEffect, useRef, useState } from "react";

export default function App() {
const [state] = useState(["a", "b", "c"]);
const liRefs = useRef([]);
useEffect(() => {
console.log(liRefs.current);
}, []);
return (
<ul>
{state.map((item, index) => (
<li ref={(element) => (liRefs.current[index] = element)} key={item}>
{item}
</li>
))}
</ul>
);
}

还可以使用useRef创建一个在渲染时不被影响的值

1
2
3
4
export default function App() {
const username = useRef("张三");
return <div>{username.current}</div>
}

5. 高阶组件forwardRef

forwardRef是一个高阶组件,用于在父子组件之间传递ref对象,实现一些高级功能,比如实现获取子组件中的DOM对象

1
2
3
4
5
6
7
8
9
10
11
12
// src/App.js
import React, { useRef, useEffect } from "react";
import Message from "./Message";

function App(){
const spanRef = useRef()
useEffect(()=>{
// 在这里可以打印出子组件的dom对象
console.log(spanRef.current)
},[])
return <Message ref={spanRef} />
}
1
2
3
4
5
6
7
8
// src/Message.js
import React, { forwardRef } from "react";

function Message(props, ref) {
return <span ref={ref}>Hello React</span>;
}

export default forwardRef(Message);

6. 父子组件通讯props

在类组件中父子通讯是使用constructor的第一个参数props来接收父组件传递的状态的,在函数组件中,函数的第一个参数就是props

1
2
3
function Message(props){
return <div>{props.msg}</div>
}

设置组件的props默认值有两种方式

1
2
3
4
5
6
7
8
9
10
11
function Message(){}

// 方式1: 通过设置函数的defaultProps属性来设置props默认值
Message.defaultProps = {

}

// 方式2: 通过js的原本的特性设置props
function Message({msg = "提示",type = "success"}){
return <div>{msg}</div>
}

推荐第二种设置方式,符合js编程规则,也可以满足props默认值的设置

7. 父子通讯useImperativeHandle

useImperativeHandle 方法将子组件里的东西暴露给父组件

props是父传子,正常的子父通讯是使用props传递函数给子组件,让子组件调用函数传递参数给父组件,而useImperativeHandle 方法通过组件的ref属性传递数据给父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/App.js
import React, { useRef } from "react";
import Message from "./Message";

function App() {
const msgRef = useRef();
return (
<>
<Message ref={msgRef} />
<button
onClick={() => {
console.log(msgRef.current.value);
}}
>
getMsg
</button>
</>
);
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Message.js
import React, { forwardRef, useImperativeHandle, useState } from "react";

function Message(props, ref) {
const [value, setValue] = useState("");
// useImperativeHandle 方法用于设置 ref 对象中的 current 属性的值
// 第一个参数传递 ref 对象
// 第二个参数传递函数, 函数返回什么, ref 对象中的 current 属性值就是什么, 该函数默认会在组件每次重新渲染时执行
// 第三个参数是一个数组, 传递依赖状态, 表示当依赖状态发生变化以后再执行第二个参数函数
useImperativeHandle(ref, () => ({ value }), [value]);
return (
<input
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
);
}
// 切勿忘记使用forwardRef高阶函数将Message封装
export default forwardRef(Message);

使用注意⚠️:

  1. useImperativeHandle的第三个参数如果不传入监听的状态的话第二个参数将每次渲染时都会执行

  2. 因为用到ref所以务必使用forwardRef高阶函数将组件进行封装,组件才能接收ref

  3. 经过实测子组件状态更新时,父组件使用useEffect是监听不到定义的ref的变化的(需要自己解决)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function App(){
    const spanRef = useRef()
    useEffect(()=>{
    console.log(spanRef) // 仅在组件创建时打印一次
    },[spanRef])
    return <Span ref={spanRef} />
    }

    let Span = forwardRef((props,ref)=>{
    const [count,setCount] = useState(0)
    useImperativeHandle(ref,()=>count,[count])
    return <button onClick={()=>{setCount(count + 1)}}>{count}</button>
    })

    如果要让每次更改都让父组件知道的话,依旧要使用到props传递函数的方式

8. 跨级组件通讯

在类组件中,跨级组件通讯使用到的是createContext方法,也就是创建上下文对象的方式,在函数组件中也是如此,只是在使用时有些许的不同。

跨级组件通讯示意图:

image-20230419201338900

单数据传递

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/App.jsx
// 1. 导入createContext方法,并创建context对象
import { createContext } from "react"
import Info from "./Info"

// 2. 创建上下文对象(可甜可不填上下文对象的默认值,但感觉填了和没填都一样,下边使用时依旧要传入value)
export const context = createContext()

export default function App(){
// 3. 使用上下文对象中的Provider(生产者)高阶组件进行包裹,并传递value值
return <context.Provider value={{name:"柒柒"}}>
<Info />
</context.Provider>
}
1
2
3
4
5
6
7
8
9
10
// src/Info.jsx
import NameSpan from "./components/NameSpan"

export default function Info(){
return <div>
<NameSpan />
<AgeSpan />
<GenderSpan />
</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/components/NameSpan.jsx
// 4. 在被Provider包裹的组件中使用context对象
// 4.1 导入useContext钩子函数
import {useContext} from "react";
// 4.2 导入上下文对象
import {context} from "../App"

export default function NameSpan(){
// 4.3 在组件中使用useContext拿到上下文对象中的值
const contextValue = useContext()
return <span>
{contextValue.name}
</span>
}

以上是单数据传递的方式

数据及方法传递

为了让数据进行穿透传递,又要让数据更改触发视图更新,那么通过上下文对象不仅需要传递数据(状态)还要传递设置数据(状态)的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { createContext, useContext, useState } from "react";

// 创建 Context 对象
const context = createContext();

// 父组件
function ParentComponent() {
// 创建组件状态
const [name, setName] = useState({"柒柒" });

return (
// 传递组件状态和设置状态的方法至上下文对象(为了修改状态能让视图更新)
<context.Provider value={{name,setName}}>
<ChildComponent />
</context.Provider>
);
}

// 子组件
function ChildComponent() {
// 使用useContext钩子函数获取上下文对象
const contextValue = useContext(context)

return (
<div>
<p>name: {contextValue}</p>
<button onClick={()=>{
contextValue.setName("六六")
}}>更改name为六六</button>
</div>
);
}

以上就是使用上下文对象传递状态和方法的示例

灵活使用上下文对象可以使状态层层传递减少哦

9. 高阶组件memo

类似于react类组件里的pureComponent,可以自定义检查新旧的props值是否一致而决定组件是否渲染,默认情况下memo方法只能进行浅比较,对于饮用数据类型仅比较内存中的饮用地址是否更改。

memo高阶函数还有第二个参数,第二个参数为函数()=>{},用于手动比对值,如果返回true则继续渲染,返回false则不渲染,类似于类组件里的shouldComponentUpdate

1
2
3
4
// prevProps上一次的props值,nextProps新props值
memo(组件,(prevProps,nextProps)=>{
return prevProps !== nextProps
}/* 参数2为自定义判断函数,根据需求进行编写 */)

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/App.js
import React, { useState, useEffect } from "react";
import Message from "./Message";

function App() {
const [person, setPerson] = useState({ name: "张三", age: 20 });
useEffect(() => {
let timer = setInterval(() => {
setPerson({ ...person, age: person.age + 1 });
}, 1000);
return () => clearInterval(timer);
}, [person]);
return (
<>
<div>{person.age}</div>
<Message name={person.name} />
</>
);
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
// src/Message.js
import React, { memo } from "react";

function Message({ name }) {
console.log("Message render");
return <div>{name}</div>;
}
// 如果使用平常的导出方式,每次定时器执行都会在重新渲染Message组件
// export default Message;
// 使用memo高阶组件则会将props的值进行对比,如值相同勿重渲染
export default memo(Message);

memo高阶函数还有第二个参数,第二个参数为函数()=>{},用于手动比对值,如果返回true则继续渲染,返回false则不渲染

10. 备忘录函数useMemo(计算属性)

备忘录函数useMemo类似于vue里的computed计算属性,监听一个值的变化而计算一个新值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default function App() {
let [count, setCount] = useState(0);
// 备忘录函数,根据指定的值进行计算获得一个新值,而不触发视图渲染
let multiple = useMemo(() => {
return count * 5;
// 参数2为数组,数组里是要监听的值
}, [count]);
return (
<div>
{count} <br />
5倍:{multiple} <br />
<button
onClick={() => {
setCount(count + 1);
}}
>
点击加1
</button>
</div>
);
}

11. 函数(方法)缓存useCallback

useCallback 方法用于在函数组件中缓存方法,以免组件在每次渲染时都返回一个新方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/App.js
import React, { useState, useMemo, useCallback } from "react";
import List from "./List";

function App() {
const [count, setCount] = useState(0);
const [dark, setDark] = useState(false);

const getItems = useCallback(() => {
return [count, count + 1, count + 2];
}, [count]);

return (
<>
<List getItems={getItems} />
<button onClick={() => setCount(count + 1)}>button</button>
<button onClick={() => setDark(!dark)}>{dark ? "dark" : "light"}</button>
</>
);
}

export default App;
1
2
3
4
5
6
7
8
// src/List.js
import React, { memo } from "react";

function List(props) {
return <ul>{props.getItems().map((item) => <li key={item}>{item}</li>)}</ul>;
}

export default memo(List);

评论