React18 函数组件精简笔记
新的React官网文档
0. 介绍
函数组件
在React16开始就有了函数式的组件写法,而到了React18更是完善了函数组件的各项功能,几乎可以和类组件平替,但在写法和运行方式上有这许多的不同之处。
基本概念:函数组件其实就是一个返回视图(JSX)的函数,函数里的状态又由各种React-hooks钩子函数维护
1. 创建函数组件
创建一个函数组件,非常简单!
1 2 3 4 5 6
| import React from "react";
export default function App() { return <div>App</div> }
|
1 2 3 4
| 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
|
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的函数
useEffect在函数组件里一般情况下有五种情况:
无效果写法
1.什么也不写仅填一个函数
这样的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(()=>{ let timer = setTimeout(()=>{ setCount(count + 5) },2000) console.log(count) return ()=>{ clearInterval(timer) } },[count])
|
完整的执行顺序:
1 2 3 4 5 6 7 8 9
| useEffect(()=>{ return ()=>{ } },[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
| useEffect(() => {})
useEffect(() => {}, [])
useEffect(() => {}, [number]);
useEffect(() => { return () => { } }, [])
useEffect(() => { return () => { }; }, [number]);
useEffect(async () => {})
useEffect(() => { (async function () { await ... })(); });
|
注意点
- useEffect可以声明多个
- useEffect中可以监听的值可以是多个
- useEffect的5中种情况中有1种是无效的,其余4种按使用情况灵活使用
- useEffect的参数1不能是异步函数,其返回值也不能是异步函数(约定)
- 使用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
| import React, { useRef, useEffect } from "react"; import Message from "./Message";
function App(){ const spanRef = useRef() useEffect(()=>{ console.log(spanRef.current) },[]) return <Message ref={spanRef} /> }
|
1 2 3 4 5 6 7 8
| 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(){}
Message.defaultProps = { }
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
| 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
| import React, { forwardRef, useImperativeHandle, useState } from "react";
function Message(props, ref) { const [value, setValue] = useState(""); useImperativeHandle(ref, () => ({ value }), [value]); return ( <input type="text" value={value} onChange={(event) => setValue(event.target.value)} /> ); }
export default forwardRef(Message);
|
使用注意⚠️:
useImperativeHandle的第三个参数如果不传入监听的状态的话第二个参数将每次渲染时都会执行
因为用到ref所以务必使用forwardRef高阶函数将组件进行封装,组件才能接收ref
经过实测子组件状态更新时,父组件使用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方法,也就是创建上下文对象的方式,在函数组件中也是如此,只是在使用时有些许的不同。
跨级组件通讯示意图:
单数据传递
使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
import { createContext } from "react" import Info from "./Info"
export const context = createContext()
export default function App(){ return <context.Provider value={{name:"柒柒"}}> <Info /> </context.Provider> }
|
1 2 3 4 5 6 7 8 9 10
| 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
|
import {useContext} from "react";
import {context} from "../App"
export default function NameSpan(){ 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";
const context = createContext();
function ParentComponent() { const [name, setName] = useState({"柒柒" });
return ( <context.Provider value={{name,setName}}> <ChildComponent /> </context.Provider> ); }
function ChildComponent() { 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
| memo(组件,(prevProps,nextProps)=>{ return prevProps !== nextProps })
|
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
| import React, { memo } from "react";
function Message({ name }) { console.log("Message render"); return <div>{name}</div>; }
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; }, [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
| 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
| import React, { memo } from "react";
function List(props) { return <ul>{props.getItems().map((item) => <li key={item}>{item}</li>)}</ul>; }
export default memo(List);
|