React Hooks使用常見的坑
React Hooks 是 React 16.8 引入的新特性,允許我們在不使用 Class 的前提下使用 state 和其他特性。React Hooks 要解決的問題是狀態(tài)共享,是繼 render-props 和 higher-order components 之后的第三種狀態(tài)邏輯復(fù)用方案,不會產(chǎn)生 JSX 嵌套地獄問題。
為什么會有Hooks?
介紹Hooks之前,首先要給大家說一下React的組件創(chuàng)建方式,一種是類組件,一種是純函數(shù)組件,并且React團隊希望,組件不要變成復(fù)雜的容器,最好只是數(shù)據(jù)流的管道。開發(fā)者根據(jù)需要,組合管道即可。也就是說組件的最佳寫法應(yīng)該是函數(shù),而不是類。。
函數(shù)組件比類組件更加方便實現(xiàn)業(yè)務(wù)邏輯代碼的分離和組件的復(fù)用,函數(shù)組件也比類組件輕量,沒有react hooks之前,函數(shù)組件是無法實現(xiàn)LocalState的,這導(dǎo)致有l(wèi)ocalstate狀態(tài)的組件無法用函數(shù)組件來書寫,這限制了函數(shù)組件的應(yīng)用范圍,而react hooks擴展了函數(shù)組件的能力??墒窃谑褂玫倪^程中,也要注意下面這些問題,否則就會掉進坑里,造成性能損失。按照下面的方法做,,才能避開這些陷阱。
1. 將與狀態(tài)改變無關(guān)的變量和方法提取到組件函數(shù)外面
每次狀態(tài)改變時,整個函數(shù)組件都會重新執(zhí)行一遍。導(dǎo)致函數(shù)組件內(nèi)部定義的方法和變量,都會重新創(chuàng)建,重新給它們分配內(nèi)存,這會導(dǎo)致性能受到影響。
import React, {useState,useCallback} from "react";
// 測試每次狀態(tài)改變時,方法是不是重新分配內(nèi)存
let testFooMemoAlloc = new Set();
const Page = (props:any) => {
console.log('每次狀態(tài)改變,函數(shù)組件從頭開始執(zhí)行')
const [count, setCount] = useState(0);
const calc = () => {
setCount(count + 1);
}
const bar = {
a:1,
b:2,
c: '與狀態(tài)無關(guān)的變量定義'
}
const doFoo = () => {
console.log('與狀態(tài)無關(guān)的方法');
}
testFooMemoAlloc.add(doFoo)
return (
<>
<button onClick={calc}>加1</button>
<p>count:{count}</p>
<p>testFooMemoAlloc.size增加的話,說明每次都重新分配了內(nèi)存:{testFooMemoAlloc.size}</p>
</>
)
}
export default Page;

與改變狀態(tài)相關(guān)的變量和方法,必須放在hooks組件內(nèi),而無狀態(tài)無關(guān)的變量和方法,可以提取到函數(shù)組件外,避免每次狀態(tài)更新,都重新分配內(nèi)存。也可以分別使用useMemo和useCallback包裹變量與函數(shù),也能達到同樣的效果,后面會講。
import React, {useState,useCallback} from "react";
// 測試每次狀態(tài)改變時,方法是不是重新分配內(nèi)存
let testFooMemoAlloc = new Set();
const bar = {
a:1,
b:2,
c: '與狀態(tài)無關(guān)的變量定義'
}
const doFoo = () => {
console.log('與狀態(tài)無關(guān)的方法');
}
const Page = (props:any) => {
console.log('每次狀態(tài)改變,函數(shù)組件從頭開始執(zhí)行')
const [count, setCount] = useState(0);
const calc = () => {
setCount(count + 1);
}
testFooMemoAlloc.add(doFoo)
return (
<>
<button onClick={calc}>加1</button>
<p>count:{count}</p>
<p>testFooMemoAlloc.size增加的話,說明每次都重新分配了內(nèi)存:{testFooMemoAlloc.size}</p>
</>
)
}
export default Page;

2. 用memo對子組件進行包裝
父組件引入子組件,會造成一些不必要的重復(fù)渲染,每次父組件更新count,子組件都會更新。
import React,{useState} from "react";
const Child = (props:any) => {
console.log('子組件?')
return(
<div>我是一個子組件</div>
);
}
const Page = (props:any) => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<Child />
</>
)
}
export default Page;

使用memo,count變化子組件沒有更新
import React,{useState,memo} from "react";
const Child = memo((props:any) => {
console.log('子組件?')
return(
<div>我是一個子組件</div>
);
})
const Page = (props:any) => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<Child />
</>
)
}
export default Page;

給memo傳入第二個參數(shù),開啟對象深度比較。當(dāng)子組件傳遞的屬性值未發(fā)生改變時,子組件不會做無意義的render。
memo不僅適用于函數(shù)組件,也適用于class組件,是一個高階組件,默認情況下只會對復(fù)雜對象做淺層比較,如果想做深度比較,可以傳入第二個參數(shù)。與shouldComponentUpdate不同的是,deepCompare返回true時,不會觸發(fā) render,如果返回false,則會。而shouldComponentUpdate剛好與其相反。
import React, {useState, memo } from "react";
import deepCompare from "./deepCompare";
const Child = memo((props:any) => {
console.log('子組件')
return (
<>
<div>我是一個子組件</div>
<div>{ props.fooObj.a}</div>
</>
);
}, deepCompare)
const Page = (props:any) => {
const [count, setCount] = useState(0);
const [fooObj, setFooObj] = useState({ a: 1, b: { c: 2 } })
console.log('頁面開始渲染')
const calc = () => {
setCount(count + 1);
if (count === 3) {
setFooObj({ b: { c: 2 }, a: count })
}
}
const doBar = () => {
console.log('給子組件傳遞方法,測試一下是否會引起不必須的渲染')
}
return (
<>
<button onClick={calc}>加1</button>
<p>count:{count}</p>
<Child fooObj={fooObj} doBar={doBar} />
</>
)
}
export default Page;
// 深度比較兩個對象是否相等
export default function deepCompare(prevProps: any, nextProps: any) {
const len: number = arguments.length;
let leftChain: any[] = [];
let rightChain: any = [];
// // console.log({ arguments });
//
if (len < 2) {
// console.log('需要傳入2個對象,才能進行兩個對象的屬性對比');
return true;
}
// for (let i = 1; i < len; i++) {
// leftChain = [];
// rightChain = [];
console.log({ prevProps, nextProps });
if (!compare2Objects(prevProps, nextProps, leftChain, rightChain)) {
// console.log('兩個對象不相等');
return false;
}
// }
// console.log('兩個對象相等');
return true;
}
function compare2Objects(prevProps: any, nextProps: any, leftChain: any, rightChain: any) {
var p;
// 兩個值都為為NaN時,在js中是不相等的, 而在這里認為相等才是合理的
if (isNaN(prevProps) && isNaN(nextProps) && typeof prevProps === 'number' && typeof nextProps === 'number') {
return true;
}
// 原始值比較
if (prevProps === nextProps) {
console.log('原始值', prevProps, nextProps);
return true;
}
// 構(gòu)造類型比較
if (
(typeof prevProps === 'function' && typeof nextProps === 'function') ||
(prevProps instanceof Date && nextProps instanceof Date) ||
(prevProps instanceof RegExp && nextProps instanceof RegExp) ||
(prevProps instanceof String && nextProps instanceof String) ||
(prevProps instanceof Number && nextProps instanceof Number)
) {
console.log('function', prevProps.toString() === nextProps.toString());
return prevProps.toString() === nextProps.toString();
}
// 兩個比較變量的值如果是null和undefined,在這里會退出
if (!(prevProps instanceof Object && nextProps instanceof Object)) {
console.log(prevProps, nextProps, 'prevProps instanceof Object && nextProps instanceof Object');
return false;
}
if (prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)) {
console.log('prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)');
return false;
}
// 構(gòu)造器不相等則兩個對象不相等
if (prevProps.constructor !== nextProps.constructor) {
console.log('prevProps.constructor !== nextProps.constructor');
return false;
}
// 原型不相等則兩個對象不相等
if (prevProps.prototype !== nextProps.prototype) {
console.log('prevProps.prototype !== nextProps.prototype');
return false;
}
if (leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1) {
console.log('leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1');
return false;
}
// 遍歷下次的屬性對象,優(yōu)先比較不相等的情形
for (p in nextProps) {
if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) {
console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)');
return false;
} else if (typeof nextProps[p] !== typeof prevProps[p]) {
console.log('typeof nextProps[p] !== typeof prevProps[p]');
return false;
}
}
// console.log('p in prevProps');
// 遍歷上次的屬性對象,優(yōu)先比較不相等的情形
for (p in prevProps) {
// 是否都存在某個屬性值
if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) {
console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)');
return false;
}
// 屬性值的類型是否相等
else if (typeof nextProps[p] !== typeof prevProps[p]) {
console.log('typeof nextProps[p] !== typeof prevProps[p]');
return false;
}
console.log('typeof prevProps[p]', typeof prevProps[p]);
switch (typeof prevProps[p]) {
// 對象類型和函數(shù)類型的處理
case 'object':
case 'function':
leftChain.push(prevProps);
rightChain.push(nextProps);
if (!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)) {
console.log('!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)');
return false;
}
leftChain.pop();
rightChain.pop();
break;
default:
// 基礎(chǔ)類型的處理
if (prevProps[p] !== nextProps[p]) {
return false;
}
break;
}
}
return true;
}

3.用useCallback對組件方法進行包裝
當(dāng)父組件傳遞方法給子組件的時候,memo好像沒什么效果,無論是用const定義的方法,還在用箭頭函數(shù)或者bind定義的方法,子組件還是執(zhí)行了
import React, { useState,memo } from 'react';
//子組件會有不必要渲染的例子
interface ChildProps {
changeName: ()=>void;
}
const FunChild = ({ changeName}: ChildProps): JSX.Element => {
console.log('普通函數(shù)子組件')
return(
<>
<div>我是普通函數(shù)子組件</div>
<button onClick={changeName}>普通函數(shù)子組件按鈕</button>
</>
);
}
const FunMemo = memo(FunChild);
const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
console.log('箭頭函數(shù)子組件')
return(
<>
<div>我是箭頭函數(shù)子組件</div>
<button onClick={changeName.bind(null,'test')}>箭頭函數(shù)子組件按鈕</button>
</>
);
}
const ArrowMemo = memo(ArrowChild);
const BindChild = ({ changeName}: ChildProps): JSX.Element => {
console.log('Bind函數(shù)子組件')
return(
<>
<div>我是Bind函數(shù)子組件</div>
<button onClick={changeName}>Bind函數(shù)子組件按鈕</button>
</>
);
}
const BindMemo = memo(BindChild);
const Page = (props:any) => {
const [count, setCount] = useState(0);
const name = "test";
const changeName = function() {
console.log('測試給子組件傳遞方法,使用useCallback后,子組件是否還會進行無效渲染');
}
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<ArrowMemo changeName={()=>changeName()}/>
<BindMemo changeName={changeName.bind(null)}/>
<FunMemo changeName={changeName} />
</>
)
}
export default Page;

使用useCallback,參數(shù)為[],頁面初始渲染后,改變count的值,傳遞普通函數(shù)的子組件不再渲染, 傳遞箭頭函數(shù)和bind方式書寫的方法的子組件還是會渲染
import React, { useState,memo ,useCallback} from 'react';
//子組件會有不必要渲染的例子
interface ChildProps {
changeName: ()=>void;
}
const FunChild = ({ changeName}: ChildProps): JSX.Element => {
console.log('普通函數(shù)子組件')
return(
<>
<div>我是普通函數(shù)子組件</div>
<button onClick={changeName}>普通函數(shù)子組件按鈕</button>
</>
);
}
const FunMemo = memo(FunChild);
const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
console.log('箭頭函數(shù)子組件')
return(
<>
<div>我是箭頭函數(shù)子組件</div>
<button onClick={changeName.bind(null,'test')}>箭頭函數(shù)子組件按鈕</button>
</>
);
}
const ArrowMemo = memo(ArrowChild);
const BindChild = ({ changeName}: ChildProps): JSX.Element => {
console.log('Bind函數(shù)子組件')
return(
<>
<div>我是Bind函數(shù)子組件</div>
<button onClick={changeName}>Bind函數(shù)子組件按鈕</button>
</>
);
}
const BindMemo = memo(BindChild);
const Page = (props:any) => {
const [count, setCount] = useState(0);
const name = "test";
const changeName = useCallback(() => {
console.log('測試給子組件傳遞方法,使用useCallback后,子組件是否還會進行無效渲染');
},[])
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<ArrowMemo changeName={()=>changeName()}/>
<BindMemo changeName={changeName.bind(null)}/>
<FunMemo changeName={changeName} />
</>
)
}
export default Page;

4.用useMemo對組件中的對象變量進行包裝
在子組件使用了memo,useCallback的情況下,給子組件傳遞一個對象屬性,對象值和方法都未發(fā)生改變的情況下,父組件無關(guān)狀態(tài)變更,子組件也會重新渲染。
import React, { useState,memo ,useCallback} from 'react';
//子組件會有不必要渲染的例子-使用了memo,useCallback的情況下,給子組件傳遞一個對象屬性值
interface ChildProps {
childStyle: { color: string; fontSize: string;};
changeName: ()=>void;
}
const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => {
console.log('普通函數(shù)子組件')
return(
<>
<div style={childStyle}>我是普通函數(shù)子組件</div>
<button onClick={changeName}>普通函數(shù)子組件按鈕</button>
</>
);
}
const FunMemo = memo(FunChild);
const Page = (props:any) => {
const [count, setCount] = useState(0);
const childStyle = {color:'green',fontSize:'16px'};
const changeName = useCallback(() => {
console.log('測試給子組件傳遞方法,使用useCallback后,子組件是否還會進行無效渲染');
},[])
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<FunMemo childStyle={childStyle} changeName={changeName} />
</>
)
}
export default Page;

使用useMemo可以解決給子組件傳遞對象屬性時的不必要更新問題。
import React, { useState,memo, useMemo, useCallback} from 'react';
//子組件會有不必要渲染的例子
interface ChildProps {
childStyle: { color: string; fontSize: string;};
changeName: ()=>void;
}
const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => {
console.log('普通函數(shù)子組件')
return(
<>
<div style={childStyle}>我是普通函數(shù)子組件</div>
<button onClick={changeName}>普通函數(shù)子組件按鈕</button>
</>
);
}
const FunMemo = memo(FunChild);
const Page = (props:any) => {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
const childStyle = {color:'green',fontSize:'16px'};
const changeName = useCallback(() => {
setName('變一下名稱')
}, [])
const childStyleMemo = useMemo(() => {
return {
color: name === '變一下名稱' ? 'red':'green',
fontSize: '16px'
}
}, [name])
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<FunMemo childStyle={childStyleMemo} changeName={changeName} />
</>
)
}
export default Page;

以上就是React Hooks使用避坑指南的詳細內(nèi)容,更多關(guān)于React Hooks使用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
webpack4 + react 搭建多頁面應(yīng)用示例
這篇文章主要介紹了webpack4 + react 搭建多頁面應(yīng)用示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
React-Route6實現(xiàn)keep-alive效果
本文主要介紹了React-Route6實現(xiàn)keep-alive效果,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧<BR>2022-06-06
ReactJS?應(yīng)用兼容ios9對標(biāo)ie11解決方案
這篇文章主要為大家介紹了ReactJS?應(yīng)用兼容ios9對標(biāo)ie11解決方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01

