useCallback
在了解useCallback之前先来看一个案例
// parentComponent.js
import React, { useState } from "react";
import { ChildComponent } from "./childComponent";
export function ParentComponent() {
console.log("ParentComponent running");
const [parentCount, setParent] = useState(1);
const parentCountHandler = () => {
setParent(parentCount + 1);
};
const childHandler = () => {
console.log("childHandler", parentCount);
};
return (
<div>
<div onClick={parentCountHandler}>change parentCount</div>
<ChildComponent onClickChild={childHandler} />
</div>
);
}
// childComponent.js
import React from "react";
export const ChildComponent = (props) => {
console.log("ChildComponent running");
return <div onClick={props.onClickChild}>ChildComponent</div>;
};
当我们点击[change parentCount]改变parentCount的值时,控制台打印的结果如下:
可以看出当parentCount改变的时候ParentComponent和ChildComponent全部re render了。但其实子组件ChildComponent并没有使用parentCount,所以子组件的重新渲染并不是我们期望的。那么想要阻止ChildComponent re render我们肯定会想到PureComponent、memo、shouldComponentUpdate,这三种方法的目的都是通过对父级传递的props进行浅比较来控制子组件的re render。 接下来我们用其中的memo来处理以下子组件
import React, { memo } from "react";
// 使用memo包裹住函数组件
export const ChildComponent = memo((props) => {
console.log("ChildComponent running");
return <div onClick={props.onClickChild}>ChildComponent</div>;
});
遗憾的是发现输出结果依然是子组件进行了re render。为什么会这样? 因为父组件传递给子组件是一个函数childHandler,而函数每次都会被重新创建,分配新的内存地址,这样子组件通过对props.onClickChild的浅比较结果自然是props.onClickChild发生了改变,因此依然会执行re render。 接下来该useCallback上场了
import React, { useCallback, useState } from "react";
import { ChildComponent } from "./childComponent";
export function ParentComponent() {
console.log("ParentComponent running");
const [parentCount, setParent] = useState(1);
const parentCountHandler = () => {
setParent(parentCount + 1);
};
// 使用useCallback包裹住回调函数
const childHandler = useCallback(() => {
console.log("childHandler", parentCount);
}, []);
return (
<div>
<div onClick={parentCountHandler}>change parentCount</div>
<ChildComponent onClickChild={childHandler} />
</div>
);
}
useCallback上场后我们终于得到了期望的结果,父组件自己re render7次,子组件岿然不动。
再来分析以下useCallback为什么能做到。
因为useCallback真正的作用是缓存了每次渲染是的回调函数的实例,只有useCallback的参数中的值发生了变化才会重新创建,否则就一直不变。而子组件通过浅比较发现props的值没有变化,自然就不会重新渲染。这也就是为什么useCallback和useMemo需要成对使用的原因。
useReducer
当然除了使用useCallback可以避免子组件不必要的渲染,我们也可以考虑useReducer
import React, { useReducer, useState } from "react";
import { ChildComponent } from "./childComponent";
export function ParentComponent() {
console.log("ParentComponent running");
const [parentCount, setParent] = useState(1);
const parentCountHandler = () => {
setParent(parentCount + 1);
};
// useReducer替代了原来的useCallback
const [_, childHandler] = useReducer(() => {
console.log("childHandler", parentCount);
});
return (
<div>
<div onClick={parentCountHandler}>change parentCount</div>
<ChildComponent onClickChild={childHandler} />
</div>
);
}
以上代码可以实现和useCallback一样的效果。因为React会保证dispatch始终是不变的。
useContext
我们再来看一个比较复杂的例子方便我们引出useContext
import React, { useReducer, useState } from "react";
import { ChildComponent } from "./childComponent";
const students = [];
const changeStudents = (oldData, action) => {
switch (action.type) {
case "add":
return [...oldData, { name: action.data.name }];
case "delete":
return oldData.filter((item) => item.name !== action.data.name);
default:
return oldData;
}
};`
export function ParentComponent() {
console.log("ParentComponent running");
const [parentCount, setParent] = useState(1);
const parentCountHandler = () => {
setParent(parentCount + 1);
};
const [studentsState, onSubmit] = useReducer(changeStudents, students);
return (
<div>
<div onClick={parentCountHandler}>change parentCount</div>
<ChildComponent onSubmit={onSubmit} />
{studentsState.map((child) => (
<div key={child.name}>{child.name}</div>
))}
</div>
);
}
import React, { memo } from "react";
import { AddComponent } from "./addComponent";
import { DeleteComponent } from "./deleteComponent";
export const ChildComponent = memo((props) => {
console.log("ChildComponent running");
return (
<div>
<AddComponent onSubmit={props.onSubmit} />
<DeleteComponent onSubmit={props.onSubmit} />
</div>
);
});
import React, { useRef, useState } from "react";
export const AddComponent = (props) => {
const [text, setText] = useState("");
const inputRef = useRef();
return (
<>
<input
ref={inputRef}
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div
onClick={() => {
props.onSubmit({ type: "add", data: { name: text } });
inputRef.current.value = "";
}}
>
add
</div>
</>
);
};
import React from "react";
export const DeleteComponent = (props) => {
return (
<div
onClick={() => {
props.onSubmit({ type: "delete", data: { name: "李雷" } });
}}
>
delete
</div>
);
};
通过上边的代码,我们实现的是一个三级组件的嵌套,通过最顶级ParentComponent传递给最子组件的子组件AddComponent和DeleteComponent回调方法,来修改父组件中的studentsState值。其中我为了把回调函数传递给第三级,我先把函数传递给了第二级。实际开发中那如果级别很多的话,可能就要继续层层传递下去,那这样估计你往上找的时候一定会头大。别着急,我们还没让useContext出场呢,再来修改以下代码
import React, { useReducer, useState } from "react";
import { ChildComponent } from "./childComponent";
const students = [];
const changeStudents = (oldData, action) => {
switch (action.type) {
case "add":
return [...oldData, { name: action.data.name }];
case "delete":
return oldData.filter((item) => item.name !== action.data.name);
default:
return oldData;
}
};
export const ParentContext = React.createContext(); // 创建了ParentContext
export function ParentComponent() {
console.log("ParentComponent running");
const [parentCount, setParent] = useState(1);
const parentCountHandler = () => {
setParent(parentCount + 1);
};
const [studentsState, onSubmit] = useReducer(changeStudents, students);
return (
<ParentContext.Provider value={onSubmit}> //使用了 ParentContext.Provider传递onSubmit
<div>
<div onClick={parentCountHandler}>change parentCount</div>
<ChildComponent /> // 删除了给ChildComponet传递的回调函数
{studentsState.map((child) => (
<div key={child.name}>{child.name}</div>
))}
</div>
</ParentContext.Provider>
);
}
import React, { memo } from "react";
import { AddComponent } from "./addComponent";
import { DeleteComponent } from "./deleteComponent";
export const ChildComponent = memo((props) => {
console.log("ChildComponent running");
return (
<div>
<AddComponent /> // 删除了回调函数的传递
<DeleteComponent /> // 删除了回调函数的传递
</div>
);
});
import React, { useContext, useRef, useState } from "react";
import { ParentContext } from "./parentComponent";
export const AddComponent = (props) => {
const [text, setText] = useState("");
const inputRef = useRef();
const onSubmit = useContext(ParentContext); // 通过useContext获取onSubmit
return (
<>
<input
ref={inputRef}
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div
onClick={() => {
onSubmit({ type: "add", data: { name: text } });
inputRef.current.value = "";
}}
>
add
</div>
</>
);
};
import React, { useContext } from "react";
import { ParentContext } from "./parentComponent";
export const DeleteComponent = (props) => {
const onSubmit = useContext(ParentContext); // 通过useContext获取onSubmit
return (
<div
onClick={() => {
onSubmit({ type: "delete", data: { name: "李雷" } });
}}
>
delete
</div>
);
};
验证一下你会发现执行结果是一样的,但是却解决了层层传递回调函数的烦恼。