必须牢牢掌握的几个React Hooks💪🏻
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook 规则
- 只在最顶层使用 Hook
- 只在 React 函数中调用 Hook。(或:在自定义 Hook 中调用其他 Hook)
useEffect
useEffect接收一个方法作为第一个参数,该方法会在每次渲染完成之后被调用。
它还会接收一个数组作为第二个参数,这个数组里的每一项内容都会被用来进行渲染前后的对比,如果没有变化,则不会调用该副作用。
useEffect 的依赖如果是个空数组,只会在 DOM 渲染后触发一次,以后都不会触发,相当于 componentDidMount
。可以看做是 componentDidMount、componentDidUpdate、componentWillUnmount
三个钩子的组合。
useEffect可以返回一个函数,用于清除副作用的回调。每当组件卸载,或者组件重新render,都会触发这个函数。
而且是先执行 return 函数
,再执行 useEffect
内部逻辑。
注意事项
-
对于传入的对象类型,React只会判断引用是否改变,不会判断对象的属性是否改变,所以建议依赖数组中传入的变量都采用基本类型。
-
useEffect的清除函数在每次重新渲染时都会执行,而不是只在卸载组件的时候执行。
useLayoutEffect
在使用方式上,和 useEffect
一样。大部分情况只使用 useEffect
即可,当 useEffect 处理 DOM 相关逻辑时,出现问题了,再使用 useLayoutEffect
。
至于出现什么问题,我们先来看一下它俩的执行时机。
组件更新过程
浏览器中 JS 线程和渲染线程是互斥的,渲染线程必须等待 JS 线程执行完毕,才开始渲染组件。
而我们的组件从 state 变化到渲染,大概可以分为如下几步:
-
改变 state,触发更新 state 变量的方法
-
React 根据组件返回的 vDOM 进行 diff 对比,得到新的 Virtual DOM
-
将新的 VDom 交给渲染线程处理,绘制到浏览器上
-
用户看到新的内容
而 useEffect
是在第 3 步之后执行的,也就是在浏览器绘制之后才调用。而且 useEffect 还是异步执行的,所谓异步就是被 requestIdleCallback 封装,只在浏览器空闲时候才会执行,保证了不会阻塞浏览器的渲染过程。
useLayoutEffect
就不一样,它会在第二步
之后(diff 出新的 vDOM 之后
),第三步之前执行,也就是渲染之前同步执行的,所以会等它执行完再渲染页面到浏览器上。
如果我们要操作 DOM,或者不想出现 内容闪烁
的问题,我们就是用 useLayoutEffect
明显的闪烁问题
useEffect(() => {
async function fn() {
if (num === 1) {
let count = 0;
console.time();
for (let i = 0; i < 99999999; i++) {
count++;
}
console.timeEnd();
setNum(Math.random());
}
}
fn();
return () => {
console.log("useEffect tail function");
};
}, [num]);
没有闪烁问题
useLayoutEffect(() => {
async function fn() {
if (num === 1) {
let count = 0;
console.time();
for (let i = 0; i < 99999999; i++) {
count++;
}
console.timeEnd();
setNum(Math.random());
}
}
fn();
return () => {
console.log("useLayoutEffect tail function");
};
}, [num]);
总结
- 优先使用 useEffect,因为它是异步执行的,不会阻塞渲染
- 会影响到渲染的操作尽量放到 useLayoutEffect中去,避免出现闪烁问题
- useLayoutEffect和componentDidMount是等价的,会同步调用,阻塞渲染
- 在服务端渲染的时候 useLayoutEffect 无效,使用 useEffect
性能优化—— useCallback、useMemo、memo
尽可能的保证组件不去发生变化,发生变化的因素有:state、props、context
。
那么 React
是如何比较这三者的呢? 答案是 内存地址
。
比如说,对比一个 function
,对比的就是这个函数在内存中的地址,通过地址的判断,从而判断 props 是否发生了改变。
React.memo
React.memo 包裹一个组件,来对它的 props 进行浅比较。等效于 PureComponent,但它只比较 props。(也可以通过第二个参数指定一个自定义的比较函数
来比较新旧 props。如果函数返回 true,就会跳过更新。)
// 不使用 memo,每一次 setCount,都会造成 Child 组件重新 render
const Child = () => {
console.log('Child')
return (
<>Child component</>
)
}
const Demo = () => {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child />
</>
)
}
// 通过 memo 包裹后,Child 组件不会再重新 render了。
const Child = memo(() => {
console.log('Child')
return (
<>Child component</>
)
})
当 memo 感知 props 没有发生改变时,不会重新 render 组件。如果传入 count 进来,Child组件就会重新 render。
总结:
- 如果我们将 setCount 当做 prop 传入进来,Child 不会重新render(
因为 setCount 在内存中的地址没有发生改变
) - 如果传入我们自己定义的方法 (fn)进来,Child会重新 render,因为 Demo 组件每次更新 count 后,重新生成了 fn 函数。
- 只是传了个 fn ,不想让 Child 组件更新怎么办?那就要用到
useCallback
钩子了
useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算。
// 只有当 count 发生变化时,才会重新计算
const computedCount = useMemo(() => {
return count * 2
}, [count])
useMemo
也允许你跳过一次子节点的昂贵的重新渲染,比如组件初始化时,需要一次大量的计算,后续就不会再改变了:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
下面这个例子,即使我们用 memo
包裹了组件,因为 setCount
每次会引起 Demo 组件重新 render,生成了新的 fn 函数
(内存地址发生了变化),导致 Child 也会重新 render。
interface IChild {
fn: React.Dispatch<React.SetStateAction<number>>
}
const Child = memo((props: IChild) => {
console.log('Child')
return (
<>Child component</>
)
})
const Demo = () => {
const [count, setCount] = useState(0)
const fn = () => console.log('is fn')
return (
<>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child fn={fn} />
</>
)
}
我们不想让 fn 函数的 内存地址
发生变化,怎么办呢?使用 useCallback
钩子将其包裹起来即可。
注意:useMemo 也可以这样用,缓存 fn,从而使得 Child 组件不会重复 render。
// 省略...
const fn = useCallback(() => {
console.log('is fn')
}, [])
// 省略...
这样 fn
函数就是一个缓存函数了,即使 count 不停的发生变化,也不会造成 Child 组件重复 render。
总结:
- 当 Demo 组件内部 state 发生了改变引起 Demo 和 Child 组件重新 render
- 并且 Child 组件接受了一个来自 Demo 组件自定义的方法(fn)
- 如果不希望 Child 组件重新 render,那么就需要用 useCallback 钩子将自定义方法
fn
包裹起来 - 因为 Child 组件 props 里面的 fn 和 useCallback 返回的 fn 指向的是内存中的同一个地址,那么 Child 组件就不会更新
- useCallback 返回新函数的条件是:依赖项(第二个参数)发生了改变。
- 如果说我们的 Child 组件,本身就是需要根据 count 变化而变化,那么就不需要加这个缓存 API了,反而增加其计算负担。
设计组件
不要为了使用钩子,过渡的使用钩子,好的页面设计,也许用不上这些钩子。
把不变的组件和变化的组件抽离出来!
比如可以把 count 相关部分抽离成一个 Count 组件,使其和 Child 组件同层级排列,Count 组件和 Child 组件分开了,也不会引起 Child 组件做多余的 render。
<Count />
<Child prop={fn} />
或者是通过 props.children 渲染 Child,也不会造成 Child 重新 render。
const Count = (props: any) => {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count => count + 1)}>+</button>
{/* children 不会重新 render */}
{props.children}
</>
)
}
const Demo = () => {
// fn 永远不会变化
const fn = () => {}
return (
<>
<Count>
<Child fn={fn} />
</Count>
</>
)
}
useRef / createRef
获取 DOM 元素。
当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为你的 ref 对象的 current 属性。
当节点从屏幕上移除时,React 将把 current 属性设回 null。
const inputRef = useRef(null) // const inputRef = React.createRef()
inputRef.current.focus()
// ...
return <input ref={inputRef} />;
通过使用 ref,你可以确保:
- 可以在重新渲染之间 存储信息(不像是普通对象,每次渲染都会重置)。
- 改变它 不会触发重新渲染(不像是 state 变量,会触发重新渲染)。
- 对于你的组件的每个副本来说,这些信息都是本地的(不像是外面的变量,是共享的)。
注意:
- 不要在渲染期间写入 或者读取 ref.current。
function MyComponent() {
// ...
// 🚩 不要在渲染期间写入 ref
myRef.current = 123;
// ...
// 🚩 不要在渲染期间读取 ref
return <h1>{myOtherRef.current}</h1>;
}
- 可以在 事件处理程序或者 effects 中读取和写入 ref。
function MyComponent() {
// ...
useEffect(() => {
// ✅ 你可以在 effects 中读取和写入 ref
myRef.current = 123;
});
// ...
function handleClick() {
// ✅ 你可以在事件处理程序中读取和写入 ref
doSomething(myOtherRef.current);
}
// ...
}
编写一个获取 DOM信息的 hook
假如我们想要获取一个 dom 的 getBoundingClientRect
信息,我可能这样做:
const getHeight = useMemo(() => {
return (node: HTMLObjectElement) => {
if (node) {
setHeight(node.getBoundingClientRect().height)
}
}
}, [])
// 或者
const getHeight = useCallback((node: HTMLObjectElement) => {
if (node) {
setHeight(node.getBoundingClientRect().height)
}
}, [])
但是,获取 DOM 信息的逻辑其实很通用,所以考虑下,将 ref 逻辑抽离成一个 Hook
。
// hook
const useClientRect = () => {
const [rect, setRect] = useState(null)
const ref = useCallback(node => {
if (node) {
setRect(node.getBoundingClientRect())
}
}, [])
return [rect, ref]
}
使用
const [rect, ref] = useClientRect()
<h1 ref={ref}>是 H1 标签 {count}</h1>
{
rect && <span>{rect.height}</span>
}
React.forwardRef
React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。
const FancyInput = forwardRef((props, ref) => (
<input ref={inputRef} {...props} />
))
// 这样可以拿到 input 元素了
const inputEle = React.createRef() // const inputEle = useRef(null)
<FancyInput ref={inputEle} />
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
对上述代码中所涉参数说明如下。
- ref:定义current对象的ref属性。
- createHandle:这是一个函数,返回值是一个对象,即这个ref的current对象。
- [deps]:依赖列表。当监听的依赖发生变化时,useImperativeHandle才会重新将子组件的实例属性输出到父组件ref的current属性上;如果为空数组,则不会重新输出。
// 注意:该组件接收到的 ref 已不再被转发到 <input> 中。
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
}));
return <input ref={inputRef} {...props} />;
})
const ref = React.createRef()
<FancyInput ref={ref} />
// 通过 ref 获取到 useImperativeHandle 暴露的方法
ref.current.focus()
ref.current.scrollIntoView()
useReducer & useContext(组件级的状态管理)
// reducers/app-reducer.js
import { createContext } from "react";
// 创建上下文
export const AppContext = createContext(null);
// 定义 app reducer
export const appReducer = (state, action) => {
switch (action.type) {
case "UPDATE_AGE":
return {
...state,
user: {
...state.user,
age: action.payload
}
};
case "UPDATE_NAME":
return {
...state,
user: {
...state.user,
name: action.payload
}
};
default:
return state;
}
};
使用上下文,可以使用 AppContext.consumer
,但是有了 useContext
了就没必要了。
根组件使用 AppContext.Provider
提供状态 initState
import { appReducer, AppContext } from "./reducers/app-reducer.js";
function App() {
const initState = {
type: "person",
user: {
age: 18,
name: "alex.cheng"
}
};
const [state, dispatch] = useReducer(appReducer, initState);
return (
<AppContext.Provider value={state}>
<Child1 />
</AppContext.Provider>
);
}
子孙子组件通过 useContext(AppContext)
获取上下文提供的状态。
const Child = () => {
const context = useContext(AppContext);
return (
<>
<p>{JSON.stringify(context)}</p>
</>
);
};
一起交流
不管你遇到什么问题,或者是想交个朋友一起探讨技术(=。=),都可以加入我们的组织,和我们一起 ~
喜欢这部分内容,就加入我们的QQ群,和大家一起交流技术吧~
QQ群:1032965518
转载自:https://juejin.cn/post/7245583330241658937