Hooks
hooks的作用
钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。
由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。
useState()
:保存状态useContext()
:保存上下文useRef()
:保存引用- ……
上面这些钩子,都是引入某种特定的副效应,而 useEffect()
是通用的副效应钩子 。找不到对应的钩子时,就可以用它。其实,从名字也可以看出来,它跟副效应(side effect)直接相关。
useEffect
useEffect()
本身是一个函数,由 React 框架提供,在函数组件内部调用即可。
例如:
1
2
3
4
5
6
7
8 import React, { useEffect } from 'react';
function Welcome(props) {
useEffect(() => {
document.title = '加载完成';
});
return <h1>Hello, {props.name}</h1>;
}
上面例子中,useEffect()
的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。
useEffect()
的作用就是指定一个副效应函数,组件每渲染一次,该函数就执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。
useEffect指定依赖项
有时候,我们不希望useEffect()
每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会执行。
如果上例改为
1 | function Welcome(props) { |
便只会在组件首次挂载时执行函数,相当于class component中的componentDidMount
useEffect() 的用途
只要是副效应,都可以使用useEffect()
引入。它的常见用途有下面几种。
- 获取数据(data fetching)
- 事件监听或订阅(setting up a subscription)
- 改变 DOM(changing the DOM)
- 输出日志(logging)
useEffect返回值
副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。
useEffect()
允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()
就不用返回任何值。
1
2
3
4
5
6 useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);
上面例子中,useEffect()
在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。
实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。
useEffect常见问题
使用useEffect应该在依赖的变量中指明定义的函数会用到哪些state或者props
effect 可能会使用一些频繁变化的值。可能会忽略依赖列表中 state,但这通常会引起 Bug:
1 | function Counter() { |
传入空的依赖数组 []
,意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval
的回调中,count
的值不会发生变化。因为当 effect 执行时,react会创建一个闭包,并将 count
的值被保存在该闭包当中,且初值为 0
。每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count
永远不会超过 1。
指定 [count]
作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval
在被清除前都会调用一次(等于说直接使用 setTimeout
就行了)。要解决这个问题,可以使用类似setState函数式更新的操作,不依赖外部count变量,它允许我们指定 state 该如何改变而不用引用state或者props
1 | function Counter() { |
useLayoutEffect vs useEffect
useLayoutEffect 和 useEffect 的传参一致,但有以下区别
执行时机不同
useLayoutEffect 的入参函数会在 react 更新 DOM 树后同步调用(其实就是dom树更新,浏览器重绘前会执行useLayoutEffect里的函数,此时比如去拿 style 或者 dom 尺寸,都是游览器即将渲染的那一次的尺寸,而不是现在页面上展示的尺寸),useEffect 为异步调用(useEffect 则肯定是在游览器渲染完后才执行),所以应该尽可能使用标准的useEffect
以避免阻塞视觉更新useLayoutEffect 在 development 模式下 SSR 会有警告⚠️
通常情况下 useLayoutEffect 会用在做动效和记录 layout 的一些特殊场景(比如防止渲染闪烁,在渲染前再给你个机会去改 DOM)。一般不需要使用 useLayoutEffect。
React 组件是一个树形结构,且每个节点都是懒计算的(类似于 Thunk 的概念)。当一个节点不需要重新计算(重绘)时,他的子树都不会计算(重绘)。所以我们做性能优化的目标,就是在尽量离根节点近的位置,拦截不必要的节点重算,从而减少重绘的计算量。
useRef
当需要存放一个数据,需要无论在哪里都取到最新状态时,需要使用 useRef,ref 是一种可变数据。
如上文提到的代码段中,因为存在闭包问题,count永远会为1,log永远为0;
可以引入useRef
解决这个问题
1 | function Counter() { |
useRef、useState如何决策用哪种来维护状态
useRef 生成的可变对象,因为使用起来就跟普通对象一样,赋值时候 React 是无法感知到值变更的,所以也不会触发组件重绘。利用其与 useState 的区别,我们一般这样区分使用:
- 维护与 UI 相关的状态,使用 useState
确保更改时刷新 UI
- 值更新不需要触发重绘时,使用 useRef
- 不需要变更的数据、函数,使用 useState
比如,需要声明一个不可变的值时,可以这样:
1 const [immutable] = useState(someState);
不返回变更入口函数。useRef 虽然可以借助 TypeScript 达到语法检测上的 immutable,但实际还是 mutable 的。
useContext
context简介:
React.createContext
新建context
1 | const MyContext = React.createContext(defaultValue); |
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider
中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined
传递给 Provider 的 value 时,消费组件的 defaultValue
不会生效。
Context.Provider
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
useContext使用
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染。
1 | import React, { useContext, useState } from "react"; |
useMemo
useMemo 主要有两个作用:
- 缓存一些耗时计算,通过声明计算结果的依赖是否变更,来重用上次计算结果
- 保证引用不变,针对下游使用 React.memo 的组件进行性能优化(useCallback 也有一样的作用)
比如,计算耗时的 fibonacci 数列,就可以用 useMemo 来优化在 n 不变的情况下,二次渲染的性能
useMemo(() => { return fibonacci(props.n) }, [props.n]);
useCallback
useCallback 是简化版的 useMemo,方便缓存函数引用。下面的代码是等价的:
1 | const memoCallback = useCallback((...args) => { |
1 | const memoCallback = useMemo(() => (...args) => { |
在没有遇到性能问题时,不要使用 useCallback 和 useMemo,性能优化先交给框架处理解决。手工的微优化在没有对框架和业务场景有深入了解时,可能出现性能劣化。
关于如何减少 useCallback 看 第二天