您现在的位置是:首页 >技术交流 >7. React Hooks网站首页技术交流
7. React Hooks
- 官方文档:https://zh-hans.react.dev/reference/react/hooks
- 官方文档:https://zh-hans.legacy.reactjs.org/docs/hooks-intro.html
- Router6 的一个中文文档:https://baimingxuan.github.io/react-router6-doc/
- react:版本 18.2.0
- node: 版本18.19.1
- 脚手架:版本 5.0.1
- 用例中的干净的脚手架的创建可以参考另一篇文章:3.React 组件化开发
一、认识 Hooks
(一)类组件与 函数组件对比
【1】优势
- class组件可以定义自己的state,用来保存组件自己内部的状态;
- 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
- 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
- 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
- class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
- 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
- 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
【2】劣势
- 复杂组件变得难以理解:
- 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
- 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);
- 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
- 难以理解的class:
- 很多人发现学习ES6的class是学习React的一个障碍。
- 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
- 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
- 组件复用状态很难:
- 在前面为了一些状态的复用我们需要通过高阶组件;
- 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
- 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
- 这些代码让我们不管是编写和设计上来说,都变得非常困难;
(二)Hook 介绍
- Hook的出现,可以解决上面提到的这些问题;
- 简单总结一下hooks:
- 它可以让我们在不编写class的情况下使用state以及其他的React特性;
- 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
- Hook的使用场景:
- Hook的出现基本可以代替我们之前所有使用class组件的地方;
- 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
- Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
- 在我们继续之前,请记住 Hook 是:
- 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
- 100% 向后兼容的:Hook 不包含任何破坏性改动。
- 现在可用:Hook 已发布于 v16.8.0。
二、Hook API
建议直接看官网:https://zh-hans.react.dev/reference/react/useState
(一)useState
-
在组件的顶层调用 useState 来声明一个 状态变量。如果没有传递参数,那么初始化值为undefined。
-
useState 返回一个由两个值组成的数组:
- 当前的 state。在首次渲染时,它将与你传递的 initialState 相匹配。
- set 函数,它可以让你将 state 更新为不同的值并触发重新渲染。
const [state, setState] = useState(initialState)
(二)useEffect
-
useEffect 可以让你来完成一些类似于class中生命周期的功能;
-
useEffect 是一个 React Hook,它允许你 将组件与外部系统同步。
useEffect(setup, dependencies?)
- setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。
- dependencies:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
-
Hook 允许我们按照代码的用途分离出多个useEfffect , 而不是像生命周期函数那样
-
创建一个干净的脚手架
-
在src 文件夹下创建 Son.jsx 文件,用于创建子组件
import React, { memo, useEffect, useState } from 'react'; const Son = (props) => { const [conter, setConter] = useState(0); const [msg, setMsg] = useState('一条信息'); // 每个 useEffect 对应一个功能 // 每次渲染时执行 useEffect(() => { console.log(props); console.log('Son组件挂载了'); const timer = setInterval(() => { console.log('Son组件内部定时器'); }, 1000); // 组件卸载时清除定时器,(类似componentWillUnmount 生命周期函数) // 如果不清除,定时器会一直存在 return () => { timer && clearInterval(timer); console.log('Son组件卸载了'); }; }); // 每次渲染时执行 useEffect(() => { document.title = conter; }); // 回调函数只在 msg 变化时执行, // 不受别的变量的影响 useEffect(() => { console.log('msg 被修改'); }, [msg]); // 不受任何的变量的影响,只执行一次 // 类似 componentDidMount 生命周期函数 useEffect(() => { console.log('不受任何的变量的影响'); }, []); return ( <div> <p>{props.title}</p> Son:{conter} <p>{msg}</p> <button onClick={(e) => setConter(conter + 1)}>增加 + 1</button> <button onClick={(e) => setMsg('另一个消息')}>修改 msg</button> </div> ); }; export default Son;
-
修改sr/App.js 文件内容如下
import React, { memo, useState } from 'react'; import Son from './Son'; const App = () => { const [flag, setFlag] = useState(true); const [title, setTitle] = useState('标题'); const changeFlag = () => { setFlag(!flag); }; const changeTitle = () => { setTitle('标题2'); }; return ( <div> {flag && <Son title={title} />} <button onClick={changeFlag}>销毁/挂载子组件</button> {/* 修改 title 后 Son组件将会被销毁并重新挂载 */} <button onClick={changeTitle}>修改title</button> </div> ); }; export default App;
(三)useContext
-
useContext 是一个 React Hook,可以让你读取和订阅组件中的 context。
const value = useContext(SomeContext)
- SomeContext:先前用 createContext 创建的 context。context 本身不包含信息,它只代表你可以提供或从组件中读取的信息类型。
-
创建一个干净的脚手架
-
在 src 文件夹下创建 context.js 文件,用于存放共用信息
import React from 'react'; export const themes = { light: { foreground: '#000000', background: '#eeeeee', color: '#000', }, dark: { foreground: '#ffffff', background: '#222222', color: '#fff', }, }; // 创建context对象的 export const ThemeContext = React.createContext(); export const user = { id: '001', name: '张三', age: 18, gender: '男', }; // 创建context对象的, 将 user 作为默认值 export const UserContext = React.createContext(user);
-
在src 文件夹下创建 User.jsx 文件,用于展示用户信息
import React, { memo, useContext } from 'react'; import { UserContext, ThemeContext } from './context'; const User = memo(() => { // user 使用的是 createContext 设置的默认值 const user = useContext(UserContext); // theme 使用的是 ThemeContext.Provider 设置的默认值 const theme = useContext(ThemeContext); return ( <div style={{ color: theme.color, background: theme.background, }} > <p>姓名:{user.name}</p> <p>年龄:{user.age}</p> </div> ); }); export default User;
-
修改App.js 文件如下
import React, { memo } from 'react';
import { themes, ThemeContext } from './context';
import User from './User';
const App = memo(() => {
return (
<div>
{/* 给 ThemeContext 设置值*/}
<ThemeContext.Provider value={themes.dark}>
<User />
</ThemeContext.Provider>
</div>
);
});
export default App;
(四)useCallback
-
useCallback实际的目的是为了进行性能的优化。
-
useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。
const cachedFn = useCallback(fn, dependencies)
-
useCallback会返回一个函数的 memoized(记忆的) 值;
-
在依赖不变的情况下,多次定义的时候,返回的值是相同的;
-
通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存
- 创建一个干净的脚手架
- 修改src/App.js 文件内容如下:
import React, { memo, useCallback, useState } from 'react'; // 创建一个子组件 const Child = memo((props) => { console.log('Child 组件重新渲染了'); const { customChangeCounter } = props; return ( <div> Child <button onClick={customChangeCounter}>修改counter</button> </div> ); }); // 每次 counter 改变时,会重新渲染整个 App 组件 const App = memo(() => { const [counter, setCounter] = useState(0); const [name, setName] = useState('张三'); console.log('App 组件重新渲染了'); function changeName() { setName('李四'); } // 每次 counter 改变时,会重新渲染整个 App 组件 // changeCounter1 也会被重新定义 function changeCounter1() { console.log('changeCounter1被定义'); setCounter(counter + 1); } // 如果没有第二个参数,和上边的 changeCounter1 没有区别 // 第二个参数是依赖项,只有依赖项改变时,useCallback 返回值才会是一个新的函数 // 注:App 组件重新渲染时 useCallback 的第一个参数会被重新定义, // 只是 useCallback 返回值会根据依赖项改变而改变 const changeCounter2 = useCallback(() => { console.log('changeCounter2被定义'); setCounter(counter + 1); console.log(counter); }, [counter]); // 依赖项不改变,useCallback 返回值不会改变 const changeCounter3 = useCallback(() => { console.log('changeCounter3被定义'); // counter 将不会改变 setCounter(counter + 1); console.log(counter); }, []); return ( <div> <p>{counter}</p> <p>{name}</p> {/** * 1.customChangeCounter 传递changeCounter1 时,修改name 后,Child1 也会重新渲染 * 因为 name 被修改后 App 组件重新渲染, changeCounter1 被重新定义, * customChangeCounter 获取到一个新值,所以 Child1 重新渲染 * 2.customChangeCounter 传递changeCounter2 时,修改name 后,Child1 不重新渲染 * 因为 counter 没有改变,changeCounter2 没有被重新定义, */} <Child customChangeCounter={changeCounter2} /> <button onClick={() => changeCounter1()}>button1:+1</button> <button onClick={() => changeCounter2()}>button2:+1</button> <button onClick={() => changeCounter3()}>button3:+1</button> <button onClick={changeName}>修改name</button> </div> ); }); export default App;
(五)useMemo
-
useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
-
在依赖不变的情况下,多次定义的时候,返回的值是相同的;
const cachedValue = useMemo(calculateValue, dependencies)
- 创建一个干净的脚手架
- 修改src/App.js 文件内容如下:
import React, { memo, use, useMemo } from 'react'; // 子组件 const Child = memo((props) => { console.log('Child 被渲染'); const { user } = props; return ( <div> {user.name}-{user.age} </div> ); }); // 计算属性 const countTotal = (count) => { let total = 0; console.log('countTotal 被调用'); for (let i = 1; i <= count; i++) { total += i; } return total; }; const App = memo(() => { console.log('App 被渲染'); const [count, setCount] = React.useState(0); const [user, setUser] = React.useState({ name: '张三', age: 18 }); const changeUser = () => { setUser({ name: '李四', age: 20 }); }; /** * user 改变时,会重新渲染整个 App 组件 * countTotal 也会被重新调用 */ // const total = countTotal(count); // 只有 count 改变时才会重新调用 countTotal const total2 = useMemo(() => { return countTotal(count); }, [count]); return ( <div> <p>{count}</p> <p>{total2}</p> <Child user={user} /> {/* 修改 count, 子组件没有被重新渲染 */} <button onClick={() => setCount(count + 1)}>+1</button> <button onClick={changeUser}>修改 User</button> </div> ); }); export default App;
(六)useRef
-
useRef 是一个 React Hook,它能帮助引用一个不需要渲染的值。
-
useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变
const ref = useRef(initialValue)
- 创建一个干净的脚手架
- 修改src/App.js 文件内容如下:
import React, { memo, useCallback, useRef } from 'react'; const App = memo(() => { const [count, setCount] = React.useState(0); console.log('App 被渲染'); // 用法1:绑定一个 Dom const titleDom = useRef(); const showTitleDom = () => { console.log(titleDom.current); }; // useCallback 没有依赖,所以回调函数不会重新生成 // 即使 count 改变,App 重新渲染,回调函数也不会重新生成 // 回调函数不重新生成, 回调函数内部的count,只会是第一次渲染时的值 const addCount = useCallback(() => { // count 会一直都是 0 setCount(count + 1); }, []); // 用法2:绑定一个值 const countRef = useRef() countRef.current = count; const addCount2 = useCallback(() => { // count 会一直都是 0 setCount(countRef.current + 1); }, []); return ( <div> {count} <h2 ref={titleDom}>标题</h2> <button onClick={showTitleDom}>显示标题Dom</button> <button onClick={addCount}>增加Count</button> <button onClick={addCount2}>增加Count2</button> </div> ); }); export default App;
(七)useImperativeHandle
- useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由 ref 暴露出来的句柄(通过useImperativeHandle可以只暴露固定的操作)
useImperativeHandle(ref, createHandle, dependencies?)
- 创建一个干净的脚手架
- 修改src/App.js 文件内容如下:
import React, { forwardRef, memo, useImperativeHandle, useRef } from 'react'; // 子组件1 const Child1 = memo(forwardRef((props, ref) => { return ( <div> <input ref={ref} type="text" /> </div> ); })); // 子组件2 const Child2 = memo(forwardRef((props, ref) => { const inputRef = useRef(); // 父组件通过ref,调用子组件的方法 // 只能使用 useImperativeHandle,第二个参数所暴露的方法 useImperativeHandle(ref, () => ({ // 只暴露聚焦方法 focus: () => { console.log(inputRef.current); inputRef.current.focus(); }, })); return ( <div> <input ref={inputRef} type="text" /> </div> ); })); const App = memo(() => { const childRef1 = useRef(); const childRef2 = useRef(); return ( <div> <Child1 ref={childRef1} /> <Child2 ref={childRef2} /> <button onClick={() => childRef1.current.focus()}>聚焦Child1</button> <button onClick={() => childRef1.current.value = 2}>修改值Child1</button> <button onClick={() => childRef2.current.focus()}>聚焦Child2</button> <button onClick={() => childRef2.current.value = 2}>修改值Child2</button> </div> ); }); export default App;
(八)useLayoutEffect
-
useLayoutEffect 可能会影响性能。尽可能使用 useEffect。
-
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
-
useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发。
useLayoutEffect(setup, dependencies?)
- 创建一个干净的脚手架
- 修改src/App.js 文件内容如下:
import React, { memo, useState, useEffect, useLayoutEffect } from 'react'; const App = memo(() => { const [count, setCount] = useState(5); // 会有闪烁现象 useEffect(() => { console.log('useEffect'); if (count === 0) { setCount(Math.random() + 88); } }); // useLayoutEffect(() => { // console.log('useEffect'); // if (count === 0) { // setCount(Math.random() + 88); // } // }); return ( <div> <p>{count}</p> <button onClick={() => setCount(0)}> count 设置为0</button> </div> ); }); export default App;