useState
1.基本使用
import { useState } from "react";
function App() {
// 數(shù)組里的第一項是sate里的變量浆熔,第二項是修改state的函數(shù)
// useState里的值就是count的初始值
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
};
return (
<div>
<div>{count}</div>
<div>
<button onClick={add}>+1</button>
</div>
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
等價于
class App extends React.Component {
constructor() {
super();
this.state = {
count: 0
};
}
setCount = () => {
this.setState({
count: this.state.count + 1
});
};
render() {
return (
<div>
<div>{this.state.count}</div>
<button onClick={this.setCount}>+1</button>
</div>
);
}
}
2. 復雜的state
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({
name: "lifa",
age: 18,
habits: ["小改改", "明星"]
});
const add = () => {
setCount(count + 1);
};
const minus = () => {
setCount(count - 1);
};
const addNum = () => {
setUser({
...user,
age: user.age + 1,
habits: [...user.habits, "Lifa"]
});
};
const minusNum = () => {
const newHabits = user.habits.splice(1, 1);
setUser({
...user,
age: user.age - 1,
habits: newHabits
});
};
return (
<div>
<div>{count}</div>
<button onClick={add}>+1</button>
<button onClick={minus}>-1</button>
<div>
{user.name}, {user.age} <br />
{user.habits.join(",")}
</div>
<button onClick={addNum}>變大</button>
<button onClick={minusNum}>減少</button>
<div />
</div>
);
}
3.使用狀態(tài)
const [n,setN] = React.useState(0)
const [user, setUser] = React.useState({name: 'F'})
4. 注意事項
1). 如果state是一個對象,我們不能對對象里的部分屬性setState呵晨,需要我們每次都把之前的屬性全部重新結(jié)構(gòu)一遍,然后下面再寫你要修改的屬性
// 錯誤代碼
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = ()=>{
setUser({
name: 'Jack'
})
}
//正確代碼
setUser({
...user,
name: 'Jack'
})
2). 地址要變
setState(obj)如果obj地址不變兄渺,那么React就認為數(shù)據(jù)沒有變化
// 錯誤代碼
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = () => {
// 在原來的引用地址上修改name屬性遭庶,不會起作用
user.name = 'jack'
setUser(user)
}
// 正確代碼
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = () => {
// 重新生成一個引用地址
setUser({
...user,
name: 'jack'
})
}
3). useState只能放在函數(shù)組件內(nèi)部魄懂,不能單獨拿出來
5. useState可以接受函數(shù)
const [state, setState] = useState(()=>{
return initialState
})
該函數(shù)返回初始state, 且只執(zhí)行一次
6. setState可以接受函數(shù)
我們?nèi)绻啻螌seState進行操作的話推薦使用函數(shù)
以兩次修改useState對其進行加一操作為例
const [n,setN] = useState(1)
const onClick = () => {
setN(n+1)
setN(n+1)
}
上面我們在點擊事件里執(zhí)行了兩次修改n熊楼,每次讓他加一鸭叙,可實際上他只會變一次觉啊,因為n本身是不會變的,而是每次生成一個新的n沈贝,所以上面結(jié)果是2而不是3杠人,如果想要它加2的話就要用函數(shù)
setN(i=>i+1)
setN(i=>i+1)
上面的i是一個占位符,隨便什么都可以,就是我們傳一個值給setN嗡善,每次返回當前的值+1辑莫,所以最后會加2,得到的結(jié)果是3
往數(shù)組 push 一條數(shù)據(jù)
const handleAddAuth = () => {
const rateCfg: Base[] = [];
for (let i = 0; i < num; i++) {
rateCfg.push({
id: '',
rate: '',
});
}
const newAuth = {
authId: '',
rateCfg,
};
setAuthBase((odlAuth) => [...odlAuth, newAuth]);
};
useReducer
用來踐行Flux/Redux的思想
代碼過程
- 創(chuàng)建初始值initialState
const intialState = {
n: 0
}
2.創(chuàng)建所有操作reducer(state,action)
const reducer = (state, action) => {
if(action.type === 'add') {
return {n: state.n + action.number}
} else if (action.type === 'mul') {
return {n: state.n * 2}
} else {
throw new Error('unknow type')
}
}
3.傳給useReducer罩引,得到讀和寫API
const [state, dispatch] = useReducer(reducer, intialState)
4.調(diào)用寫({type: '操作類型'})
const onClick = () => {
dispatch({ type: "add", number: 1 });
}
總的來說useRducer是useState的復雜版
一個用useReducer的表單例子
https://codesandbox.io/s/awesome-mahavira-foyk3
用useReducer代替redux
這里以一個簡單的列表為例
function User () {
return (
<div>
<h1>個人信息</h1>
</div>
)
}
function Books () {
return (
<div>
<h1>我的書籍</h1>
</div>
)
}
function Movies () {
return (
<div>
<h1>我的電影</h1>
</div>
)
}
function App () {
return (
<div>
<User/>
<hr />
<Books />
<Movies />
</div>
)
}
- 步驟
1). 將數(shù)據(jù)集中在一個store對象
const store = {
user: null,
books: null,
movies: null
}
2). 將所有操作集中在reducer上
const 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()
}
}
3). 創(chuàng)建Context
const Context = createContext(null
4). 創(chuàng)建對數(shù)據(jù)的讀寫API
function App () {
+ const [state, dispatch] = useReducer(reducer, store)
}
5). 將第4步的內(nèi)容放到第3步的Context
function App () {
const [state, dispatch] = useReducer(reducer, store)
return (
<Context.Provider value={{state, dispatch}}>
</Context.Provider>
)
}
6). 用Context.Provider將Context提供給所有組件
<Context.Provider value={{state, dispatch}}>
<User/>
<hr />
<Books />
<Movies />
</Context.Provider>
7). 各個組件用useContext獲取讀寫API
function User () {
const {state, dispatch} = useContext(Context)
ajax('/user').then(user => {
dispatch({ type: 'setUser', user })
})
return (
<div>
<h1>個人信息</h1>
<div>name: {state.user ? state.user.name : ""}</div>
</div>
)
}
function ajax(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path === '/user') {
resolve({
id: 1,
name: 'Lifa'
})
} else if (path === '/books') {
resolve([
{
id: 1,
name: '金瓶梅'
},
{
id: 2,
name: '肉蒲團'
}
])
} else if (path === '/movies') {
resolve([
{
id: 1,
name: '性女傳奇'
},
{
id: 2,
name: '電車癡漢'
}
])
}
}, 2000)
})
}
上面的User每次執(zhí)行的時候都會修改state各吨,state一修改就會重新調(diào)一下User,User重新調(diào)了又會重新請求ajax蜒程,也就是每次render都會請求一次绅你,所以我們需要使用useEffect
function User() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/user").then(user => {
console.log("111");
dispatch({ type: "setUser", user });
});
}, []);
return (
<div>
<h1>個人信息</h1>
<div>name: {state.user ? state.user.name : ""}</div>
</div>
);
}
- 代碼模塊化
1). 方法分模塊放到components里
2). Context單獨放到當前目錄下
3). 接口請求單獨放到當前目錄下 - components/books.js
import React, { useContext, useEffect } from "react";
import ajax from '../ajax'
import Context from '../Context'
function Books() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/books").then(books => {
console.log("111");
dispatch({ type: "setBooks", books });
});
}, []);
return (
<div>
<h1>我的書籍</h1>
<div>
<ul>
{state.books ? (
state.books.map(book => <li>{book.name}</li>)
) : (
<li>加載中</li>
)}
</ul>
</div>
</div>
);
}
export default Books;
- Context.js
import { createContext} from "react";
const Context = createContext(null);
export default Context;
4). 對reducer分模塊
首先把之前的reducer改寫成對象的形式
const obj = {
'setUser': (state, action) => {
return { ...state, user: action.user }
},
'setBooks': (state, action) => {
return { ...state, books: action.books };
},
'setMovies': (state, action) => {
return { ...state, movies: action.movies };
}
}
function reducer(state, action) {
const fn = obj[action.type]
if (fn) {
fn(state, action)
} else {
throw new Error()
}
}
然后新建reducers目錄
- reducers/user_reducer.js
export default {
setUser: (state, action) => {
return { ...state, user: action.user };
},
removeUser: () => {}
};
- index.js
import React, { createContext, useReducer, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
import Books from "./components/books";
import User from "./components/user";
import Movies from "./components/movies";
import Context from "./Context";
import userReducer from "./reducers/user_reducer";
import moviesReducer from "./reducers/movies_reducer";
import booksReducer from "./reducers/books_reducer";
const store = {
user: null,
books: null,
movies: null
};
const obj = {
...userReducer,
...moviesReducer,
...booksReducer
};
function reducer(state, action) {
const fn = obj[action.type];
if (fn) {
return fn(state, action);
} else {
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, store);
return (
<Context.Provider value={{ state, dispatch }}>
<User />
<hr />
<Books />
<Movies />
</Context.Provider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
完整代碼:https://codesandbox.io/s/staging-pine-s2k5k
useContext
1.一個簡單的四層函數(shù)參數(shù)傳遞的demo
1.1 原生js寫法
function f1(n1) {
console.log(n1);
f2(n1);
}
function f2(n2) {
console.log(n2);
f3(n2);
}
function f3(n3) {
console.log(n3);
f4(n3);
}
function f4(n4) {
console.log(n4);
}
function f(n) {
f1(n);
}
{
let n = 100;
f(n);
}
上面的代碼我如果要在每個函數(shù)里拿到n就需要一直把n作為參數(shù)傳遞下去
1.2. react寫法
function F1(props) {
return (
<div>
{props.n1}
<F2 n2={props.n1} />
</div>
);
}
function F2(props) {
return (
<div>
{props.n2}
<F3 n3={props.n2} />
</div>
);
}
function F3(props) {
return (
<div>
{props.n3}
<F4 n4={props.n3} />
</div>
);
}
function F4(props) {
return <div>{props.n4}</div>;
}
class App extends React.Component {
constructor() {
super();
this.state = {
n: 100
};
}
render() {
return (
<div>
aaa
<F1 n1={this.state.n} />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
現(xiàn)在我們?nèi)绻朐贔4里獲取到state里的n,我們也必須得一層一層通過props傳遞下去昭躺,也就是說即使我們不需要在F2和F3中獲取n我們也得傳下去忌锯,這樣的代碼寫起來就很冗余很復雜
2.代碼改進
2.1. 對原生js代碼改進
(1). 把n作為全局變量,這樣f4就可以直接訪問到n了
let n = 100
function f4() {
console.log(n) // 100
}
問題:全局變量有可能會被人隨意的修改领炫,所以我們要慎用全局變量
(2). 使用局部全局變量
{
let context = {};
window.setContext = (key, value) => {
context[key] = value;
};
window.f1 = () => {
f2();
};
function f2() {
f3();
}
function f3() {
f4();
}
function f4() {
console.log(context.n);
}
}
window.setContext("n", 100);
window.f1();
上面的代碼我們的context是一個局部變量偶垮,我們外界獲取不到它,而修改它的唯一方式是通過一個全局的setContext方法修改帝洪,因為我們的f4和context是在同一個作用域所以可以直接獲取到我們的context里面的值
2.2. 對react代碼進行改進
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function F1(props) {
return (
<F2 />
)
}
function F2(props) {
return (
<F3 />
)
}
function F3(props) {
return (
<div>
<nContext.Consumer>
{(n) => <F4 n4={n} />}
</nContext.Consumer>
</div>
)
}
function F4(props) {
return (
<div>{props.n4}</div>
)
}
const nContext = React.createContext()
class App extends React.Component {
render() {
return (
<div>
<nContext.Provider value="999">
<F1 />
</nContext.Provider>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
聲明一個React.createContext變量似舵,然后通過它的Provider指定一個value為初始值,需要獲取值的地方通過它的Consumer葱峡,然后標簽里面是一個函數(shù)返回你引用的組件砚哗,然后通過函數(shù)里的參數(shù)可以拿到value的值,之后在對應組件中還是通過props獲取
3.自己寫一個接受函數(shù)的組件砰奕,以便理解<nContext.Consumer>
3.1.
function Consumer(props) {
// 這里打印出來的是F1函數(shù)
console.log(props.children)
return (
<div>{props.c1}</div>
)
}
function F1() {
return 'F1'
}
class App extends React.Component {
render() {
return (
<div>
<Consumer c1="c1">
{F1}
</Consumer>
</div>
)
}
}
3.2. 我們可以ton過props.children拿到Consumer標簽里的內(nèi)容也就是{F1}蛛芥,所以我們可以直接在Consumer函數(shù)里調(diào)用F1
function Consumer(props) {
// 調(diào)用標簽里的函數(shù)
props.children()
return (
<div>{props.c1}</div>
)
}
<Consumer c1="c1">
{F1}
</Consumer>
3.3. 因為我們的F1實際上就是一個函數(shù)聲明,所以我們可以直接寫成函數(shù)聲明
<Consumer c1="c1">
{() => console.log('我被調(diào)用了')}
</Consumer>
3.4. 在我們的箭頭函數(shù)聲明里面?zhèn)魅胍粋€參數(shù)
<Consumer>
{(n) => console.log('我被調(diào)用了', n)}
</Consumer>
我們就需要在調(diào)用的地方傳入一個實參
function Consumer(props) {
// 調(diào)用標簽里的函數(shù)
let x = 100
props.children(x)
return (
<div>{props.children}</div>
)
}
所以我們的n就可以拿到100
3.5. 變成{(n) => <F4 n4={n} />}的形式
function Consumer(props) {
let x = 100
let result = props.children(x)
return (
<div>{result}</div>
)
}
function F1() {
return 'F1'
}
class App extends React.Component {
render() {
return (
<div>
<Consumer>
{(n) => <div>{n}</div>}
</Consumer>
</div>
)
}
}
上面的props.children(x)
返回的是<div>{n}</div>
,所以Consumer
的返回值也就是<div><div>{n}</div></div>
而{n}
是100军援,所以就等價于
function Consumer(props) {
return (
<div>
<div>100</div>
</div>
)
}
4.更改context里的value值
4.1. 組件本身改變
class App extends React.Component {
constructor() {
super()
this.state = {
n: 100
}
setTimeout(() => {
this.setState({
n: this.state.n + 10
})
}, 2000)
}
render() {
return (
<div>
<nContext.Provider value={this.state.n}>
<F1 />
</nContext.Provider>
</div>
)
}
}
4.2. 在其他組件中改變context的value
const nContext = React.createContext()
function F1() {
return (
<F2 />
)
}
function F2() {
return (
<F3 />
)
}
function F3() {
return (
<div>
<nContext.Consumer>
{x => <F4 n={x.n} setN={x.setN}/>}
</nContext.Consumer>
</div>
)
}
function F4(props) {
return (
<div>
{props.n}
<button onClick={props.setN}>點我</button>
</div>
)
}
class App extends React.Component {
constructor() {
super()
this.state = {
x: {
n: 300,
setN: ()=> {
console.log('aaaa')
this.setState({
x: {
n: Math.random()
}
})
}
}
}
}
render() {
return (
<div>
<nContext.Provider value={this.state.x}>
<F1 />
</nContext.Provider>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
問題:我們只有第一次點擊按鈕的時候value
才會修改仅淑,之后再次點擊value
就不會修改了
補充知識:修改state
里一個對象的屬性,要把整個對象都重新寫一遍胸哥,然后這個對象里面還要把它之前的屬性都擴展到新的對象里涯竟,而不能直接對象.對應的屬性
比如修改下面的x
里面的n
,我們就得把整個x
對象都重新賦值一遍空厌,然后對象里面還要寫...this.state.x
錯誤寫法:直接修改'x.n'
this.state = {
x: {
n: 300,
setN: () => {
'x.n': Math.random()
}
}
}
正確寫法:
this.state = {
x: {
n: 300,
setN: ()=> {
this.setState({
x: {
...this.state.x,
n: Math.random()
}
})
}
}
}
5.總結(jié)
5.1. 使用方法
5.1.1. 使用C = createContext(initial)創(chuàng)建上下文
import React, { createContext, useContext } from "react";
const C = createContext(null);
5.1.2. 使用<C.provider>圈定作用域
function App() {
const [n, setN] = useState(0);
return (
<C.Provider value={{ n, setN }}>
<div>
<Father />
</div>
</C.Provider>
);
}
5.1.3. 在作用域內(nèi)使用useContext(C)來使用上下文
function Father() {
const { n, setN } = useContext(C);
return (
<div>
我是父組件n: {n} <Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(C);
const onClick = () => {
setN(i => i + 1);
};
return (
<div>
我是子組件 我得到的n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
6.注意事項
- 不是響應式
你在一個模塊將C里面的值改變庐船,另一個模塊不會感知到這個變化,每次都是App重新render的過程
useEffect
官方文檔解釋:Effect Hook 可以讓你在函數(shù)組件中執(zhí)行副作用操作
什么叫副作用:
就是一個函數(shù)里依賴的東西不知道是哪里來的嘲更,那么這個未知的東西就有可能改變你函數(shù)的結(jié)果醉鳖,也就是副作用
比如:
function f1(){
console.log(1)
}
fucntion f2(a, b) {
return a+ b
}
上面的f1
函數(shù)里的console
就是一個未知的,當我們執(zhí)行f1
函數(shù)的時候會打印出1哮内,但這不是必然的,因為console
不是函數(shù)內(nèi)部的東西,所以我們可以修改它
console.log = function(){}
f1()
這時候我們再次執(zhí)行f1
就不會打印出1北发,所以我們每次執(zhí)行的結(jié)果都是未知的纹因,也就是所謂的副作用,而f2
函數(shù)里所有的代碼都是函數(shù)內(nèi)部的琳拨,不管怎么運行返回的都是你兩個參數(shù)的和
案例:
<div id="output"></div>
- index.js
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({
name: "lifa",
age: 18,
habits: ["小改改", "明星"]
});
useEffect(() => {
document.querySelector("#output").innerText = count;
});
const add = () => {
setCount(count + 1);
};
}
上面代碼中的#output
也是一個未知的不屬于函數(shù)內(nèi)部的瞭恰,所以也是有副作用的,所以我們就可以把它放在useEffect
里
可以使用生命周期
const App: React.FunctionComponent<Props> = props => {
const [n, setN] = useState(1);
const x = () => {
setN(n + 1);
};
useEffect(() => {
console.log("aaa");
});
return (
<div>
<h1>{props.message}</h1>
<div>{n}</div>
<button onClick={x}>+1</button>
</div>
);
};
只要有數(shù)據(jù)更新就會觸發(fā)這個api狱庇,如果我們想針對某一個數(shù)據(jù)的改變才調(diào)用這個api惊畏,那么需要在后面指定一個數(shù)組,數(shù)組里面是需要更新的那個值密任,它變了就會觸發(fā)這個api
useEffect(() => {
console.log("aaa");
}, [n]);
只有在n改變的時候才會觸發(fā)
如果我們想只在mounted的時候觸發(fā)一次颜启,那我們需要指定后面的為空數(shù)組,那么就只會觸發(fā)一次浪讳,適合我們做ajax請求
useEffect(() => {
console.log("mounted");
}, []);
如果想在組件銷毀之前執(zhí)行缰盏,那么我們就需要在useEffect
里return 一個函數(shù)
useEffect(() => {
console.log("mounted");
return () => {
console.log('我死了')
}
}, []);
如果在有依賴項的 useEffect 里 return一個函數(shù),那么只有這個依賴項被 setState 了才會執(zhí)行(也就是說return里面的操作只有在依賴項改變了才執(zhí)行淹遵,而return外面的第一次mouted也會執(zhí)行)比如:
const [b, setB] = useState(2);
const onClick = () => {
setA(567);
};
const onClickB = () => {
setB(6)
}
useEffect(() => {
console.log('bbb')
return () => {
console.log(1);
};
}, [b]);
return (
<div className="App" onClick={onClick}>
<h1 onClick={onClickB}>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
上面的代碼 'bbb' 頁面初始化就會執(zhí)行口猜,而 1 只有 b 變了才會執(zhí)行,所以return里面拿到的是上一次的state里的值透揣,而外面每次拿到的都是最新的state济炎,因為外面初始化的時候多執(zhí)行了一次(就相當于return 里拿到的是每次這個依賴項銷毀前上一次的數(shù)據(jù))
const App = () => {
const [obj, setObj] = useState({})
const handleClick = () => {
setObj({
a: Math.random()
})
}
useEffect(() => {
console.log(obj, 'obj')
return () => {
console.log(obj, 'return obj')
}
}, [obj])
useEffect(() => {
setObj({
a: Math.random()
})
}, [])
return (
<div onClick={handleClick}>
{obj.a}
</div>
)
}
代替 shouldComponentUpdate
該函數(shù)返回 true 時表示不更新函數(shù),返回 false 則重新更新
function Child(props){
return <h2>{props.count}</h2>
}
// 模擬shouldComponentUpdate
const areEqual = (prevProps, nextProps) => {
//比較
};
const PureChild = React.memo(Child, areEqual)
總結(jié)
1.副作用
對環(huán)境的改變即為副租用辐真,如修改document.title须尚,useEffect每次render后運行
2.用途
作為componentDidMount使用,[]作第二個參數(shù)
作為componentDidUpdate使用拆祈,可指定依賴
作為componentWillUnmount使用恨闪,通過return
以上三種用途可同時存在
3.特點
如果同時存在多個useEffect,會按照出現(xiàn)次序執(zhí)行
useLayoutEffect
- 布局副作用
useEffect在瀏覽器渲染完成后執(zhí)行放坏,而useLayoutEffect在瀏覽器渲染前執(zhí)行
案例:
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")
);
上面的代碼在我們頁面刷新或者打開的時候會閃一下(白屏)咙咽,主要原因與我們一個組件的渲染過程有關(guān),如下圖
首先調(diào)用App淤年,然后執(zhí)行構(gòu)造一個對應的虛擬DOM(VDOM)钧敞,之后將虛擬DOM渲染到DOM里,然后加到頁面中麸粮,頁面改變溉苛,最后才會執(zhí)行useEffect,而實際上我們最初的值0已經(jīng)掛載到了頁面上弄诲,這時候再在useEffect中修改就會出現(xiàn)二次更新頁面白屏的情況愚战,而useLayoutEffect是在DOM元素還未掛載到頁面中的時候就執(zhí)行了娇唯,所以它初次展現(xiàn)在頁面中就是1000,而不是0寂玲,也就不會有白屏現(xiàn)象
- 特點
useLayoutEffect總是比useEffect先執(zhí)行塔插,為了用戶體驗,優(yōu)先使用useEffect(優(yōu)先渲染)
useMemo
memo
問題1:當我們引用一個組件的時候如果這個組件依賴的屬性沒變拓哟,我們不希望這個組件去重新渲染想许,但是實際上只要是頁面上有任何數(shù)據(jù)變化了當前頁面上的組件就都會重新渲染,比如:
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}/>
</div>
);
}
function Child(props) {
console.log("child 執(zhí)行了");
console.log('假設這里有大量代碼')
return <div>child: {props.data}</div>;
}
我們點擊按鈕n被修改了断序,但是我們的Child組件并沒有依賴于n流纹,而是m,可是n變了违诗,Child也重新執(zhí)行了漱凝,解決辦法:對不需要每次更新的組件使用memo
const Child = React.memo(props => {
console.log("child 執(zhí)行了");
console.log("假設這里有大量代碼");
return <div>child: {props.data}</div>;
});
這樣我們在修改m之外的屬性都不會重新執(zhí)行我們的Child了
問題2:如果我們在上面的Child組件添加一個監(jiān)聽函數(shù),那么當我們點擊按鈕更新
n后较雕,Child組件又會重新執(zhí)行
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
const onClickChild = () => {
console.log(m);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild} />
{/* Child2 居然又執(zhí)行了 */}
</div>
);
}
function Child(props) {
console.log("child 執(zhí)行了");
console.log("假設這里有大量代碼");
return <div onClick={props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
原因是我們更新了n碉哑,App就會重新渲染,然后onClickChild每次都會生成功能相同的一個新的引用地址的函數(shù)亮蒋,所以Child就會認為onClick對應的屬性函數(shù)變了扣典,就會重新更新。
解決方法:使用useMemo
const onClickChild = useMemo(() => {}, [m])
這樣就只有m改變的時候才會重新渲染Child
總結(jié)
特點:
第一個參數(shù)是()=>value
第二個參數(shù)是依賴[m,n]
只有當依賴變化時慎玖,才會計算出新的value
如果依賴不變贮尖,那么久重用之前的value
注意:
如果你的value是個函數(shù),那么你就要寫成useMemo(()=>(x)=>console.log(x))
趁怔,這是一個返回函數(shù)的函數(shù)湿硝,我們也可以使用usecallback
簡寫
useCallback
用法:
useCallback(x => log(x), [m])
// 等價于
useMemo(()=>x => log(x), [m])
useCallback 使用場景:有一個父組件,其中包含子組件润努,子組件接收一個函數(shù)作為props关斜;通常而言,如果父組件更新了铺浇,子組件也會執(zhí)行更新痢畜;但是大多數(shù)場景下,更新是沒有必要的鳍侣,我們可以借助useCallback來返回函數(shù)丁稀,然后把這個函數(shù)作為props傳遞給子組件;這樣倚聚,子組件就能避免不必要的更新线衫。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
為什么要用 useMemo 和 useCallback?什么時候需要用惑折?
原因:因為我們每次 setState 的時候組件都會重新 render授账,對于 hooks 來說除了寫在 useEffect 里的方法不會重新聲明和額外執(zhí)行外枯跑,寫在useEffect 外的代碼隨著組件重新render都會從上而下把組件里的代碼重新運行一遍,對象和方法就會生成一個新的引用地址矗积,這個時候如果我們需要把useEffect 外聲明的對象和方法傳遞給其他組件的話全肮,那么其他組件使用的這個對象和方法的屬性就會一直改變,就會帶來不必要的 render棘捣,所以如果我們要把一個把一個屬性方法傳遞給其他組件的話,一定要使用useMemo 和 useCallback
useMemo 和 useEffect 依賴項不變的情況下會緩存之前的值和方法
在子組件不需要父組件的值和方法的情況下休建,只需要使用 memo 函數(shù)包裹子組件即可乍恐。
如果有方法傳遞給子組件,使用 useCallback
如果有值傳遞給子組件测砂,使用 useMemo
useEffect茵烈、useMemo、useCallback 都是自帶閉包的砌些。也就是說呜投,每一次組件的渲染,其都會捕獲當前組件函數(shù)上下文中的狀態(tài)(state, props)存璃,所以每一次這三種hooks的執(zhí)行仑荐,反映的也都是當前的狀態(tài),你無法使用它們來捕獲上一次的狀態(tài)纵东。對于這種情況粘招,我們應該使用 ref 來訪問。
useRef
目的:如果你需要一個值偎球,在組件不斷render的過程中保持不變(永遠都是同一個n洒扎,而不是說值不變)那么你就需要使用useRef
初始化:const count = useRef(0)
讀取:count.current
問題:問什么需要count.current來讀取值而不能直接count哪衰絮?
答:為了保證每次useRef是同一個引用地址袍冷,假設我們的count是一個對象,初始的時候是useRef({x:1})猫牡,然后你修改它就得count = {x:2}胡诗,這樣就會生成一個新的對象,就沒法保證每次都是同一個了镊掖,而如果是count = useRef({current: {x:1}})那么你每次修改都得count.current它的引用地址就不會變
useState/useReducer --> n每次都會變(都是不同的變量n)
useMemo/useCallback --> 只有依賴的[m]變的時候乃戈,fn才會變
useRef --> 永遠不變
與vue3的ref相比
初始化:const count = ref(0)
讀取:count.value
不同點:當count.value變化時亩进,Vue3會自動render
forwardRef
如果我們使用的是函數(shù)組件症虑,我們想在組件里獲取到外界傳來的ref的話,那么我們直接通過props來獲取就會報錯
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button2 ref={buttonRef}>按鈕</Button2>
{/* 看瀏覽器控制臺的報錯 */}
</div>
);
}
const Button2 = props => {
return <button className="red">{props.ref}</button>;
};
所以我們需要使用forwardRef
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按鈕</Button3>
</div>
);
}
const Button3 = React.forwardRef((props, ref) => {
console.log(ref);
return (
<button className="red" ref={ref} {...props}>
{props.children}
</button>
);
});
useRef與forwardRef的比較
- useRef
可以用來引用DOM對象归薛,也可以用來引用普通對象 - forwardRef
由于props不包含ref谍憔,所以需要forwardRef
useImperativeHandle
應該叫setRef匪蝙,用于自定義ref的屬性
不用useImperativeHandle的代碼
https://codesandbox.io/s/awesome-goldwasser-v7vsp
使用useImperativeHandle的代碼
https://codesandbox.io/s/elegant-poitras-mxoym
自定義Hook
- 封裝數(shù)據(jù)操作
簡單例子:
https://codesandbox.io/s/wizardly-tesla-sy077 - 復雜案例
https://codesandbox.io/s/jovial-villani-v0xue
過時閉包
function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
const message = `Current value is ${value}`;
return function logValue() {
console.log(message);
};
}
return increment;
}
const inc = createIncrement(1);
const log = inc(); // logs 1
inc(); // logs 2
inc(); // logs 3
// Does not work!
log(); // logs "Current value is 1"
上面代碼我們的vlaue已經(jīng)變成了3了,可我們的message打印出來還是1习贫,這就是因為我們調(diào)用log()的時候逛球,實際上保留了第一次的值;
解決辦法
1.每次log調(diào)用的時候都重新取上一次的log
const inc = createIncrement(1);
inc(); // logs 1
inc(); // logs 2
const latestLog = inc(); // logs 3
// Works!
latestLog(); // logs "Current value is 3"
- 如果用舊的log那么你每次都要去讀新的value苫昌,也就是把message放到最內(nèi)層
function createIncrementFixed(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
return function logValue() {
const message = `Current value is ${value}`;
console.log(message);
};
}
return increment;
}
const inc = createIncrementFixed(1);
const log = inc(); // logs 1
inc(); // logs 2
inc(); // logs 3
// Works!
log(); // logs "Current value is 3"