React文檔
Hooks:useState、useEffect、useLayoutEffect、useContext、useReducer、useMemo、React.memo、callCallback、useRef、useImperativeHandle、自定義Hook、useDebugValue
useState(最常用)
在React的函數(shù)組件里,默認只有屬性,沒有狀態(tài)。
1.使用狀態(tài)
//數(shù)組第1項是讀接口,第2項是寫接口,初始值0
const [n,setN] = React.useState(0) //數(shù)字
const [user,setUser] = React.useState({name:'F'}) //對象
2.注意事項(1):不可局部更新
更新部分屬性時,未更新的屬性會消失。
3.注意事項(2):地址要變
setState(obj)如果obj對象地址不變,那么React就認為數(shù)據(jù)沒有變化,因此不會幫你改變內(nèi)容。
4.useState接受函數(shù)
5.setState接受函數(shù)
例1:不可局部更新
如果state是個對象,能否部分setState?
不行,因為setState不會幫我們合并屬性。所以當只更新部分屬性時,未更新的屬性就會消失。
那怎么解決"未更新的屬性會消失"的問題?
用...拷貝之前所有的屬性,然后再覆蓋屬性。
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新版的所有東西都不會幫你合并,它認為這是你自己要做的事。
例2.地址要變
我想把name改下:于是直接修改user.name然后setUser(user)
你會發(fā)現(xià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ù)的好處是:減少多余的計算過程,因為JS引擎不會立即執(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ù))
點擊button后你會發(fā)現(xiàn)n=1而不是2,因為當你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進行多次操作時,優(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)需要導入useReducer或者直接使用全稱React.useReducer
(2)useReducer接收2個參數(shù):所有操作reducer和初始狀態(tài)initical
(3)你將得到讀API、寫API寫API一般叫dispatch,因為你必須通過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操作
})
}
相當于useState,只不過把所有操作聚攏在一個函數(shù)里,這樣的好處是:調(diào)用的代碼簡短了。
調(diào)用傳參:+2時傳了參數(shù)number:2,那么reducer里的1就可以變成一個參數(shù)。因為dispatch()里傳的對象就是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的復雜版。好處是用來踐行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對對象進行整體的操作。
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事件。用戶輸入即更新,因為內(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ù)里面運行。
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);
});
}
知識點
1.useEffect設(shè)置只在第一次渲染時執(zhí)行某函數(shù)
每次User刷新時,代碼setStatedispatch就會再執(zhí)行一遍并重復請求ajax。
怎樣減少請求ajax,設(shè)置只在第一次進入頁面時請求?
借助useEffect
需要自動引入或者直接React.useEffect
useEffect需要傳個函數(shù),當?shù)?個參數(shù)是空數(shù)組時,那么前面的函數(shù)就只會在第一次渲染時執(zhí)行,之后永遠不會執(zhí)行。例子:
React.useEffect(()=>{},[])
項目代碼
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)自己刷新,因為React知道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,是如何實現(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)的代碼分別剪切進去,并導出。
第3步.對于共用的函數(shù),也要新建文件,單獨拎出來。
(1)Context是組件共用的,所以要新建文件Context.js,把相關(guān)代碼剪切出來,并導出。
同樣公共的ajax也是如此
出了組件放components里,其它都放外面(src)
新建文件ajax.js,把相關(guān)代碼剪切出來,并導出。
(2)使用Context、ajax
要想使用Context、ajax,那每個組件都需要import
import Context from '../Context.js' //導入Context`
import ajax from '../ajax' //導入ajax
第4步.使用模塊和公共的函數(shù)
index.js
[圖片上傳失敗...(image-157144-1651443540127)]
細化reducer
假設(shè)我的組件有很多,那reducer的switch的case豈不是要寫累死了?
第一部分.先重構(gòu)代碼
變成對象之后就好弄了,因為對象很好合并,函數(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個模塊呢?
第二部分.細化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(常用)
概念
上下文就是你運行一個程序所需要知道的所有其它變量(全局變量)。
全局變量是全局的上下文,所有變量都可以訪問它。
上下文是局部的全局變量,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注意事項
不是響應(yīng)式的
你在一個模塊將C里面的值改變,另一個模塊不會感知到這個變化。
更新的機制并不是響應(yīng)式的,而是重新渲染的過程。
比如,當我們點擊+1時:setN去通知useState,useState重新渲染App,發(fā)現(xiàn)n變了,于是問里面的組件<Baba />有沒有用到n?沒有,就繼續(xù)問<Child />有沒有用到n?用到了,這時候兒子就知道要刷新了,是一個從上而下逐級通知的過程,并不是響應(yīng)式的過程。
Vue3是你改n時,它就知道n變了,于是它就找誰用到了n,它就把誰直接改變了。它不會從上而下整體過一遍,沒有這么復雜,因為它是一個響應(yīng)式的過程。
總結(jié): useContext的更新機制式是自頂向下,逐級更新數(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
以上三種用途可同時存在
特點
如果同時存在多個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("只有當n變了才會執(zhí)行這句話")//監(jiān)聽某個值變化時執(zhí)行,包含第一次
},[n])
useEffect(()=>{
if(n !== 0){
console.log("n變化時會執(zhí)行這句話,剔除第一次")//默認包含第1次,要想排除第1次可以判斷下
}
},[n])
//第一次進來時使實現(xiàn)setInterval,每秒打印一個hi
//當組件消失時,把定時器關(guān)掉,不然會一直打印hi
//告訴React return一個函數(shù):當組件掛掉時要執(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.當?shù)?個參數(shù)是[]時,表示只會在第一次渲染后執(zhí)行前面的函數(shù)。
2.當不寫第2個參數(shù)時,表示每次update都會執(zhí)行前面的函數(shù)。
3.當?shù)?個參數(shù)是[n]時,表示只會在某個值變化(n)時才會去執(zhí)行前面的函數(shù),包含第一次。
要想剔除第一次可以,可以加個判斷。
4.加return死亡時執(zhí)行
如果我這個組件要掛了,我這個組件正要離開頁面,一般在使用router時會經(jīng)常去用。
比如,一開始是第1個頁面,點了按鈕后會跳到第2個頁面,那么第1個頁面的所有組件都掛掉了。
掛掉的時候你可能需要做一些清理動作。用return,return一個函數(shù):函數(shù)里面是當組件掛掉時要執(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?
不是,因為大部分時候不會去改變DOM,不用截胡。
因為用戶想看的就是外觀,本來只需要1ms的,現(xiàn)在加了幾句話變成3ms了,影響用戶體驗。
所以從經(jīng)驗上來說,我們更希望將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) //打點一:setN后馬上打點
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);
知識點:performance.now()是全局對象,用來打印當前的時間
特點
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ō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)]
點擊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)在點擊button后,2個log就再也不會執(zhí)行了。除了第一次渲染時會執(zhí)行console,之后再也不會執(zhí)行。除非當m第一次渲染時才會執(zhí)行,因為m的數(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)聽,在點擊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í)行了,為什么呢?
因為當我點擊n+1時,App會重新執(zhí)行,const onClickChild=()=>{}這句話也會重新執(zhí)行。之前是一個空函數(shù),現(xiàn)在又是另一個空函數(shù),2個不同的空函數(shù)就代表onClick變了。
那為什么n可以呢?
因為當你寫m=0時,第一次的0和第二次的0都是數(shù)值,數(shù)值是相等的。但是函數(shù)是個對象,第一、二次的空函數(shù)的地址是不相等的,這就是值與引用的區(qū)別。
那怎么解決這個問題呢?
我不希望用戶在更新n時,由于函數(shù)的更新而去渲染自己。
用useMemo,useMemo可以實現(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 ()=>{} //復用
},[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í)行。因為函數(shù)已經(jīng)被我們復用,只有在m變化時,你再重新給我生成一個,因為有可能這個函數(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ù)。這就導致可props本質(zhì)上還是變了,變了就會一秒破功。新舊函數(shù)雖然功能一樣,都是地址不一樣。我們可以使用React.useMemo
useMemo特點
1.第一個參數(shù)一定是函數(shù)()= value,不接受參數(shù)。
2.第二個參數(shù)是數(shù)組
3.只有當依賴變化時,才會計算出新的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實現(xiàn)count +=1操作:
全局變量window.count可記錄render的次數(shù)。但是全局變量有個壞處,變量名容易沖突。
這時我們可以用useRef。
每次更新完后用useEffect對conut.current進行操作。
conut規(guī)定: 如果你要對count進行操作的話,必須要用conut.current,因為current才是它真正的值。
在我們不停的渲染中,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)學了3個關(guān)于"是否要變化"的hook。
1.useState/useReducer
它們兩個每次的n都會變化,n每次變
2.useMemo/useCallback
只在依賴m,[m]變的時候fn才會變,有條件的改變
3.useRef
永遠不變
延伸
Vue3的ref就是抄襲React的ref,但是有一點不一樣:
如果你對Vue的ref進行改變,UI會自動變化,不需要手動刷新。但是React不會自動變化。
例子:點擊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,當ref.current變化時,調(diào)用setX即可。
1.useRef
初始化:const count=useRef(0)
讀取:count.current
2.Vue3
初始化:const count=ref(0)
讀取:count.value
不同點:當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);
知識點
1.用buttonRef引用到Button2對應(yīng)的DOM對象,這樣我就不需要用jQuery去找了。
相當于:
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.實現(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是可以包含到外面給我傳進來的button ref的。
[圖片上傳失敗...(image-3eec8d-1651443540128)]
總結(jié): 如果你的函數(shù)組件(Button2),想要接收別人App傳來的ref參數(shù),你必須把自己用React.forwardRef包起來。想用ref就必須要用React.forwardRef,僅限函數(shù)組件,class組件是默認可以用的。
優(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} />;
});
// 僅用于實驗目的,不要在公司代碼中使用
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呢?因為大部分時候不需要
如果你希望一個組件支持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進行一些操作。
setRef是個假的ref,把它暴露在外面
我自己使用真的refuseImperativeHandle
這樣別人引用我時,只能引用到假的setRef
所以這個hook真正意圖是對ref進行設(shè)置,以達到某種不可告人的目的,這個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就會變,引用的人也就知道了。[] 確保只在第一次運行, 把讀寫接口return出去,引用/調(diào)用useList函數(shù)時就可以得到讀寫接口,list是同一個對象的引用,把地址傳給外面list(index.js的list引用)。
在我調(diào)用setList時,我set的雖然是我這個state(useState),但是由于useList是在App組件里調(diào)用的。所以在使用useList時,相當于把代碼(useList函數(shù)里的代碼)拷到App組件里了。所以雖然我的useState不是在App里寫的,但是依然不報錯,因為我是在這里運行的。
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之處。
但是你既然可以封裝,不妨封裝的更厲害一點,不要只有一個讀和寫,增刪改查全部都可以做出來。
比如說,我們對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。
點按鈕就刪除: 當你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ù)里運行
只要這個函數(shù)在函數(shù)組件里運行即可
希望大家在React項目中盡量使用自定義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就永遠只會打0,不會打后面的。因為后面的是由自己的log,那么這個log就叫做過時的log,因為i已經(jīng)創(chuàng)建了3次,log也創(chuàng)建了3次,但是你卻保留的是初始值log,這就導致它過時了。
怎么解決?
每次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放在依賴里,同時把之前的id清掉。
生成了id又把id給clearInterval了,這不就相當于什么都沒做嘛?
不是,生成的是最新的id,刪掉的是上一次組件消失時的id,調(diào)用時機不同。
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根本不知道它變了,你用的永遠都是舊的count。
解決方法:堅持使用函數(shù)作為setState的參數(shù)。
這樣你就不會受制于舊的還是新的,因為你傳的是一個動作,這個動作是不關(guān)心這個數(shù)據(jù)當前的值是什么的,不關(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提前一點點。
但是很少用,因為會影響渲染的效率,除非特殊情況才會用。
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,非常好用。
有個默認的自定義HookuseDebugValue就是你在debugger時,可以給你的組件加上名字,很少用。
更多文章,請點擊 我的博客