为什么需要
Hook
是 React 16.8
的新增特性,它可以让我们在不编写class
的情况下使用state
以及其他的React
特性(比如生命周期),先来思考一下class
组件相对于函数式组件有什么优势?比较常见的是下面的优势:
-
class
组件可以定义自己的state
,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量 -
class
组件有自己的生命周期,可以在生命周期中完成自己的逻辑-
比如在
componentDidMount
中发送网络请求,并且该生命周期函数只会执行一次 -
函数式组件在学习
hooks
之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求
-
-
class
组件可以在状态改变时只会重新执行render
函数以及希望重新调用的生命周期函数componentDidUpdate
等- 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次
-
所以在
Hook
出现之前,对于上面这些情况我们通常都会编写class
组件 -
Class
组件存在的问题:-
复杂组件变得难以理解:随着业务的增多,
class
组件会变得越来越复杂 -
难以理解的
class
:比如在class
中,必须搞清楚this
的指向到底是谁 -
组件复用状态很难:一些状态的复用需要通过高阶组件,类似于
Provider、Consumer
来共享一些状态,但是多次使用Consumer
时,代码会存在很多嵌套
-
-
Hook
的出现,可以解决上面提到的这些问题:- 可以让我们在不编写
class
的情况下使用state
以及其他的React
特性
- 可以让我们在不编写
-
Hook
的使用场景:-
Hook
的出现基本可以代替之前所有使用class
组件的地方 -
如果是一个旧的项目,不需要直接将所有的代码重构为
Hooks
,它完全向下兼容,可以渐进式的来使用它 -
Hook
只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
-
-
Hook
的规则:-
只能在函数最外层调用
Hook
,不要在循环、条件判断或者子函数中调用 -
只能在
React
的函数组件中调用Hook
,不要在其他JavaScript
函数中调用
-
-
通过一个计数器案例,来对比一下
class
组件和函数式组件结合hooks
: -
create-react-app learn_react_router
创建项目练习 -
src/App.jsx
整体练习代码如下:jsimport React, { memo } from 'react' import ComUseState from './components/ComUseState' import ComUseEffect from './components/ComUseEffect' import ComUseContext from './components/ComUseContext' import ComUseReducer from './components/ComUseReducer' import ComUseCallback from './components/ComUseCallback' import ComUseMemo from './components/ComUseMemo' import ComUseRef from './components/ComUseRef' import ComUseImperativeHandle from './components/ComUseImperativeHandle' import ComUseLayoutEffect from './components/ComUseLayoutEffect' import { ThemeContext, UserContext } from './context' import MyUseLogLift from './components/MyUseLogLift' import MyPublicContext from './components/MyPublicContext' import MyPageScroll from './components/MyPageScroll' import MyUseLocalStorage from './components/MyUseLocalStorage' import ComReduxHooks from './components/ComReduxHooks' import ComUseTransition from './components/ComUseTransition' import ComUseDeferredValue from './components/ComUseDeferredValue' import { Provider } from 'react-redux' import store from './store' import ComUseId from './components/ComUseId' const App = memo(() => { return ( <div style={{height: '2000px'}}> <ComUseState /> <hr /> <ComUseEffect /> <hr /> <ThemeContext.Provider value={{color: 'red', fontSize: '24px'}}> <UserContext.Provider value={{name: 'WuLi'}}> <ComUseContext /> </UserContext.Provider> </ThemeContext.Provider> <hr /> <ComUseReducer /> <hr /> <ComUseCallback /> <hr /> <ComUseMemo /> <hr /> <ComUseRef /> <hr /> <ComUseImperativeHandle /> <hr /> <ComUseLayoutEffect /> <hr /> {/* 自定义hooks */} <h3>自定义hooks</h3> <MyUseLogLift /> <ThemeContext.Provider value={{color: 'red', fontSize: '24px'}}> <UserContext.Provider value={{name: 'WuLi'}}> <MyPublicContext /> </UserContext.Provider> </ThemeContext.Provider> <MyPageScroll /> <MyUseLocalStorage /> <hr /> {/* redux里面的两个hook */} <Provider store={store}> <ComReduxHooks /> </Provider> <hr /> <ComUseId /> <hr /> {/* react18新增的两个hook */} <div style={{display:'flex'}}> <ComUseTransition /> <ComUseDeferredValue /> </div> </div> ) }) export default App
useState
useState
是 React
中的一个 Hook
,用于在函数组件中添加状态管理。它允许你在函数组件中声明和更新状态,而无需将组件转换为类组件
-
const [state, setState] = useState(initialState)
-
state
:当前的状态值 -
setState
:用于更新状态的函数-
是异步的更新状态后立即访问状态值可能得到的是旧值
-
如果新状态依赖于旧状态,可以使用函数式更新
setCount((prevCount) => prevCount + 1)
-
对于对象或数组状态,避免直接修改,而是创建新的对象或数组
jssetUser({ ...user, name: 'New Name' }); setTodos([...todos, newTodo]);
-
-
initialState
:状态的初始值,可以是任何数据类型(如数字、字符串、对象、数组等)-
如果初始状态需要复杂计算,可以传递一个函数
const [state, setState] = useState(() => computeInitialState())
-
如果没有传递参数,那么初始化值为
undefined
-
-
练习代码如下:
js// component/ComUseState.jsx import React, { memo } from 'react' import { useState } from 'react' const ComUseState = memo(() => { // 初始值只有在组件第一次渲染时才有效,第二次渲染传入什么值都无效 const [count, setCount] = useState(0) return ( <div> <h3>useState的使用</h3> <p>count: {count}</p> <button onClick={e=>setCount(count+5)}>+5</button> </div> ) }) export default ComUseState
Effect Hook
目前已经通过hook
在函数式组件中定义state
,那么类似于生命周期这些呢?Effect Hook
可以让你来完成一些类似于class
中生命周期的功能
useEffect
-
useEffect
是React
中的一个Hook
,用于在函数组件中执行副作用操作 -
副作用操作包括数据获取、订阅、手动操作
DOM
等 -
useEffect
可以看作是类组件中componentDidMount
、componentDidUpdate
和componentWillUnmount
的组合 -
useEffect
要求传入一个回调函数 ,在React
执行完更新DOM
操作之后,就会回调这个函数,默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数jsuseEffect(() => { // 副作用操作 return () => { // 清理操作(可选) }; }, [dependencies])
-
第一个参数:一个函数,包含需要执行的副作用操作
-
第二个参数(可选) :一个依赖数组,只有当依赖项发生变化时,才会重新执行副作用操作如果省略,副作用操作会在每次渲染后执行
-
-
使用注意事项:
-
依赖数组:
如果依赖数组为空,副作用操作只会在组件挂载和卸载时执行
如果依赖数组包含某些状态或属性,副作用操作会在这些依赖项变化时执行
-
清理操作:
如果副作用操作需要清理(如取消订阅、清除定时器),可以在
useEffect
中返回一个清理函数,会在组件更新和卸载的时候执行清除操作 -
避免无限循环:
如果副作用操作中更新了状态,且该状态是依赖项之一,可能会导致无限循环。确保依赖项的选择是合理的
-
多个
useEffect
:可以在一个组件中使用多个
useEffect
,将不同的副作用操作分开管理,将按照effect
声明的顺序依次调用组件中的每一个effect
-
-
代码示例如下:
js// cmponent/ComUseEffect.jsx import React, { memo } from 'react' import { useEffect,useState } from 'react' const ComUseEffect = memo(() => { const [count, setCount] = useState(20) /* 可以使用多个useEffect,更新完成后他们会按照你代码从上到下的顺序执行,卸载时也是 useEffect(): 第一个参数:函数,函数中return函数表示更新和卸载之前执行 第二个参数:数组,[放影响你代码执行的变量],[]空数组表示不受任何影响 */ useEffect(()=> { // 组件第一次渲染和每次更新完成都会执行此处代码 document.title = count console.log('修改title'); },[count]) useEffect(()=> { console.log('发送网络请求'); return ()=> { console.log("会在组件被卸载时, 才会执行一次") console.log('取消网络请求'); } // 写了第二个参数[],表示不受任何值改变的影响,只会执行一次回调函数 },[]) useEffect(()=> { console.log('eventBus事件监听'); return ()=> { console.log("会在组件被卸载时, 才会执行一次") console.log('取消eventBus事件监听'); } },[]) return ( <div> <h3>useEffect的使用</h3> <p>count: {count}</p> <button onClick={e=>setCount(count+10)}>+10</button> </div> ) }) export default ComUseEffect
useLayoutEffect
useLayoutEffect
是 React
提供的一个 同步执行的副作用 Hook
,它的执行时机比 useEffect
更早,用于在浏览器绘制更新前执行代码
-
语法:
jsuseLayoutEffect(() => { // 这里的代码在浏览器绘制前执行 return () => { // 可选的清理函数 }; }, [依赖项])
-
useLayoutEffect
会在渲染的内容更新到DOM
上之前执行,会阻塞DOM
的更新 -
和
useEffect
比较: 大多数情况下,应该优先使用useEffect
-
练习代码如下:
jsimport React, { memo, /* useEffect, */ useLayoutEffect, useState } from 'react' const ComUseLayoutEffect = memo(() => { const [count, setCount] = useState(0) /* useEffect是在dom更新完成时执行,useLayoutEffect在dom更新之前执行 */ useLayoutEffect(()=> { console.log('useLayoutEffect'); // 点击按钮这里会在渲染成5之前先执行这个代码,不会渲染5会直接渲染下面的随机数,不会有闪烁 if(count === 5) { setCount(Math.random()+9) } },[count]) /* useEffect(()=> { console.log('useEffect'); // 点击按钮这里会先渲染完成5然后在执行下面代码,会有闪烁 if(count === 5) { setCount(Math.random()+9) } },[count]) */ return ( <div> <h3>useLayoutEffect的使用</h3> <p>当前计数:{count}</p> <button onClick={e=>setCount(5)}>设置为5</button> </div> ) }) export default ComUseLayoutEffect
useContext
在之前的开发中,要在组件中使用共享的Context
有两种方式:
-
类组件可以通过
类名.contextType = MyContext
方式,在类中获取context
-
多个
Context
或者在函数式组件中通过MyContext.Consumer
方式共享context
-
但是多个
Context
共享时的方式会存在大量的嵌套,Context Hook
允许通过Hook
来直接获取某个Context
的值 -
当组件上层最近的更新时,该
Hook
会触发重新渲染,并使用最新传递给MyContext provider
的context value
值 -
练习代码如下:
js// component/ComUseContext.jsx import React, { memo } from 'react' import { useContext } from 'react' import { ThemeContext, UserContext } from '../context' const ComUseContext = memo(() => { // 当他导入的context的数据发生变化时,这个组件也会重新渲染 const user = useContext(UserContext) const theme = useContext(ThemeContext) return ( <div> <h3>useContext的使用</h3> <p>UserContext -- user.name: {user.name}</p> <p>ThemeContext -- theme -- color: {theme.color}-fontSize: {theme.fontSize}</p> </div> ) }) export default ComUseContext // src/context.js import { createContext } from "react"; const UserContext = createContext() const ThemeContext = createContext() export { UserContext, ThemeContext }
useReducer
很多人看到useReducer
的第一反应应该是redux
的某个替代品其实并不是,useReducer
仅仅是useState
的一种替代方案:
-
在某些场景下,如果
state
的处理逻辑比较复杂,可以通过useReducer
来对其进行拆分 -
或者这次修改的
state
需要依赖之前的state
时,也可以使用 -
数据是不会共享的,只是使用了相同的
reducer
的函数而已,所以useReducer
只是useState
的一种替代品,并不能替代Redux
-
和
useState
相比较:jsimport React, { useReducer } from "react"; // 1. 定义 Reducer const reducer = (state, action) => { return { ...state, [action.field]: action.value }; }; // 2. 初始状态 const initialState = { username: "", email: "" }; function Form() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <input type="text" placeholder="用户名" value={state.username} onChange={(e) => dispatch({ field: "username", value: e.target.value })} /> <input type="email" placeholder="邮箱" value={state.email} onChange={(e) => dispatch({ field: "email", value: e.target.value })} /> <p>用户名: {state.username}</p> <p>邮箱: {state.email}</p> </div> ); } export default Form;
-
练习代码如下:
jsimport React, { memo, useReducer /* useState */ } from "react"; function reducer(state, action) { switch (action.type) { case "addOneCount": return { ...state, count: state.count + 1 }; case "subOneCount": return { ...state, count: state.count - 1 }; case "addCount": return { ...state, count: state.count + action.num }; case "subCount": return { ...state, count: state.count - action.num }; default: return { ...state }; } } const ComUseReducer = memo(() => { // const [count,setCount] = useState(0) // useReducer当定义多个数据时可以使用,但用的非常少,因为定义的reducer和组件分离了,如果数据很多时还不如直接使用redux省事 const [state, dispath] = useReducer(reducer, { count: 0, user: {}, banners: [], }); return ( <div> <h3>useReducer的使用</h3> {/* useReducer类似于useState的使用,逻辑复杂时使用 */} {/* <p>当前计数: {count}</p> <button onClick={e=>setCount(count + 15)}>+15</button> <button onClick={e=>setCount(count - 23)}>-23</button> <button onClick={e=>setCount(count + 1)}>+1</button> <button onClick={e=>setCount(count - 1)}>-1</button> */} <p>当前计数: {state.count}</p> <button onClick={(e) => dispath({ type: "addCount", num: 15 })}> +15 </button> <button onClick={(e) => dispath({ type: "subCount", num: 23 })}> -23 </button> <button onClick={(e) => dispath({ type: "addOneCount", num: 15 })}> +1 </button> <button onClick={(e) => dispath({ type: "subOneCount" })}>-1</button> </div> ); }); export default ComUseReducer;
useRef
useRef
返回一个ref
对象,返回的ref
对象在组件的整个生命周期保持不变,可以用于:
-
获取 DOM 元素 (类似
document.getElementById
) -
存储可变值 (修改
.current
不会触发组件重新渲染) -
持久化存储数据 (组件重新渲染时数据不会丢失,适用于存储
setTimeout
/setInterval
ID,避免定时器无法清除) -
const refContainer = useRef(initialValue)
-
initialValue
:初始值 -
refContainer.current
:存储的值,可随时读取或修改
-
-
练习代码如下:
js// component/ComUseRef.jsx import React, { memo, useEffect, useRef, useState } from "react"; const ComUseRef = memo(() => { const [count, setCount] = useState(0); const countRef1 = useRef(); const countRef2 = useRef(0); useEffect(() => { console.log(countRef1.current); console.log(countRef2.current); // 存储上一次的 count 值 }); const handleClick = () => { setCount(count + 5); // 触发重新渲染 countRef2.current += 1; // 不触发重新渲染 console.log("useRef 计数:", countRef2.current); }; return ( <div> <h3>useRef的使用</h3> {/* useRef有两个用法: 1. 获取dom 2. 保存一个数据,这个数据在整个生命周期中保持不变,用来解决闭包陷阱(参考ComUseCallback代码) */} <p ref={countRef1}>当前计数:{count}</p> <button onClick={handleClick}>+5</button> </div> ); }); export default ComUseRef;
useCallback
useCallback
是 React
的一个 性能优化 Hook
-
const memoizedCallback = useCallback(() => { // 你的函数逻辑 }, [依赖项])
-
fn
:要缓存的回调函数 -
[deps]
:依赖数组,只有 依赖变化时,才会重新创建函数 ,如果deps
为空[]
,函数只创建一次,之后不会再变
-
-
useCallback
会返回一个函数的记忆值,在依赖不变,多次定义的时候,返回的值是相同的 -
适用于需要传递给子组件或避免不必要重新渲染 的函数
-
通常使用
useCallback
目的是不希望子组件进行多次渲染,并不是为了函数进行缓存 -
只有在组件频繁渲染或者子组件因函数变化导致重新渲染时,才需要
useCallback
-
思考一: 使用
useCallback
和不使用useCallback
定义一个函数是否会带来性能的优化js// 没有useCallback,每次setCount触发更新时,handleClick都会重新创建影响性能,可能导致不必要的子组件重新渲染 import React, { useState } from "react"; function App() { const [count, setCount] = useState(0); // 每次组件重新渲染,handleClick 都会被重新创建! const handleClick = () => { setCount(count + 1); }; console.log("组件重新渲染,handleClick 重新创建"); return ( <div> <h1>计数:{count}</h1> <button onClick={handleClick}>+1</button> </div> ); } export default App // handleClick只有count变化时才会更新,否则不会重新创建,提高性能 import React, { useState, useCallback } from "react"; function App() { const [count, setCount] = useState(0); // 只有 count 变化时,handleClick 才会重新创建 const handleClick = useCallback(() => { setCount(count + 1); }, [count]); console.log("组件重新渲染,但 handleClick 只会在 count 变化时重新创建"); return ( <div> <h1>计数:{count}</h1> <button onClick={handleClick}>+1</button> </div> ); } export default App
-
思考二: 使用
useCallback
和不使用useCallback
定义一个函数传递给子组件是否会带来性能的优化js// 每次App组件渲染时,handleClick都是一个新函数,即使 `Child` 组件没有变化,仍然会重新渲染 import React, { useState } from "react"; function Child({ onClick }) { console.log("Child 组件重新渲染"); return <button onClick={onClick}>点击</button>; } function App() { const [count, setCount] = useState(0); // handleClick 每次都会重新创建,导致 Child 组件重新渲染! const handleClick = () => { setCount(count + 1); }; return ( <div> <h1>计数:{count}</h1> <Child onClick={handleClick} /> </div> ); } export default App import React, { useState, useCallback } from "react"; // 1️⃣ 使用 React.memo 让子组件避免不必要的重新渲染 const Child = React.memo(({ onClick }) => { console.log("Child 组件渲染"); return <button onClick={onClick}>点击</button>; }); function App() { const [count, setCount] = useState(0); // 2️⃣ 使用 useCallback,让 onClick 在 count 不变时不会重新创建 const handleClick = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); return ( <div> <h1>计数:{count}</h1> <Child onClick={handleClick} /> </div> ); } export default App
-
useCallback
什么时候不需要用?-
函数本身不需要缓存(例如简单
onClick
事件) -
组件不会频繁重新渲染(比如只有
useState
而无子组件) -
滥用
useCallback
可能会让代码更复杂,而没有明显优化
-
-
练习代码如下:
js// component/ComUseCallback.jsx import React, { memo, useCallback, useRef, useState } from "react"; // 当子组件没有被memo包裹时,父组件的useCallback也是无性能优化的 const ComUseCallbackSon = memo((props) => { console.log("ComUseCallbackSon渲染"); // 当props中的属性发生变化时,子组件就会重新渲染 const { changeCountCall } = props; return ( <div> <button onClick={changeCountCall}>+5</button> </div> ); }); const ComUseCallback = memo(() => { console.log("ComUseCallback渲染"); /* useCallback 有以下需要明白: 1. useCallback会返回一个函数的记忆值 2. 在依赖不改变的情况下,不返回新的函数地址而返回旧的函数地址,多次定义的时候返回的值是一样的 3. 他是函数缓存的,但是函数被包裹后无法阻止组件render时函数的重新创建 4. 在往子组件传入了一个函数并且子组件被momo缓存了的时候使用 5. 使用的目的不是为了函数进行缓存,而是防止子组件多次渲染 6. 滥用还会增加寻找指定函数和校验依赖是否改变的负担 7. 下面我们来一一练习 */ const [count, setCount] = useState(150); const [message, setMessage] = useState(150); /* 没有任何依赖这个就会有闭包陷阱的问题,count的值只会改变一次 因为当点击按钮时,回调函数中引用的 count 变量是在 useCallback 内部的闭包中。 因此,每次count变化时,useCallback创建一个新的回调函数,但该回调函数仍然引用最初创建时的count值 这时我们就想到了添加依赖[count],这是可以的, 但是当count发生变化时就会产生一个全新的事件引用,就失去了使用useCallback缓存函数的意义 */ /* let changeCountCall = useCallback(()=> { setCount(count + 5) },[]) */ // 闭包陷阱解决方案,这时传给子组件的就是一个函数,就算点击函数变化时子组件也不会更新 const countRef = useRef(); countRef.current = count; let changeCountCall = useCallback(() => { console.log("changeCountCall"); setCount(countRef.current + 5); // 不使用useRef,也可以使用函数式更新setCount来确保使用最新的prevCount值进行递增 // react新文档做了解释 https://react.dev/learn/queueing-a-series-of-state-updates setCount((prevCount) => prevCount + 5); }, []); /* 只要render更新组件重新渲染,则下面函数每次都会重新构建 如果吧这个函数传给子组件,那么当每次执行这个函数时, 就算子组件没有依赖count值时子组件都会渲染,因为子组件的props改变了 */ function changeCount(num) { setCount(count - num); } return ( <div> <h3>useCallback的使用</h3> <p>当前计数:{count}</p> <p>当前message:{message}</p> <button onClick={changeCountCall}>+5</button> <button onClick={(e) => changeCount(14)}>-14</button> <button onClick={(e) => setMessage(Math.random())}>修改message</button> <ComUseCallbackSon changeCountCall={changeCountCall} /> </div> ); }); export default ComUseCallback
useMemo
useMemo
是 React
的一个性能优化 Hook
,用于缓存计算结果,避免不必要的重复计算
-
const memoizedValue = useMemo(() => { return 计算逻辑 }, [依赖项])
-
fn
:要执行的计算逻辑 -
[deps]
:依赖数组,只有依赖变化时,才会重新计算 -
返回值:缓存的计算结果,在依赖不变的情况下,多次定义的时候,返回的值是相同的
-
-
适用于计算开销大的逻辑(如排序、筛选、计算总和),也可以优化
React.memo
组件的props
计算,避免组件每次渲染都重新计算 -
useMemo
什么时候不需要用?-
简单计算 (如
a + b
这种计算) -
没有性能问题的情况下(避免不必要的优化,代码更复杂)
-
-
和
useCallback
对比如下: -
练习代码如下:
js// component/ComUseMemo.jsx import React, { memo, useMemo, useState } from 'react' const ComUseMemoSon = memo((props)=> { console.log('ComUseMemoSon渲染'); return <div> ComUseMemoSon-userName: {props.user.name}-userPhone: {props.user.phone} </div> }) const ComUseMemo = memo(() => { const [count, setCount] = useState(0) /* useCallback(fn, [])相当于useMemo(() => fn, []) useCallback: 返回的值是回调函数 useMemo: 返回的值是回调函数中return的值 */ /* 和useCallback包裹一样,将changeCountMemo传给子组件, 改变时子组件也不会渲染,若将changeCount传过去就会渲染 */ const changeCountMemo = useMemo(()=> {return changeCount}, []) function changeCount() { setCount((pre)=> pre + 5) } const totalCount = useMemo(()=> {return getTotalCount(50)}, []) function getTotalCount(count) { let total = 0 for(let i = 0; i <= count; i++) { total = total + i } return total } const user = useMemo(()=> ({name: 'han', phone: '1234567890'}),[]) return ( <div> <h3>useMemo的使用</h3> <p>当前计数:{count}</p> <button onClick={changeCountMemo}>+5</button> <p>当前count总数: {totalCount}</p> <ComUseMemoSon changeCountMemo={changeCountMemo} user={user} /> </div> ) }) export default ComUseMemo
useImperativeHandle
useImperativeHandle
是 React
提供的一个 Hook
,用于自定义 ref
的暴露方法,通常和 forwardRef
搭配使用
-
useImperativeHandle(ref, () => ({ 方法1, 方法2, ... }), [依赖项])
-
ref
:传入的ref
,由forwardRef
提供 -
返回值是一个对象 ,定义了该
ref
可以暴露的方法 -
只有 依赖项
[deps]
发生变化时,才会重新创建方法
-
-
ref
和forwardRef
结合使用 :通过forwardRef
可以将ref
转发到子组件,子组件拿到父组件中创建的ref
,绑定到自己的某一个元素中 -
以前的
ref
和forwardRef
的做法本身没有什么问题,但是将子组件的DOM
直接暴露给了父组件 -
useImperativeHandle
主要用于限制父组件访问子组件的功能,而不是直接暴露整个ref
-
练习代码如下:
js// component/ComUseImperativeHandle.jsx import React, { forwardRef, memo, useImperativeHandle, useRef } from 'react' const ComUseImperativeHandleSon = memo(forwardRef((props,ref)=> { const inputSonRef = useRef() useImperativeHandle(ref, ()=> { // 要子组件再重新定义ref然后return给父组件的ref,只要return出去的方法父组件才可以使用 return { focus() { inputSonRef.current.focus() }, // 修改子组件输入值的方法 setValue(value) { inputSonRef.current.value = value }, // 获取子组件输入值的方法 getValue() { return inputSonRef.current.value } } }) return <input ref={inputSonRef} type="text" /> })) const ComUseImperativeHandle = memo(() => { /* useImperativeHandle通常结合在子组件被forwardRef包裹, 父组件可以操作子组件ref时使用,限制父组件可以使用的范围 */ const inputRef = useRef() function inputFocus() { console.log(inputRef.current.getValue()); inputRef.current.focus() inputRef.current.setValue(Math.random()) } return ( <div> <h3>useImperativeHandle的使用</h3> <ComUseImperativeHandleSon ref={inputRef} /> <button onClick={e=>inputFocus()}>操作ComUseImperativeHandleSon</button> </div> ) }) export default ComUseImperativeHandle
自定义Hook
自定义Hook
本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React
的特性`
-
导出练习的
hooks
:创建hooks/index.js
jsimport useLogLift from "./useLogLift"; import usePublicContext from "./usePublicContext"; import usePageScroll from "./usePageScroll"; import useLocalStorage from "./useLocalStorage"; export { useLogLift, usePublicContext, usePageScroll, useLocalStorage };
-
练习一:所有的组件在创建和销毁时都进行打印
js// hooks/useLogLift.js import { useEffect, useLayoutEffect } from "react"; function useLogLift(name) { useLayoutEffect(() => { console.log(name, "组件mount之前"); }, [name]); useEffect(() => { console.log(name, "组件mount完成"); return () => { console.log(name, "组件unMount之前"); }; }, [name]); } export default useLogLift; // component/MyUseLogLift.jsx import React, { memo, useState } from 'react' import { useLogLift } from '../hooks' const MyUseLogLiftSon1 = memo(() => { useLogLift('MyUseLogLiftSon1') return ( <div>MyUseLogLiftSon1</div> ) }) const MyUseLogLiftSon2 = memo(() => { // useLayoutEffect(()=> { // console.log('组件mount之前'); // },[]) // useEffect(()=> { // console.log('组件mount完成'); // return ()=> { // console.log('组件unMount之前'); // } // },[]) useLogLift('MyUseLogLiftSon2') return ( <div>MyUseLogLiftSon2</div> ) }) const MyUseLogLift = memo(() => { const [isShow, setIsShow] = useState(true) function changeIsShow() { setIsShow(!isShow) } // useLayoutEffect(()=> { // console.log('组件mount之前'); // },[]) // useEffect(()=> { // console.log('组件mount完成'); // return ()=> { // console.log('组件unMount之前'); // } // },[]) useLogLift('MyUseLogLift') return ( <> <h4>打印生命周期</h4> <MyUseLogLiftSon1 /> {isShow && <MyUseLogLiftSon2 />} <button onClick={e=>changeIsShow()}>切换卸载组件</button> </> ) }) export default MyUseLogLift
-
练习二:
Context
的共享js// hooks/usePublicContext.js import { useContext } from "react"; import { UserContext, ThemeContext } from "../context"; function usePublicContext() { const user = useContext(UserContext); const theme = useContext(ThemeContext); return [user, theme]; } export default usePublicContext; // component/MyPublicContext.jsx import React, { memo } from 'react' import { usePublicContext } from '../hooks' const MyPublicContext = memo(() => { const [user, theme] = usePublicContext() return ( <div> MyPublicContext <p>{user.name}</p> <p>{theme.color}</p> <p>{theme.fontSize}</p> </div> ) }) export default MyPublicContext
-
练习三:获取滚动位置
js// hooks/usePageScroll.js import { useEffect, useState } from "react"; function usePageScroll() { let [scrollX, setScrollX] = useState(0); let [scrollY, setScrollY] = useState(0); useEffect(() => { function handleScroll() { setScrollX(window.scrollX); setScrollY(window.scrollY); } window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); }; }, []); return [scrollX, scrollY]; } export default usePageScroll; // component/MyPageScroll.jsx import React, { memo } from 'react' import { usePageScroll } from '../hooks' const MyPageScroll = memo(() => { // useEffect(()=> { // function handleScroll() { // console.log('handleScroll', window.scrollX, window.scrollY); // } // window.addEventListener('scroll', handleScroll) // return ()=>{ // window.removeEventListener('scroll', handleScroll) // } // }) const [scrollX,scrollY] = usePageScroll() return ( <div>MyPageScroll--scrollX:{scrollX}--scrollY:{scrollY}</div> ) }) export default MyPageScroll
-
练习四:
localStorage
数据存储js// hooks/useLocalStorage.js /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useState } from "react"; function useLocalStorage(key) { // 1.从localStorage中获取数据, useState可以传入一个回调函数 const [storage, setStorage] = useState(() => { return !localStorage.getItem(key) ? "" : JSON.parse(localStorage.getItem(key)); }); // 2.监听data改变, 一旦发生改变就存储data最新值 useEffect(() => { localStorage.setItem(key, JSON.stringify(storage)); }, [storage]); // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值 return [storage, setStorage]; } export default useLocalStorage; // component/MyUseLocalStorage.jsx import React, { memo } from "react"; import useLocalStorage from "../hooks/useLocalStorage"; const MyUseLocalStorage = memo(() => { const [storage, setStorage] = useLocalStorage("token"); return ( <div> MyUseLocalStorage-{storage} <button onClick={(e) => setStorage(Math.random())}>修改storage值</button> </div> ); }); export default MyUseLocalStorage;
redux hooks
Redux
提供了一些 React Hooks
让我们在函数组件中更方便地使用 Redux
状态管理,最常用的主要有以下几个:
-
useStore
用得比较少,通常useSelector
就足够了,它可以用来 直接访问Redux store
(而不是state
),适用于 需要手动访问整个 store 的情况,一般调试时用jsimport { useStore } from "react-redux"; function Debugger() { const store = useStore(); console.log("Redux Store:", store.getState()); // 获取完整 state return <div>打开控制台查看 Redux store</div>; } export default Debugger;
-
useSelector
用于 从 Redux store 中获取 state ,类似于mapStateToProps
(在connect
中)-
参数一 :将
state
映射到需要的数据中,会订阅 store,当 state 发生变化时,组件会重新渲染 -
参数二 :可以进行比较来决定是否组件重新渲染,
const refEquality = (a, b) => a === b
,必须返回两个完全相等的对象才可以不引起重新渲染
-
-
useDispatch
用于分发action
来更新Redux state
,是直接获取dispatch
函数 ,之后在组件中直接使用即可,相当于mapDispatchToProps
-
练习代码如下:
js// store/modules/message.js import { createSlice } from "@reduxjs/toolkit"; const messageSlice = createSlice({ name: "message", initialState: { message: "", }, reducers: { changeMessageAction(state, { payload }) { state.message = payload; }, }, }); export const { changeMessageAction } = messageSlice.actions; export default messageSlice.reducer; // store/modules/token.js import { createSlice } from "@reduxjs/toolkit"; const tokenSlice = createSlice({ name: "token", initialState: { token: "937539r-395639", }, reducers: { changeTokenAction(state, { payload }) { state.token = payload; }, }, }); export const { changeTokenAction } = tokenSlice.actions; export default tokenSlice.reducer; // component/ComReduxHooks.jsx import React, { memo } from 'react' import { shallowEqual, useDispatch, useSelector, /* useStore */ } from 'react-redux' import { changeTokenAction } from '../store/modules/token' import { changeMessageAction } from '../store/modules/message'; const ComReduxHooksSon = memo(()=>{ console.log('ComReduxHooksSon渲染'); /* 当不写shallowEqual时,只要是store里面的值改变,不管是不是message改变都会重新渲染, 比如父组件的token改变时也会重新渲染 当写shallowEqual时,则只会在message改变时子组件再渲染 */ const {message} = useSelector((state)=> ({ message: state.message.message }),shallowEqual) const dispatch = useDispatch() function setMessage() { dispatch(changeMessageAction('你好呀')) } return ( <div> <p>ComReduxHooksSon-message{message}</p> <button onClick={e=>setMessage()}>修改message</button> </div> ) }) const ComReduxHooks = memo(() => { console.log('ComReduxHooks渲染'); /* 参数一:将state映射到需要的数据中; 参数二:可以进行比较来决定是否组件重新渲染 默认会比较我们返回的两个对象是否相等,也就是我们必须返回两个完全相等的对象才可以不引起重新渲染; */ /* 当不写shallowEqual时,只要是store里面的值改变不管是不是token改变都会重新渲染, 比如子组件的message改变时也会重新渲染 当写shallowEqual时,则只会在token改变时组件再渲染 */ const { token } = useSelector((state)=> ({ token: state.token.token }), shallowEqual) // const store = useStore() // 可以拿到整个store对象 const dispath = useDispatch() // console.info(store, token, dispath); function setToken() { dispath(changeTokenAction(Math.random()+100)) } return ( <div> <h3>ComReduxHooks</h3> <p>{token}</p> <button onClick={e=>setToken()}>设置token</button> <ComReduxHooksSon /> </div> ) }) export default ComReduxHooks
react18 hooks
React 18
引入了一些新的Hooks
,同时优化了一些现有的 Hooks
,使得并发渲染和性能优化更加容易
useTransition
官方解释:返回一个状态值,表示过渡任务的等待状态,以及一个启动该过渡任务的函数,简单解释如下:
-
用于 状态更新的优先级管理 ,将部分更新标记为低优先级,避免
UI
卡顿 -
适用于 搜索过滤、大数据列表渲染、动画 等场景
-
练习代码如下:
js// component/ComUseTransition.jsx import React, { memo, useState, useTransition } from 'react' import array from '../array.js' const ComUseTransition = memo(() => { const [names, setNames] = useState(array) /* 这一段代码若把性能的cpu降一下就会发现,删除input的值时会删的很慢出现卡顿 这时我们想要的结果是,input的值的增删快速的反应给用户,列表的过滤可以稍后完成 */ /* function changeInputValue(e) { setNames(array.filter((f)=> {return f.includes(e.target.value)})) } */ /* useTransition告诉react对于某部分任务的更新优先级较低,可以稍后进行更新 返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数 */ const [pending, startTransition] = useTransition() function changeInputValue(e) { // 会在其他改变之后 startTransition(()=> { setNames(array.filter((f)=> {return f.includes(e.target.value)})) }) } return ( <div> <h3>useTransition的使用</h3> <input type="text" placeholder='输入值过滤数组' onChange={changeInputValue} /> {pending && <h4>数据加载中...</h4>} {!names.length && <h4>暂无符合数据</h4>} <ul> { names.map((item,index)=> { return <li key={index}>{item}</li> }) } </ul> </div> ) }) export default ComUseTransition
useDeferredValue
官方解释 :useDeferredValue
接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后
-
useDeferredValue
用于延迟某个值的更新,而不影响其他状态更新 -
适用于 搜索框联动渲染、复杂计算等
-
练习代码如下:
js// component/ComUseDeferredValue.jsx import React, { memo, useDeferredValue, useState } from 'react' import array from '../array' const ComUseDeferredValue = memo(() => { const [names, setNames] = useState(array) /* function changeInputValue(e) { setNames(array.filter((f)=> f.includes(e.target.value))) } */ /* useDeferredValue的使用和useTransition同理都是更新延迟 单useDeferredValue不会有pending状态 参数: value:你想延迟的值,可以是任何类型。 */ const list = useDeferredValue(names) function changeInputValue(e) { setNames(array.filter((f)=> f.includes(e.target.value))) } return ( <div> <h3>useDeferredValue的使用</h3> <input type="text" placeholder='输入值过滤数组' onChange={changeInputValue} /> {!list.length && <h4>暂无符合数据</h4>} <ul> { list.map((item,index)=> { return <li key={index}>{item}</li> }) } </ul> </div> ) }) export default ComUseDeferredValue // src/array.js import { faker } from "@faker-js/faker"; let list = []; for (let i = 0; i < 10000; i++) { list.push(faker.internet.userName()); } export default list;
useId
官方的解释 :useId
是一个用可以保证应用程序在客户端和服务器端生成唯一的ID
,同时避免 hydration
不匹配的 hook
,这里需要先理解一些服务器端渲染(SSR
)的概念和hydration
-
useId
是用于react
的同构应用开发的,前端的SPA
页面并不需要使用它 -
练习代码如下:
js// component/ComUseId.jsx import React, { memo, useId } from 'react' const ComUseId = memo(() => { // 服务端渲染时使用userId,要了解ssr和同构应用和Hydration // useId会生成唯一的ID,让其在服务端渲染时和客户端渲染时的id一致 const id = useId() return ( <div> <h3 id={id}>ComUseId的使用-id-{id}</h3> </div> ) }) export default ComUseId
SSR
同构应用
-
SSR
(Server-Side Rendering
,服务端渲染) 是React
应用的一种渲染模式,指的是在服务器端预渲染页面的HTML
,再发送给客户端,以提高首屏加载速度和SEO
友好性。 -
同构(
Isomorphic/Universal
) 应用指的是同一份React
代码可以同时运行在服务器端(Node.js
)和客户端(浏览器),提供流畅的用户体验 -
同构是一种
SSR
的形态,是现代SSR
的一种表现形式,核心特点如下:-
服务器端 :使用
ReactDOMServer.renderToString()
在 Node.js 中预渲染 HTML -
客户端 :使用
ReactDOM.hydrate()
在浏览器端接管 React
-
-
基本流程如下:
-
浏览器请求 Node.js 服务器
-
服务器端渲染 HTML 并返回(首屏可见)
-
客户端加载 React 组件 ,使用
hydrate()
让组件变得可交互 -
之后的路由更新由 客户端接管
-
Hydration
什么是Hydration
?这里引入vite-plugin-ssr
插件的官方解释:
-
Hydration
(水合) 是React SSR
(服务端渲染)中的一个关键过程 ,它指的是:-
服务器端 生成
HTML
并发送到客户端 -
客户端 加载
React
组件,并在已有的HTML
结构上重新绑定事件和状态,让页面变得可交互
-
-
简单理解如下:
-
SSR
负责 首次渲染HTML
(提高首屏速度、SEO
友好)。 -
Hydration
让 静态HTML
变为动态可交互 (React
组件接管)
-