React文檔
Hooks:useState蜓席、useEffect呀枢、useLayoutEffect缕减、useContext、useReducer捞奕、useMemo牺堰、React.memo、callCallback颅围、useRef伟葫、useImperativeHandle、自定義Hook院促、useDebugValue
useState(最常用)
在React的函數(shù)組件里筏养,默認(rèn)只有屬性,沒有狀態(tài)常拓。
1.使用狀態(tài)
//數(shù)組第1項(xiàng)是讀接口渐溶,第2項(xiàng)是寫接口,初始值0
const [n,setN] = React.useState(0) //數(shù)字
const [user,setUser] = React.useState({name:'F'}) //對象
2.注意事項(xiàng)(1):不可局部更新
更新部分屬性時弄抬,未更新的屬性會消失茎辐。
3.注意事項(xiàng)(2):地址要變
setState(obj)如果obj對象地址不變,那么React就認(rèn)為數(shù)據(jù)沒有變化,因此不會幫你改變內(nèi)容拖陆。
4.useState接受函數(shù)
5.setState接受函數(shù)
例1:不可局部更新
如果state是個對象弛槐,能否部分setState?
不行,因?yàn)閟etState不會幫我們合并屬性慕蔚。所以當(dāng)只更新部分屬性時,未更新的屬性就會消失斋配。
那怎么解決"未更新的屬性會消失"的問題孔飒?
用...
拷貝之前所有的屬性,然后再覆蓋屬性艰争。
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
setUser({
...user, //拷貝user的所有屬性
name: 'Jack' //覆蓋name
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
題外話:useReducer
也不會合并屬性坏瞄,React新版的所有東西都不會幫你合并,它認(rèn)為這是你自己要做的事甩卓。
例2.地址要變
我想把name改下:于是直接修改user.name然后setUser(user)
你會發(fā)現(xiàn)改不了鸠匀,因?yàn)槟愀牡氖峭粋€對象,地址是一樣的逾柿。
React不會看你里面的內(nèi)容它只看地址缀棍,你不改地址它就不幫你改內(nèi)容。
那怎么改地址机错?
const onClick=()=>{
user.name="小李"
setUser(user)
}
const onClick=()=>{ //改地址
setUser({ //新的對象
...user,
name:"小李"
})
}
例3.useState接受函數(shù)(很少用)
引用狀態(tài)爬范,可用函數(shù),但很少會這樣寫,多算一遍就多算唄弱匪。
useState寫成函數(shù)的好處是:減少多余的計算過程青瀑,因?yàn)镴S引擎不會立即執(zhí)行函數(shù)。
function App() {
const [user,setUser]=useState({name:'Frank', age: 9+9})//引用狀態(tài)
//useState(()=>( {name:'Frank', age: 9+9} ))
const onClick = ()=>{
setUser({ ... }) //設(shè)置狀態(tài)
}
例4.setState接受函數(shù)(推薦優(yōu)先使用函數(shù))
點(diǎn)擊button后你會發(fā)現(xiàn)n=1
而不是2萧诫,因?yàn)楫?dāng)你setN(n+1)時斥难,n不會變。
不管你做多少次計算帘饶,只有最后一次有用哑诊。
解決方法: 改成函數(shù)
function App() {
const [n, setN] = useState(0)
const onClick = ()=>{
//setN(n+1) 第1次計算
//setN(n+1) 第2次計算,也是最后1次計算
setN(n => n + 1) //形式化的操作
setN(n => n + 1)
}
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+2</button>
</div>
);
}
JS語法有問題:對象必須加()及刻。(JS的bug)
總結(jié):對state進(jìn)行多次操作時搭儒,優(yōu)先使用函數(shù)。
useReducer(最常用)
useReducer4步走:
1.創(chuàng)建初始值initicalState
const initical = { n:0 }
2.創(chuàng)建所有操作reducer(state,action)
reducer接受2個參數(shù):舊的狀態(tài)state和操作的類型action(一般是類型)提茁,最后返回新的state淹禾。
怎么得到新的state?
看下動作的的類型是什么
規(guī)則和useState一樣,必須返回新的對象茴扁。(不能直接操作n)
const reducer=(state,action)=>{
if(action.type==='add'){
return { n:state.n+1 } //return新對象
}else if(action.type==='mult'){
return { n:state.n*2 }
}else{
console.log("unknown type")
}
}
3.傳給useReducer,得到讀和寫API
(1)需要導(dǎo)入useReducer或者直接使用全稱React.useReducer
(2)useReducer接收2個參數(shù):所有操作reducer和初始狀態(tài)initical
(3)你將得到讀API铃岔、寫API
寫API一般叫dispatch,因?yàn)槟惚仨毻ㄟ^reducer才能setState,所以叫dispatch。
import React,{useReducer} from "react"
function App(){
const [state,dispatch]=useReducer(reducer,initical)
}
拿出屬性n的2種方法: 1' {state.n}
2'const {n}=state
然后{n}
4.調(diào)用 寫({type:'操作類型'})
const onClick=()=>{
dispatch({
type:'add' //調(diào)用reducer的add操作
})
}
相當(dāng)于useState,只不過把所有操作聚攏在一個函數(shù)里,這樣的好處是:調(diào)用的代碼簡短了毁习。
調(diào)用傳參:+2
時傳了參數(shù)number:2
,那么reducer里的1
就可以變成一個參數(shù)智嚷。因?yàn)閐ispatch()里傳的對象就是action。
if (action.type === "add") {
//return { n: state.n + 1 };
return { n: state.n + action.number };
}
...
const onClick2 = () => {
//dispatch({type:'add'})
dispatch({type:'add',number:2}) //里面的對象就是action
}
這就是useReducer對useState的升級操作,總的來說useReducer是useState的復(fù)雜版纺且。好處是用來踐行React社區(qū)一直推崇的flux/Redux思想盏道。隨著hooks的流行這個思想會退化。
完整代碼
import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";
const initial = { n: 0};
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
如何選擇 使用useReducer還是useState?
事不過三原則
如果你發(fā)現(xiàn)有幾個變量應(yīng)該放一起(對象里)這時候就用useReducer對對象進(jìn)行整體的操作载碌。
const initFormData = {
name: "",
age: 18,
nationality: "漢族"
};
function reducer(state, action) {
switch (action.type) {
case "patch": //更新
//把第1個對象的所有屬性和第2個對象的所有屬性全部放到第3個空對象里猜嘱,這就是更新
return { ...state, ...action.formData };
case "reset": //重置,返回最開始的對象
return initFormData;
default:
throw new Error("你傳的啥 type 呀");
}
}
function App() {
const [formData, dispatch] = useReducer(reducer, initFormData);
// const patch = (key, value)=>{
// dispatch({ type: "patch", formData: { [key]: value } })
// }
const onSubmit = () => {};
const onReset = () => {
dispatch({ type: "reset" });
};
return (
<form onSubmit={onSubmit} onReset={onReset}>
<div>
<label>
姓名
<input value={formData.name} onChange={e => dispatch(
{type:"patch", formData:{ name: e.target.value }})
}
/>
</label>
</div>
<div>
<label>
年齡
<input value={formData.age} onChange={e =>dispatch(
{type:"patch",formData: { age: e.target.value }})
}
/>
</label>
</div>
<div>
<label>
民族
<input value={formData.nationality}
onChange={e => dispatch({type:"patch",
formData:{nationality: e.target.value}})
}
/>
</label>
</div>
<div>
<button type="submit">提交</button>
<button type="reset">重置</button>
</div>
<hr />
{JSON.stringify(formData)}
</form>
);
}
用戶一旦輸入就會觸發(fā)onChange事件嫁艇。用戶輸入即更新朗伶,因?yàn)閮?nèi)容不一樣了嘛。
每次更新步咪,App都會render遍论皆。
[圖片上傳失敗...(image-e51e4c-1651443540127)]
如何用useReducer代替Redux ?
前提:你得知道Redux是什么
用React的reducer
+context
即可代替Redux猾漫。
import React, { useReducer, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
const store = { //第1步.將數(shù)據(jù)集中在一個store對象
user: null,
books: null,
movies: null
};
function reducer(state, action) { //第2步.將所有操作集中在reducer
switch (action.type) {
case "setUser":
return { ...state, user: action.user };
case "setBooks":
return { ...state, books: action.books };
case "setMovies":
return { ...state, movies: action.movies };
default:
throw new Error();
}
}
const Context = React.createContext(null); //第3步.創(chuàng)建一個Context
function App() {
const [state, dispatch] = useReducer(reducer, store); //第4步.創(chuàng)建對數(shù)據(jù)的讀寫API
const api = { state, dispatch };
return (
<Context.Provider value={api}> //第5步.將創(chuàng)建的"數(shù)據(jù)的讀寫API"放到Context
<User /> //第6步.用Context.Provider將Context提供給所有組件,就是將組件放里面
<hr />
<Books />
<Movies />
</Context.Provider>
);
}
function User() {
const { state, dispatch } = useContext(Context); //第7步.各個組件用useContext獲取讀寫API
useEffect(() => {
ajax("/user").then(user => {
dispatch({ type: "setUser", user: user });
});
}, []);
return (
<div>
<h1>個人信息</h1>
<div>name: {state.user ? state.user.name : ""}</div>
</div>
);
}
function Books() {
const { state, dispatch } = useContext(Context);//第7步.使用useContext獲取讀寫API
useEffect(() => {
ajax("/books").then(books => {
dispatch({ type: "setBooks", books: books });
});
}, []);
return (
<div>
<h1>我的書籍</h1>
<ol>
{state.books ? state.books.map(book =>
<li key={book.id}>{book.name}</li>) : "加載中"}
</ol>
</div>
);
}
function Movies() {
const { state, dispatch } = useContext(Context);//使用useContext獲取讀寫API
useEffect(() => {
ajax("/movies").then(movies => {
dispatch({ type: "setMovies", movies: movies });
});
}, []);
return (
<div>
<h1>我的電影</h1>
<ol>
{state.movies ? state.movies.map(movie =>
<li key={movie.id}>{movie.name}</li>)
: "加載中"}
</ol>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// 幫助函數(shù)
// 假 ajax
// 兩秒鐘后点晴,根據(jù) path 返回一個對象,必定成功不會失敗
function ajax(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path === "/user") {
resolve({
id: 1,
name: "Frank"
});
} else if (path === "/books") {
resolve([
{
id: 1,
name: "JavaScript 高級程序設(shè)計"
},
{
id: 2,
name: "JavaScript 精粹"
}
]);
} else if (path === "/movies") {
resolve([
{
id: 1,
name: "愛在黎明破曉前"
},
{
id: 2,
name: "戀戀筆記本"
}
]);
}
}, 2000);
});
}
解析
第1步.將數(shù)據(jù)集中在一個store對象
const store = { //加載信息
user:null,
books:null,
movies:null
}
第2步.將所有操作集中在reducer
接收一個舊的狀態(tài)悯周,給我一個操作觉鼻,我就可以得到一個新的狀態(tài)。
怎么得到新的狀態(tài)呢队橙?
看你操作的類型是什么坠陈。
比如說你要填充user:你得給我一個user,所以你的action里面要有一個user。我把你給我的user傳到store上捐康。
const reducer = (state,action) => {
switch(action.type){
case 'setUser': //填充user
return {...state,user:action.user};
case 'setBooks':
return {...state,books:action.books};
case 'setMovies':
return {...state,movies:action.movies};
default:
throw new Error();
}
}
第3步.創(chuàng)建一個Context
createContext需要自動引入或者直接React.createContext
const Context = React.createContext(null) //初始值一般是null仇矾,不傳會報錯
第4步.創(chuàng)建對數(shù)據(jù)的讀寫API
useReducer
的第2個參數(shù)是初始值。
useReducer
一般寫在函數(shù)里面,只能在函數(shù)里面運(yùn)行解总。
const Context = React.createContext(null)
function App() {
const [state,dispatch]=useReducer(reducer,store) //(reducer,初始值)
}
//也可以寫在外面贮匕,不過要在函數(shù)里調(diào)用。
//function x(){ const [state,dispatch]=useReducer(reducer,store) }
//function App() {
// x()
//}
第5步.將創(chuàng)建的"數(shù)據(jù)的讀寫API"放到Context
方法:把<div>
刪了改為<Context.Provider>
花枫,value就是把讀寫API[state,dispatch]
賦值給Context.Provider
刻盐。
語法:value={JS}
告訴React里面是JS。{state:state,dispatch:dispatch}
這個{}里才是對象,對象的state就是上面的state變量劳翰,對象的dispatch就是上面的dispatch變量敦锌。
const Context = React.createContext(null)
function App() {
const [state,dispatch]=useReducer(reducer,store)
return (
<Context.Provider value = {{state:state,dispatch:dispatch}}>
<User />
<hr />
<Books />
<Movies />
</Context.Provider>
)
value={{state:state,dispatch:dispatch}}
ES6可以直接縮寫成value={{state,dispatch}}
第6步.用Context.Provider將Context提供給所有組件
就是將組件<User />、<Books />佳簸、<Movies />
放到<Context.Provider>
里面
return (
<Context.Provider value = {{state:state,dispatch:dispatch}}>
<User />
<hr />
<Books />
<Movies />
</Context.Provider>
)
第7步.各個組件用useContext獲取讀寫API
現(xiàn)在各個組件就可以使用讀寫API了
useContext接收的值就是你創(chuàng)建的Context
import React, { useReducer, useContext, useEffect } from "react";
function User(){
const {state,dispatch} = useContext(Context) //注意這里是{}
ajax("/user").then((user)=>{ //初始化user:調(diào)用ajax()
//dispatch觸發(fā)"setUser",user的值就是得到的user,形參占位
dispatch({type:"setUser",user:user})
})
return (
<div>
<h1>個人信息</h1>
//展示
<div>name:{state.user ? state.user.name : ""}</div>
</div>
)
}
由誰來設(shè)置一開始的值呢乙墙?
一開始是null,所以name是空的。
用假的ajax獲取用戶信息,很簡單的promise。
// 幫助函數(shù),假的ajax
// 2s后,根據(jù) path 返回一個對象听想,必定成功不會失敗
function ajax(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path === "/user") {
resolve({
id: 1,
name: "Frank"
});
} else if (path === "/books") {
resolve([
{
id: 1,
name: "JavaScript 高級程序設(shè)計"
},
{
id: 2,
name: "JavaScript 精粹"
}
]);
} else if (path === "/movies") {
resolve([
{
id: 1,
name: "愛在黎明破曉前"
},
{
id: 2,
name: "戀戀筆記本"
}
]);
}
}, 2000);
});
}
知識點(diǎn)
1.useEffect設(shè)置只在第一次渲染時執(zhí)行某函數(shù)
每次User刷新時,代碼setStatedispatch
就會再執(zhí)行一遍并重復(fù)請求ajax腥刹。
怎樣減少請求ajax,設(shè)置只在第一次進(jìn)入頁面時請求?
借助useEffect
需要自動引入或者直接React.useEffect
useEffect需要傳個函數(shù),當(dāng)?shù)?個參數(shù)是空數(shù)組時汉买,那么前面的函數(shù)就只會在第一次渲染時執(zhí)行衔峰,之后永遠(yuǎn)不會執(zhí)行。例子:
React.useEffect(()=>{},[])
項(xiàng)目代碼
import React, { useReducer, useContext, useEffect } from "react";
function User() {
const { state , dispatch } = react.useContext(Context)
useEffect(()=>{
ajax("/user").then((user)=>{
dispatch({type:"setUser",user:user})
})
},[])
}
請求user數(shù)據(jù)ajax("/user")
蛙粘,得到user數(shù)據(jù)后(這里的user是形參)垫卤,用setUser把數(shù)據(jù)user:user
放到上下文Context
里面。然后它自己就會刷新了组题,不用手動調(diào)自己刷新葫男,因?yàn)镽eact知道state變了就要變了抱冷。
2.加載中怎么做的崔列?
如果movies存在就展示n個<li>
,如果不存在就展示"加載中"
function Movies() {
const { state, dispatch } = useContext(Context);//使用useContext獲取讀寫API
useEffect(() => {
ajax("/movies").then(movies => {
dispatch({ type: "setMovies", movies: movies });
});
}, []);
return (
<div>
<h1>我的電影</h1>
<ol>
{state.movies ? state.movies.map(movie =>
<li key={movie.id}>{movie.name}</li>)
: "加載中"}
</ol>
</div>
);
}
總結(jié)
用useReducer代替Redux,是如何實(shí)現(xiàn)代替的旺遮?
1.redux有個store赵讯,我們對象代替了const store={}
2.redux有個reducer,我們用函數(shù)代替了function reducer(state,action){}
3.redux它可以在任意地方使用耿眉,我們用Context代替了const Context=React.createContext(null)
非常好的代替redux的方法边翼。
如何模塊化?
模塊化不屬于React內(nèi)容,屬于基礎(chǔ)知識鸣剪。
模塊就是文件组底,文件就是模塊,文件名小寫筐骇,組件名大寫债鸡。
步驟
我們有3個組件,把這3個組件分別放到不同的組件
第1步.新建目錄components
第2步.新建組件文件
(1)有幾個組件就建幾個文件:分別新建文件user.js铛纬、books.js厌均、movies.js
然后把各個部分相關(guān)的代碼分別剪切進(jìn)去,并導(dǎo)出告唆。
第3步.對于共用的函數(shù)棺弊,也要新建文件,單獨(dú)拎出來擒悬。
(1)Context是組件共用的模她,所以要新建文件Context.js
,把相關(guān)代碼剪切出來,并導(dǎo)出懂牧。
同樣公共的ajax也是如此
出了組件放components里缝驳,其它都放外面(src)
新建文件ajax.js
,把相關(guān)代碼剪切出來,并導(dǎo)出用狱。
(2)使用Context运怖、ajax
要想使用Context、ajax,那每個組件都需要import
import Context from '../Context.js' //導(dǎo)入Context`
import ajax from '../ajax' //導(dǎo)入ajax
第4步.使用模塊和公共的函數(shù)
index.js
[圖片上傳失敗...(image-157144-1651443540127)]
細(xì)化reducer
假設(shè)我的組件有很多夏伊,那reducer的switch的case豈不是要寫累死了摇展?
第一部分.先重構(gòu)代碼
變成對象之后就好弄了,因?yàn)閷ο蠛芎煤喜?函數(shù)難合并(基礎(chǔ)知識)溺忧。
function reducer(state, action) {
switch (action.type) {
case "setUser":
return { ...state, user: action.user };
case "setBooks":
return { ...state, books: action.books };
case "setMovies":
return { ...state, movies: action.movies };
default:
throw new Error();
}
}
重構(gòu)后
const obj = {
setUser:(state, action)=>{
return { ...state, user: action.user };
},
//removeUser:()=>{},
setBooks:(state, action)=>{
return { ...state, books: action.books };
},
//deleteBook:()=>{},
setMovies:(state, action)=>{
return { ...state, movies: action.movies };
},
//deleteMovie:()=>{}
}
//使用obj
function reducer(state, action) {
const fn = obj[action.type] //判空
if(fn){
fn(state,action)
}else{
throw new Error('你傳的什么鬼 type')
}
}
分開后就好弄了咏连,setUser是user模塊的reducer、setBooks是books模塊的reducer、setMovies是movies模塊的reducer。
假如還有其他的窃判,比如除了setUser可能還有removeUser,除了setBooks可能還有deleteBook,除了setMovies可能還有deleteMovie...
那怎么對這6個函數(shù)分成3個模塊呢菠剩?
第二部分.細(xì)化reducer(模塊化)
1.新建目錄reducers
2.新建子文件
(1)新建user_reducer.js、books_reducer.js俯逾、movies_reducer.js
(2)然后將代碼剪切放到export default{ ... }
里
3.使用
import userReducer from './reducers/user_reducer'
import booksReducer from './reducers/books_reducer'
import moviesReducer from './reducers/movies_reducer'
const obj = {
...userReducer, //把userReducer里的2個函數(shù)地址拷過來
...booksReducer,
...moviesReducer
}
useContext(常用)
概念
上下文就是你運(yùn)行一個程序所需要知道的所有其它變量(全局變量)。
全局變量是全局的上下文,所有變量都可以訪問它草慧。
上下文是局部的全局變量,context只在<C.Provider>
內(nèi)有用匙头,出了這個范圍的組件是用不到這個contextde漫谷。
使用方法:
一.使用C = createContext(initical)
創(chuàng)建上下文
二.使用<C.provider value={}>
初始化并圈定作用域
三.在作用域內(nèi)的組件里使用useContext(C)
來獲取上下文
import React, { createContext } from "react";
const C = createContext(null)
<C.Provider value={}>
...
</C.Provider>
value的初始值可以是任何值,一般我們會給一個讀寫接口.
<C.Provider>
內(nèi)的所有組件都可以用上下文C
import React, { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";
const C = createContext(null);
function App() {
console.log("App 執(zhí)行了");
const [n, setN] = useState(0);
return (
<C.Provider value={{ n, setN }}>
<div className="App">
<Baba />
</div>
</C.Provider>
);
}
function Baba() {
const { n, setN } = useContext(C); //使用context
return (
<div>
我是爸爸 n: {n} <Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(C); //使用context
const onClick = () => {
setN(i => i + 1);
};
return (
<div>
我是兒子 我得到的 n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
+1
操作的不是本身的state蹂析,而是從App那里得到的讀舔示、寫接口。
App也可以不用state电抚,用reducer:const [n, setN] = useState(0);
惕稻,context不管你用啥,它只是告訴你n喻频、setN
可以共享給你的子代的任何組件的缩宜,范圍就是由<C.Provider>
圈定的。
useContext注意事項(xiàng)
不是響應(yīng)式的
你在一個模塊將C里面的值改變甥温,另一個模塊不會感知到這個變化锻煌。
更新的機(jī)制并不是響應(yīng)式的,而是重新渲染的過程姻蚓。
比如宋梧,當(dāng)我們點(diǎn)擊+1
時:setN去通知useState
,useState重新渲染App,發(fā)現(xiàn)n變了,于是問里面的組件<Baba />
有沒有用到n?沒有狰挡,就繼續(xù)問<Child />
有沒有用到n?用到了捂龄,這時候兒子就知道要刷新了释涛,是一個從上而下逐級通知的過程,并不是響應(yīng)式的過程倦沧。
Vue3是你改n時唇撬,它就知道n變了,于是它就找誰用到了n,它就把誰直接改變了展融。它不會從上而下整體過一遍窖认,沒有這么復(fù)雜,因?yàn)樗且粋€響應(yīng)式的過程告希。
總結(jié): useContext的更新機(jī)制式是自頂向下扑浸,逐級更新數(shù)據(jù)。
而不是監(jiān)聽這個數(shù)據(jù)變化燕偶,直接通知對應(yīng)的組件喝噪。
useEffect & useLayoutEffect
useEffect副作用
對環(huán)境的改變即為副作用,如修改document.title
但我們不一定非要把副作用放在useEffect里
useEffect API名字叫的不好指么,建議理解成afterRender酝惧,每次render后就會調(diào)用的一個函數(shù)。
用途: 它可以代替之前的3種鉤子:出生涧尿、更新系奉、死亡
1.作為componentDidMount
使用檬贰,[]作第2個參數(shù)
2.作為componentDidUpdate
使用姑廉,可指定依賴
3.作為componentWillUnmount
使用,通過return
以上三種用途可同時存在
特點(diǎn)
如果同時存在多個useEffect,會按從上倒下的順序執(zhí)行翁涤。
如何使用
import React, { useState,useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0);
const onclick=()=>{
setN(i => i+1)
}
useEffect(()=>{
console.log("第一次渲染后執(zhí)行這句話")
},[])
useEffect(()=>{
console.log("每次都會執(zhí)行這句話桥言,update")
})
useEffect(()=>{
console.log("只有當(dāng)n變了才會執(zhí)行這句話")//監(jiān)聽某個值變化時執(zhí)行,包含第一次
},[n])
useEffect(()=>{
if(n !== 0){
console.log("n變化時會執(zhí)行這句話葵礼,剔除第一次")//默認(rèn)包含第1次号阿,要想排除第1次可以判斷下
}
},[n])
//第一次進(jìn)來時使實(shí)現(xiàn)setInterval,每秒打印一個hi
//當(dāng)組件消失時鸳粉,把定時器關(guān)掉扔涧,不然會一直打印hi
//告訴React return一個函數(shù):當(dāng)組件掛掉時要執(zhí)行的代碼
afterRender(()=>{
const id=setInterval(() => {
console.log("hi")
}, 1000);
return ()=>{
window.clearInterval(id)
}
})
return (
<div>
n:{n}
<button onClick={onclick}>+1</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
如果你只是改變自己的狀態(tài)就不是副作用,如果改變環(huán)境或者全局變量就是副作用届谈。
注意:
1.當(dāng)?shù)?個參數(shù)是[]
時枯夜,表示只會在第一次渲染后執(zhí)行前面的函數(shù)。
2.當(dāng)不寫第2個參數(shù)時艰山,表示每次update都會執(zhí)行前面的函數(shù)湖雹。
3.當(dāng)?shù)?個參數(shù)是[n]
時,表示只會在某個值變化(n)時才會去執(zhí)行前面的函數(shù)曙搬,包含第一次摔吏。
要想剔除第一次可以鸽嫂,可以加個判斷。
4.加return死亡時執(zhí)行
如果我這個組件要掛了征讲,我這個組件正要離開頁面据某,一般在使用router時會經(jīng)常去用。
比如诗箍,一開始是第1個頁面哗脖,點(diǎn)了按鈕后會跳到第2個頁面,那么第1個頁面的所有組件都掛掉了扳还。
掛掉的時候你可能需要做一些清理動作才避。用return,return一個函數(shù):函數(shù)里面是當(dāng)組件掛掉時要執(zhí)行的代碼氨距。
這樣就不會造成內(nèi)存泄露或者是不必要的代碼桑逝。
useLayoutEffect
例子:一開始是value:0,然后迅速變成value:1000
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const BlinkyRender = () => {
const [value, setValue] = useState(0);
useEffect(() => {
document.querySelector('#x').innerText = `value: 1000`
}, [value]);
return (
<div id="x" onClick={() => setValue(0)}>value: {value}</div>
);
};
ReactDOM.render(
<BlinkyRender />,
document.querySelector("#root")
);
[圖片上傳失敗...(image-d484e4-1651443540127)]
useEffect在瀏覽器渲染完成后執(zhí)行: 一開始是value是0,然后迅速變成1000,中間閃爍了下,有閃爍過程俏让。
如果我們改變useEffect的執(zhí)行順序,在瀏覽器渲染前執(zhí)行,會有什么效果楞遏?
沒有閃爍過程
代碼
import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0)
const time = useRef(null)
const onClick = ()=>{
setN(i=>i+1)
time.current = performance.now()
}
useLayoutEffect(()=>{ // 改成 useEffect 試試
if(time.current){
console.log(performance.now() - time.current)
}
})
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useLayoutEffect總是比useEffect先執(zhí)行。用useEffect有閃爍首昔,用useLayoutEffect沒有閃爍寡喝。
那是不是應(yīng)該多用useLayoutEffect?
不是勒奇,因?yàn)榇蟛糠謺r候不會去改變DOM预鬓,不用截胡。
因?yàn)橛脩粝肟吹木褪峭庥^赊颠,本來只需要1ms的格二,現(xiàn)在加了幾句話變成3ms了,影響用戶體驗(yàn)竣蹦。
所以從經(jīng)驗(yàn)上來說顶猜,我們更希望將useEffect放到瀏覽器改變外觀之后,所以優(yōu)先使用useEffect痘括。
useEffect和useLayoutEffect的本質(zhì)區(qū)別:
useEffect在瀏覽器渲染完成后執(zhí)行,useLayoutEffect在瀏覽器渲染完成前執(zhí)行长窄。
總結(jié):
優(yōu)先使用useEffect,除非不能滿足你的需求再使用useLayoutEffect纲菌。
雖然useLayoutEffect的性能更好挠日,優(yōu)先級更高,但是會影響用戶看到畫面變換的時間驰后,得不償失肆资。
代碼佐證時間差別:從setN到副作用開始執(zhí)行,中間有多久灶芝?
結(jié)果: useLayoutEffect是0.3ms,useEffect是0.8ms郑原,相差0.5ms唉韭。
如果你改變的外觀越多,時間就越多犯犁,呈線性的属愤。
import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0)
const time = useRef(null)
const onClick = ()=>{
setN(i=>i+1) //打點(diǎn)一:setN后馬上打點(diǎn)
time.current = performance.now() //beforeRender
}
useLayoutEffect(()=>{ // 改成 useEffect 試試
//afterRender
if(time.current){
console.log(performance.now() - time.current)
}
})
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
知識點(diǎn):performance.now()
是全局對象,用來打印當(dāng)前的時間
特點(diǎn)
1.useLayoutEffect總是比useEffect先執(zhí)行酸役。
下面的代碼打印2和3住诸,再打印1。
useEffect(()=>{
if(time.current){ console.log("1") },[])
}
useLayoutEffect(()=>{
if(time.current){ console.log("2") },[])
}
useLayoutEffect(()=>{
if(time.current){ console.log("3") },[])
}
2.useLayoutEffect里的任務(wù)最好影響了Layout
如果沒有改變屏幕外觀Layout涣澡,就沒必要放瀏覽器渲染前贱呐,占時間。
經(jīng)驗(yàn): 為了用戶體驗(yàn)入桂,優(yōu)先使用useEffect(優(yōu)先渲染)
useMemo & useCallback
useMemo(最常用)
要理解
React.useMemo
需要先了解React.memo
奄薇。
useCallback是useMemo的語法糖。
React.memo
import React from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child data={m}/>
// <Child2 data={m}/> 優(yōu)化版
</div>
);
* [ ] }
function Child(props) {
console.log("child 執(zhí)行了");
console.log('假設(shè)這里有大量代碼')
return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);//接收Child組件
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
[圖片上傳失敗...(image-5c0181-1651443540127)]
點(diǎn)擊button時n
會變抗愁,那child會再次執(zhí)行嗎馁蒂?
child會再次執(zhí)行。child只依賴m,初始值為0蜘腌,既然參數(shù)不變?yōu)槭裁催€會再執(zhí)行呢沫屡,不應(yīng)該執(zhí)行的。
使用React.memo
把child封裝下,Child2是Child的優(yōu)化版,它會只在它的props變化時渲染撮珠,代碼<Child2 data={m}/>
現(xiàn)在點(diǎn)擊button后沮脖,2個log就再也不會執(zhí)行了。除了第一次渲染時會執(zhí)行console劫瞳,之后再也不會執(zhí)行倘潜。除非當(dāng)m第一次渲染時才會執(zhí)行绷柒,因?yàn)閙的數(shù)據(jù)變了志于,這就是React.memo的好處。
React.memo使得一個組件只有在它的props變化時废睦,它才會再執(zhí)行一遍并且再次渲染
Child組件還可以優(yōu)化:
const Child = React.memo(props=>{
console.log("child 執(zhí)行了");
console.log('假設(shè)這里有大量代碼')
return <div> child:{props.data} </dic>
})
但是有個bug
例子:假設(shè)onClick支持onClick事件伺绽,它希望別人給它傳個onClick監(jiān)聽,在點(diǎn)擊div時嗜湃,就會調(diào)用props.onClick
奈应。給Child2傳個onClick。
function App() {
console.log("App 執(zhí)行了")
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => { setN(n + 1); };
const onClickChild=()=>{} //這句話重新執(zhí)行
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild}/>
</div>
);
}
function Child(props) {
console.log("child 執(zhí)行了");
console.log('假設(shè)這里有大量代碼')
return <div onClick = {props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Child2是優(yōu)化過后的函數(shù)购披,理論上來說杖挣,只要m和onClickChild不變,它就不需要重新執(zhí)行刚陡。比如我更新n惩妇,它應(yīng)該不需要重新執(zhí)行株汉。
測試下:Child2竟然執(zhí)行了,為什么呢歌殃?
因?yàn)楫?dāng)我點(diǎn)擊n+1
時乔妈,App會重新執(zhí)行,const onClickChild=()=>{}
這句話也會重新執(zhí)行氓皱。之前是一個空函數(shù)路召,現(xiàn)在又是另一個空函數(shù),2個不同的空函數(shù)就代表onClick變了波材。
那為什么n可以呢股淡?
因?yàn)楫?dāng)你寫m=0時,第一次的0和第二次的0都是數(shù)值廷区,數(shù)值是相等的揣非。但是函數(shù)是個對象,第一躲因、二次的空函數(shù)的地址是不相等的早敬,這就是值與引用的區(qū)別。
那怎么解決這個問題呢大脉?
我不希望用戶在更新n時搞监,由于函數(shù)的更新而去渲染自己。
用useMemo镰矿,useMemo可以實(shí)現(xiàn)函數(shù)的重用琐驴。
方法:useMemo接受一個函數(shù),這個函數(shù)的返回值就是你要緩存的東西秤标。
function App() {
console.log("App 執(zhí)行了")
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => { setN(n + 1); };
const onClickChild = useMemo(()=>{
return ()=>{} //復(fù)用
},[m])
//const onClickChild=()=>{}
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild}/>
</div>
);
}
function Child(props) {
console.log("child 執(zhí)行了");
console.log('假設(shè)這里有大量代碼')
return <div onClick = {props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
[圖片上傳失敗...(image-4fadcd-1651443540128)]
App執(zhí)行了绝淡,child沒執(zhí)行。因?yàn)楹瘮?shù)已經(jīng)被我們復(fù)用苍姜,只有在m變化時牢酵,你再重新給我生成一個,因?yàn)橛锌赡苓@個函數(shù)用到了m衙猪。useMemo用來緩存一些馍乙,你希望在2次新舊組件迭代的時候,希望用上次的值垫释,這個值就是一個函數(shù)丝格。
總結(jié)
我們在使用React時經(jīng)常發(fā)現(xiàn)有多余的render,比如說n變了,但是依賴m的組件卻自動刷新了棵譬,為了解決這個問題可以使用React.memo
,這個memo可以做到如果props不變就沒有必要再執(zhí)行了显蝌。但它有個bug,就算我2次用到的是空函數(shù)/函數(shù)订咸,由于我的App重新渲染了曼尊,所以這個函數(shù)的地址就變了,是一個新的空函數(shù)扭屁。這就導(dǎo)致可props本質(zhì)上還是變了,變了就會一秒破功涩禀。新舊函數(shù)雖然功能一樣料滥,都是地址不一樣。我們可以使用React.useMemo
useMemo特點(diǎn)
1.第一個參數(shù)一定是函數(shù)()= value
,不接受參數(shù)艾船。
2.第二個參數(shù)是數(shù)組
3.只有當(dāng)依賴變化時葵腹,才會計算出新的value。如果依賴不變屿岂,那么就重用之前的value
這不就是Vue2的computed嗎践宴?
我這個值是根據(jù)計算得出來的,而且我會緩存使用之前的值爷怀。
注意
如果你的value是個函數(shù)阻肩,那么你就要寫成useMemo( ()=> (x)=> console.log(x))
這是一個返回函數(shù)的函數(shù),很難用运授,于是就有了useCallback
烤惊。
useCallback(最常用)
用法
直接寫你return的函數(shù)就行了。
useCallback(x=>log(x),[m])等價于
useMemo(()=> x=> log(x),[m])
優(yōu)化技巧2
const onClickChild = useMemo(()=>{
return ()=>{
console.log(m)
}
},[m])
//useCallback語法糖
const onClickChild =useCallback(()=>{ console.log(m) },[m])
優(yōu)化技巧1
用useMemo使得一些函數(shù)被重用吁朦,這樣就不至于去更新你已經(jīng)用React.memo優(yōu)化過的組件,一般這2個是一起用的柒室,先memo再useMemo。
優(yōu)化技巧2
如果你覺得useMemo太難用逗宜,可以用useCallback代替雄右。
useRef & forwardRef & useImperativeHandle
useRef(常用)
forwardRef、useImperativeHandle跟useRef有非常大的關(guān)系
import React,{useRef} from "react"
import ReactDOM from "react-dom"
//window.count = 0;
function App() {
console.log("App 執(zhí)行了");
const count=useRef(0) //current是隨著App render不會變的量
useEffect(()=>{
count.current +=1
console.log(count)
})
//window.count +=1
const [n, setN] = useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<button onClick={onClick}>update n {n}</button>
</div>
);
}
useRef
+useEffect
實(shí)現(xiàn)count +=1
操作:
全局變量window.count
可記錄render的次數(shù)纺讲。但是全局變量有個壞處擂仍,變量名容易沖突。
這時我們可以用useRef熬甚。
每次更新完后用useEffect對conut.current
進(jìn)行操作逢渔。
conut規(guī)定: 如果你要對count進(jìn)行操作的話,必須要用conut.current
则涯,因?yàn)閏urrent才是它真正的值复局。
在我們不停的渲染中,count始終不會變化粟判,每一次得到的都是同一個count,count的值被記錄在useRef對應(yīng)的一個對象上峦剔,這個對象跟App一一對應(yīng)档礁。
為什么需要current?
App每次渲染都會得到一個count吝沫。
為了保證2次useRef是同一樣的值(只有引用能做到)
新舊組件引用的對象必須是同一個對象呻澜,否則就會出問題递礼。對象地址是同一個,只是值改變了羹幸。
如果沒有current你改的就是對象本身脊髓。
const count=useRef({current:0}) //一開始不是對象,這里假設(shè)它就是一個對象
count.current +=1
總結(jié):
目前為止栅受,我們已經(jīng)學(xué)了3個關(guān)于"是否要變化"的hook将硝。
1.useState/useReducer
它們兩個每次的n都會變化,n每次變
2.useMemo/useCallback
只在依賴m屏镊,[m]變的時候fn才會變依疼,有條件的改變
3.useRef
永遠(yuǎn)不變
延伸
Vue3的ref就是抄襲React的ref,但是有一點(diǎn)不一樣:
如果你對Vue的ref進(jìn)行改變,UI會自動變化,不需要手動刷新而芥。但是React不會自動變化律罢。
例子:點(diǎn)擊button后,雖然useRef改變了棍丐,但是UI不會自動變化误辑。
function App() {
//console.log("App 執(zhí)行了");
const [n, setN] = useState(0);
//const [_, set_] = useState(null);
const count = useRef(0);
const onClick2 = () => {
count.current +=1
//set_(Math.random);
console.log(count.current);
};
useEffect(() => {
console.log(count.current);
});
return (
<div className="App">
<div>
<button onClick={onClick2}> update count{count.current} </button>
</div>
</div>
);
}
[圖片上傳失敗...(image-f864cd-1651443540128)]
要想刷新UI只需要調(diào)用setState下并手動set:
const [_,set_]=React.useState(null) //調(diào)用useState
//手動set,只要這次值跟上次不一樣UI就會更新
const onClick2 = ()=>{
count.current +=1
set_(Math.random())
coneolr.log(count.current)
}
Vue3的思路就是,你不需要寫set_(Math.random())
歌逢,我發(fā)現(xiàn)你對current變更就會自動更新UI稀余。
對比
React的理念是UI=f(data),你要想變化時自動render就自己加,監(jiān)聽ref,當(dāng)ref.current變化時趋翻,調(diào)用setX即可睛琳。
1.useRef
初始化:const count=useRef(0)
讀取:count.current
2.Vue3
初始化:const count=ref(0)
讀取:count.value
不同點(diǎn):當(dāng)count.value變化時,Vue3會自動render
forwardRef
forwardRef跟useRef有非常大的關(guān)系
例1.為什么要用forwardRef
原因:props無法傳遞ref屬性
import React, { useRef } from "react";
import ReactDOM from "react-dom";
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button2 ref={buttonRef}>按鈕</Button2>
{/* 看瀏覽器控制臺的報錯 */}
</div>
);
}
const Button2 = props => {
console.log(props)
return <button className="red" {...props} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
知識點(diǎn)
1.用buttonRef
引用到Button2
對應(yīng)的DOM對象踏烙,這樣我就不需要用jQuery去找了师骗。
相當(dāng)于:
const button =document.querySelector("#x")
<Button2 id="x">
[圖片上傳失敗...(image-c28286-1651443540128)]
error:函數(shù)組件不能接受refs,只有類組件才能接受refs,你應(yīng)該用forwardRef
log下props:只把按鈕傳過去了,ref沒有傳讨惩,這就是報錯的原因辟癌。
[圖片上傳失敗...(image-99732d-1651443540128)]
你給我的ref我根本讀不到引用,那我怎么把<button>
給你凹瞿怼黍少?應(yīng)該用forwardRef
。
如何使用React.forwardRef
1.Button3
先用forwardRef包裝Button2,把外邊給你的ref轉(zhuǎn)發(fā)給你的第二個參數(shù)处面,這樣你就可以使用refl了厂置。
2.Button2
添加第二個參數(shù)ref
3.使用ref
例2.實(shí)現(xiàn)ref的傳遞
import React, { useRef } from "react";
import ReactDOM from "react-dom";
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按鈕</Button2>
</div>
);
}
const Button2 = (props, ref) => { //2.添加ref
console.log(props);
console.log(ref)
return <button className="red" ref={ref} {...props} />;//3.使用ref
};
const Button3 = React.forwardRef(Button2); //1.用forwardRef包裝Button2
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
這樣改就沒有任何問題了,同樣props里還是沒有ref,但是ref是可以包含到外面給我傳進(jìn)來的button ref的魂角。
[圖片上傳失敗...(image-3eec8d-1651443540128)]
總結(jié): 如果你的函數(shù)組件(Button2)昵济,想要接收別人App
傳來的ref參數(shù),你必須把自己用React.forwardRef
包起來。想用ref就必須要用React.forwardRef
访忿,僅限函數(shù)組件瞧栗,class組件是默認(rèn)可以用的。
優(yōu)化代碼
const Button3 = React.forwardRef((props, ref) => {
console.log(props);
console.log(ref)
return <button className="red" ref={ref} {...props} />;
})
例3.2次ref傳遞得到button的引用
通過ref引用到里面的button需要做兩次傳遞:
buttonRef第一次通過forwardRef傳給了Button2,Button2得到ref后傳遞給了button海铆。
function App() {
//MovableButton就是對Button2的一個包裝
const MovableButton = movable(Button2);
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.curent);
});
return (
<div className="App">
<MovableButton name="email" ref={buttonRef}>//通過ref引用到里面的button
按鈕
</MovableButton>
</div>
);
}
const Button2 = React.forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
// 僅用于實(shí)驗(yàn)?zāi)康募?郑灰诠敬a中使用
function movable(Component) { //可以移動的組件
function Component2(props, ref) { //接收組件1Component,返回組件2Component2
console.log(props, ref);
const [position, setPosition] = useState([0, 0]);
const lastPosition = useRef(null);
const onMouseDown = e => {
lastPosition.current = [e.clientX, e.clientY];
};
const onMouseMove = e => {
if (lastPosition.current) {
const x = e.clientX - lastPosition.current[0];
const y = e.clientY - lastPosition.current[1];
setPosition([position[0] + x, position[1] + y]);
lastPosition.current = [e.clientX, e.clientY];
}
};
const onMouseUp = () => {
lastPosition.current = null;
};
return (
<div
className="movable"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
style={{ left: position && position[0], top: position && position[1] }}
>
<Component {...props} ref={ref} />
</div>
);
}
return React.forwardRef(Component2);
}
總結(jié): 由于props不包含ref,所以需要forwardRef卧斟。
為什么props不包含ref呢殴边?因?yàn)榇蟛糠謺r候不需要
如果你希望一個組件支持ref屬性,那么你就需要用forwardRef把這個函數(shù)組件包起來唆涝,然后給他增加第二個屬性ref找都。
useImperativeHandle(用不著)
useImperativeHandle跟useRef相關(guān)的鉤子
使用一個重要的handle,名字起的稀爛廊酣,應(yīng)該叫setRef
分析:用于自定義ref的屬性
例1.不用useImperativeHandle的代碼:
import React, {useRef,useState,useEffect,useImperativeHandle,createRef} from "react";
import ReactDOM from "react-dom";
function App() {
const buttonRef = useRef(null); //buttonRef就是buttonDOM對象的引用
useEffect(() => { //渲染之前不存在能耻,只能在渲染之后打
console.log(buttonRef.current);
});
return (
<div className="App">
<Button2 ref={buttonRef}>按鈕</Button2>
<button className="close" onClick={() => {
console.log(buttonRef);
buttonRef.current.remove();
}}
>
x
</button>
</div>
);
}
const Button2 = React.forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
[圖片上傳失敗...(image-33afe5-1651443540128)]
buttonRef就是button DOM對象的引用,打印出來就是個<button>
如果你希望得到的不是<button>
而是一個你對<button>
的封裝呢亡驰?
這個需求很奇怪晓猛,所以大部分時候用不到。
例2.用了useImperativeHandle的代碼:
function App() {
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.current);
});
return (
<div className="App">
<Button2 ref={buttonRef}>按鈕</Button2> //Button2想自定義ref
<button className="close" onClick={() => {
console.log(buttonRef);
buttonRef.current.x();
}}>
x
</button>
</div>
);
}
const Button2 = React.forwardRef((props, ref) => {
const realButton = createRef(null);
//如何自定義ref
const setRef = useImperativeHandle;
setRef(ref, () => { //假的ref
return {
x: () => {
realButton.current.remove();
},
realButton: realButton //真的ref(也可以給它真正的ref用)
};
});
return <button ref={realButton} {...props} />;
});
[圖片上傳失敗...(image-448248-1651443540128)]
ref可以支持自定義
比如說Button2想自定義ref,不想把button給別人凡辱,那怎么自定義ref呢戒职?
把ref賦值成一個對象。
ref就是個對象透乾,ref的x就是一個函數(shù)洪燥,這個函數(shù)會去對button進(jìn)行一些操作。
setRef是個假的ref,把它暴露在外面
我自己使用真的refuseImperativeHandle
這樣別人引用我時乳乌,只能引用到假的setRef
所以這個hook真正意圖是對ref進(jìn)行設(shè)置捧韵,以達(dá)到某種不可告人的目的,這個useImperativeHandle
幾乎不用汉操。
總結(jié): 如果一個函數(shù)組件暴露了ref在外面再来,那么你可以自定義這個ref。
自定義 Hook
例1.封裝數(shù)據(jù)操作
步驟
1.新建目錄hooks,新建文件useList.js
useList.js
import { useState, useEffect } from "react";
const useList = () => {
const [list, setList] = useState(null); //設(shè)置state
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []);
return {
list: list, //是同一個對象的引用磷瘤,把地址傳給外面
setList: setList
};
};
export default useList;
function ajax() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, name: "Frank" },
{ id: 2, name: "Jack" },
{ id: 3, name: "Alice" },
{ id: 4, name: "Bob" }
]);
}, 2000);
});
}
useList.js解析
一開始就請求"/list"數(shù)據(jù):得到list之后就setList芒篷,setList之后list就會變,引用的人也就知道了采缚。[] 確保只在第一次運(yùn)行针炉, 把讀寫接口return出去,引用/調(diào)用useList函數(shù)時就可以得到讀寫接口仰担,list
是同一個對象的引用糊识,把地址傳給外面list(index.js的list引用)绩社。
在我調(diào)用setList時摔蓝,我set的雖然是我這個state(useState),但是由于useList是在App組件
里調(diào)用的赂苗。所以在使用useList
時,相當(dāng)于把代碼(useList函數(shù)里的代碼)拷到App組件里了。所以雖然我的useState不是在App里寫的贮尉,但是依然不報錯拌滋,因?yàn)槲沂窃谶@里運(yùn)行的。
2.引用useList
index.js
import React from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
const { list, setList } = useList();
return ( //DOM
<div className="App">
<h1>List</h1>
{list ? (
<ol>
{list.map(item=> (<li key={item.id}>{item.name}</li>))}
</ol>
) : ("加載中...")}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
[圖片上傳失敗...(image-7cd7e0-1651443540128)]
如何封裝猜谚?
1.你(useList.js)不管用到什么hook,你全部都把它寫在一個函數(shù)(useList)里面:把相關(guān)的邏輯都寫到一起败砂,最后把你的讀接口、寫接口暴露出去就行了魏铅。
2.然后別人(index.js)就只需要知道你的讀接口昌犹、寫接口,其它的一概不管览芳。
比如說你有很多數(shù)據(jù)
const { list } = useList()
const { user } = useUser()
useUser會自己去初始化user,自己去請求user,請求完了自己去setUser斜姥。
我這邊只需要讀user就行了,這就是自定義hook的牛B之處沧竟。
但是你既然可以封裝铸敏,不妨封裝的更厲害一點(diǎn),不要只有一個讀和寫悟泵,增刪改查全部都可以做出來杈笔。
比如說,我們對useList做了升級糕非。
import { useState, useEffect } from "react";
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []);
return {
list: list, //讀接口
addItem: name => { //增接口
setList([...list, { id: Math.random(), name: name }]);
},
deleteIndex: index => { //刪接口
setList(list.slice(0, index).concat(list.slice(index + 1)));
}
};
};
export default useList;
function ajax() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: "1", name: "Frank" },
{ id: "2", name: "Jack" },
{ id: "3", name: "Alice" },
{ id: "4", name: "Bob" }
]);
}, 2000);
});
}
給了一個讀接口蒙具,用來讀list。給了一個增接口朽肥,用來添加item禁筏。給了一個刪接口,用來刪除index鞠呈。
點(diǎn)按鈕就刪除: 當(dāng)你onClick時融师,我就直接調(diào)用deleteIndex,然后把index傳給你deleteIndex(index)
就刪掉了。根本不需要知道list是從哪里請求數(shù)據(jù)蚁吝、是怎么刪除的旱爆、我一概不關(guān)心。我只需要得到一個讀或者幾個寫窘茁。
index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
const { list, deleteIndex } = useList();
//const { list, deleteIndex, addItem} = useList(); //得到一個讀或者幾個寫
return (
<div className="App">
<h1>List</h1>
{list ? (
<ol>
{list.map((item, index) => (
<li key={item.id}>
{item.name}
<button onClick={() => { deleteIndex(index);}} >
x
</button>
</li>
))}
</ol>
) : ( "加載中...")}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
分析
1.你甚至還可以在自定義Hook里使用Context
這樣你可以把自定義Hook和useReducer以及useContext結(jié)合起來,完全代替了redux怀伦。
所以在新版的React里面沒有必要再使用redux了。
2.useState只說了不能在if else里使用山林,但沒說不能在函數(shù)里運(yùn)行
只要這個函數(shù)在函數(shù)組件里運(yùn)行即可
希望大家在React項(xiàng)目中盡量使用自定義Hook,不要再去搞一些useState房待、useEfect放到這個組件上部,不要出現(xiàn)這種代碼。
Stale Closure
Stale Closure(過時閉包)
用來描述你的函數(shù)引用的變量是之前產(chǎn)生的那個變量桑孩。
怎么避免呢拜鹤?
基本上是通過加個依賴,讓它自動刷新流椒,要記得清除舊的計時器敏簿。
所以一般來說不用計時器,比較麻煩。
JS中的Stale Closure
function createIncrement(i) {
//每調(diào)用一次這個函數(shù)宣虾,就會對value+i的操作惯裕,閉包读宙。
function increment() {
let value = 0;
value += i;
console.log(value);
}
const message = `Current value is ${value}`;
function log() {
console.log(message);
}
return [increment, log];
}
const [increment, log] = createIncrement(1);//析構(gòu)函數(shù)
increment(); // 1
increment(); // 2
increment(); // 3
// Does not work!
log(); // "Current value is 0"
useState里多次講過祸穷,由于每次你在執(zhí)行函數(shù)時都生成了一個message,所以第一次執(zhí)行message得到1屋彪,第二次執(zhí)行message得到2鹉胖,第三次執(zhí)行message得到3握玛。
那你要是初始就把message記住了,那這個message里面的value就是0啊次员,log就永遠(yuǎn)只會打0败许,不會打后面的。因?yàn)楹竺娴氖怯勺约旱膌og,那么這個log就叫做過時的log,因?yàn)閕已經(jīng)創(chuàng)建了3次淑蔚,log也創(chuàng)建了3次市殷,但是你卻保留的是初始值log,這就導(dǎo)致它過時了刹衫。
怎么解決醋寝?
每次log前重新去取這個log
function log() {
const message = `Current value is ${value}`;
console.log(message);
}
不要一開始就記下value,而是在調(diào)用log時,用log去取最新的值带迟。
這就是JS中過時閉包的解決方法音羞。
React中的Stale Closure
1' useEffect()
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
}, []);//只在第一次設(shè)置計時器,所以count是過時的仓犬。
return (
<div>
{count}
<button onClick={() => setCount(count + 1) }>
Increase
</button>
</div>
);
}
解決方法:把count放在依賴?yán)镄岽拢瑫r把之前的id清掉。
生成了id又把id給clearInterval了搀继,這不就相當(dāng)于什么都沒做嘛窘面?
不是,生成的是最新的id叽躯,刪掉的是上一次組件消失時的id财边,調(diào)用時機(jī)不同。
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
const id = setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
return function() {
clearInterval(id);
}
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1) }>
Increase
</button>
</div>
);
}
2' useState()
function DelayedCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count + 1);
}, 1000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>
Increase async
</button>
</div>
);
}
1s后打印count点骑,在這1s之間count +=1
根本不知道它變了酣难,你用的永遠(yuǎn)都是舊的count谍夭。
解決方法:堅持使用函數(shù)作為setState的參數(shù)。
這樣你就不會受制于舊的還是新的憨募,因?yàn)槟銈鞯氖且粋€動作紧索,這個動作是不關(guān)心這個數(shù)據(jù)當(dāng)前的值是什么的,不關(guān)心你現(xiàn)在是什么值馋嗜,只關(guān)心+1
齐板。
function DelayedCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count => count + 1);
}, 1000);
}
function handleClickSync() {
setCount(count + 1);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
<button onClick={handleClickSync}>Increase sync</button>
</div>
);
}
總結(jié)
1.useState狀態(tài)
2.useEffect
(副作用)就是afterRender
3.useLayoutEffect
就是比useEffect
提前一點(diǎn)點(diǎn)吵瞻。
但是很少用葛菇,因?yàn)闀绊戜秩镜男?除非特殊情況才會用。
4.useContext
上下文橡羞,用來把一個讀眯停、寫接口
給整個頁面用。
5.useReducer
專門給Redux的用戶設(shè)計的(能代替Redux的使用)卿泽,我們甚至可以不用useReducer
莺债。
6.useMemo
(記憶)需要與React.Memo
配合使用,useMemo
不好用我們可以升級為更好用的useCallback
(回調(diào))
7.useRef
(引用)就是保持一個量不變签夭,關(guān)于引用還有個forwardRef
,forwardRef
并不是一個Hook,還有個useImperativeHandle
就是setRef齐邦。
就是我支持ref時,可以自定義ref長什么樣子第租,那就使用useImperativeHandle
措拇。
8.自定義Hook
示例中的useList
就是自定義Hook,非常好用。
有個默認(rèn)的自定義HookuseDebugValue
就是你在debugger時,可以給你的組件加上名字慎宾,很少用丐吓。
更多文章,請點(diǎn)擊 我的博客