1.动机
Hook 这个单词的意思是"钩子"。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。
你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于Eslint识别。你要使用 xxx 功能,钩子就命名为 usexxx。
在 Hooks 出现之前,类组件和函数组件的分工一般是这样的:
- 类组件提供了完整的状态管理和生命周期控制,通常用来承接复杂的业务逻辑,被称为 “聪明组件”
- 函数组件则是纯粹的从数据到视图的映射,对状态毫无感知,因此通常被称为 “傻瓜组件”
函数组件更轻量,但只能用来视图展示。那么 Hooks 的出现又是为了解决什么问题呢?我们可以试图总结一下类组件颇具代表性的痛点
- 令人头疼的 this 管理,容易引入难以追踪的 Bug
- 生命周期的划分并不符合 “内聚性” 原则,例如 setInterval 和 clearInterval 这种具有强关联的逻辑被拆分在不同的生命周期方法中
- 组件复用(数据共享或功能复用)的困局,从早期的 Mixin,到高阶组件(HOC),再到 Render Props,始终没有一个清晰直观又便于维护的组件复用方案
React 里,组件是代码复用的基本单元,基于组合的组件复用机制相当优雅。而对于更细粒度的逻辑(状态逻辑、行为逻辑等),复用起来却不那么容易 ,一般有以下几种模式来解决我们状态逻辑复用问题,这里就引出了组件复用发展史👇
Mixin
Mixin模式就是一些提供能够被一个或者一组子类简单继承功能的类,意在重用其功能
缺点:Mixins 引入了隐式的依赖关系,并且会导致滚雪球式的复杂性,且ES6的class支持继承但不支持Mixin,所以,React v0.13.0 放弃了 Mixin(继承),转而走向HOC(组合)
HOC
高阶函数 : 把函数作为参数传入,返回一个新的函数,这样的函数称为高阶函数
创建一个函数, 该函数接收一个组件作为输入。除了组件, 还可以传递其他的参数, 基于该组件返回了一个不同的组件
缺点:HOC 对于使用者来说是一个黑盒,还需要去看他们的实现。组件多层嵌套问题,相同命名的props会覆盖老属性,不清楚props来源与哪个高阶组件,难以溯源
Render Props
A render prop is a function prop that a component uses to know what to render
Render Props 的核心思想是,通过一个函数将class组件的state作为props传递给纯函数组件
Render Props就是一个函数,做为一个属性被赋值给父组件,使得父组件可以根据该属性去动态的决定如何渲染子组件。也就是数据给你,其他的你自己来。
缺点:无法利用SCU这个生命周期,来实现渲染性能的优化。React的优化是基于 shouldComponentUpdate 的,该生命周期默认返回true,所以一旦prop或state有任何变化,都会引起重新render。
react在每个组件生命周期更新的时候都会调用一个shouldComponentUpdate(nextProps, nextState)函数。它的职责就是返回true或false,true表示需要更新,false表示不需要,默认返回为true,即便你没有显示地定义 shouldComponentUpdate 函数,造成资源的浪费。
2.规则
2.1 只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
2.2 只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
- ✅ 在 React 的函数组件中调用 Hook
- ✅ 在自定义 Hook 中调用其他 Hook
遵循此规则,确保组件的状态逻辑在代码中清晰可见。
3.常用hooks 概览
3.1 useState
允许在function component中拥有state的hooks
const [state, setState] = useState(initialValue);
其中 state 就是一个状态变量,setState 是一个用于修改状态的 Setter 函数,而 initialValue 则是状态的初始值
状态值为什么不是最新的?
官方相关 issue:Why am I seeing stale props or state inside my function?
经典计数器例子:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
实现了上面这个计数器后(也可以直接通过这个 Sandbox 进行体验),按如下步骤操作:1)点击 Click me 按钮,把数字增加到 3;2)点击 Show alert 按钮;3)在 setTimeout 触发之前点击 Click me,把数字增加到 5。
结果是 Alert 显示 3!
Capture Value的概念:
- 每次渲染相互独立,因此每次渲染时组件中的状态、事件处理函数等等都是独立的,或者说只属于所在的那一次渲染。也就是说,每个函数中的 state 变量只是一个简单的常量,每次渲染时从钩子中获取到的常量,并没有附着数据绑定之类的操作。
这道理就像,你翻开十年前的日记本,虽然是现在翻开的,但记录的仍然是十年前的时光。或者说,日记本 Capture 了那一段美好的回忆。
3.2 useReducer
在设计 Reducer 应当注意按照纯函数来设计,一个函数在程序执行的过程中只会根据输入参数给出运算结果,我们把这类函数称为“纯函数”。纯函数由于不依赖外部变量,使得给定函数输入其返回结果永远不变
比如整数的加法函数,它接收两个整数值并返回一个整数值,对于给定的两个整数值,它的返回值永远是相同的整数值
可以把 Reducer 想想成一个漏斗,它接收先前的 state 和 action,输出新的 state 来更新 store:
由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象
常见错误 #1:指向同一对象的新变量
定义一个新变量不会创建一个新的实际对象,它只创建另一个引用到同一个对象。这个错误的示例如下:
function updateNestedState(state, action) { let nestedState = state.nestedState // 错误: 这将导致直接修改已经存在的对象引用-不要这么做! nestedState.nestedField = action.data return { ...state, nestedState } }
function updateNestedState(state, action) {
let nestedState = state.nestedState
// 错误: 这将导致直接修改已经存在的对象引用-不要这么做!
nestedState.nestedField = action.data
return {
...state,
nestedState
}
}
这个函数正确返回了顶层状态对象的浅复制,但是变量 nestedState 依然指向已经存在的对象,这个状态被直接修改了。
常见错误 #2:仅仅在一个层级上做浅复制
这个错误的另外一个常见版本的如下所示:
function updateNestedState(state, action) {
// 问题: 这仅仅做了浅复制!
let newState = { ...state }
// 错误: nestedState 仍然是同一个对象!
newState.nestedState.nestedField = action.data
return newState
}
做一个顶层的浅复制是不够的 - nestedState 对象也应该被复制。
正确方法:复制嵌套数据的所有层级
更新 state.first.second[someId].fourth 的示例大概如下所示:
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
了解reducer也就不难了解useReducer
const [state, dispatch] = useReducer(reducer, initState);
useReducer接收两个参数:
第一个参数:reducer函数,Reducer 函数的形式是(state, action) => newState。第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)
// 官方 useReducer Demo
// 第二个参数:应用的初始化
const initialState = {count: 0};
// 第一个参数:state的reducer处理函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
// 返回值:最新的state和dispatch函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
// useReducer会根据dispatch的action,返回最终的state,并触发rerender
Count: {state.count}
// dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
1.更清晰的表达了用户的意图,表现和业务分离。
2.所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码,比如在其他函数中也需要触发状态,只需要dispatch({ type: '*' })**。
3.useReducer可以让我们将what和how分开。比如点击了增加按钮,我们要做的就是发起增加操作dispatch({ type: 'increment' }),点击减少按钮就发起减少操作dispatch({ type: 'decrement' }),所有和how相关的代码都在reducer中维护,组件中只需要思考What,让我们的代码可以像用户的行为一样,更加清晰。
4.我们在前文提过Reducer其实一个UI无关的纯函数,useReducer的方案是的我们更容易构建自动化测试用例。
3.3 useContext
简单来说Context的作用就是对它所包含的组件树提供全局共享数据的一种技术
// 第一步:创建需要共享的context
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 第二步:使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// Toolbar 组件并不需要透传 ThemeContext
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
// 第三步:使用共享 Context
const theme = useContext('ThemeContext');
render() {
return <Button theme={theme} />;
}
}
关于Context还有一个比较重要的点是:当Context Provider的value发生变化是,他的所有子级消费者都会rerender。
3.4 useCallback
首先先了解一下记忆化缓存(Memoization)
Memoization,一般称为记忆化缓存,它背后的思想很简单:假如我们有一个计算量很大的纯函数(给定相同的输入,一定会得到相同的输出),那么我们在第一次遇到特定输入的时候,把它的输出结果 “记”(缓存)下来,那么下次碰到同样的输出,只需要从缓存里面拿出来直接返回就可以了,省去了计算的过程。
实际上,除了节省不必要的计算、从而提高程序性能之外,Memoization 还有一个用途:用了保证返回值的引用相等。
我们先通过一段简单的求平方根的函数,熟悉一下 Memoization 的原理。首先是一个没有缓存的版本:
function sqrt(arg) { return { result: Math.sqrt(arg) }; }
特地返回了一个对象来记录结果,我们后面会和 Memoized 的版本进行对比分析。然后是加了缓存的版本:
function memoizedSqrt(arg) {
// 如果 cache 不存在,则初始化一个空对象
if (!memoizedSqrt.cache) {
memoizedSqrt.cache = {};
}
// 如果 cache 没有命中,则先计算,再存入 cache,然后返回结果
if (!memoizedSqrt.cache[arg]) {
return memoizedSqrt.cache[arg] = { result: Math.sqrt(arg) };
}
// 直接返回 cache 内的结果,无需计算
return memoizedSqrt.cache[arg];
}
然后我们尝试调用这两个函数,就会发现一些明显的区别:
sqrt(9) // { result: 3 }
sqrt(9) === sqrt(9) // false
Object.is(sqrt(9), sqrt(9)) // false
memoizedSqrt(9) // { result: 3 }
memoizedSqrt(9) === memoizedSqrt(9) // true
Object.is(memoizedSqrt(9), memoizedSqrt(9)) // true
普通的 sqrt 每次返回的结果的引用都不相同(或者说是一个全新的对象),而 memoizedSqrt 则能返回完全相同的对象。因此在 React 中,通过 Memoization 可以确保多次渲染中的 Prop 或者状态的引用相等,从而能够避免不必要的重渲染或者副作用执行。
让我们来总结一下记忆化缓存(Memoization)的两个使用场景:
- 通过缓存计算结果,节省费时的计算
- 保证相同输入下返回值的引用相等[指向同一块内存地址]
为了解决函数在多次渲染中的引用相等(Referential Equality)问题,React 引入了一个重要的 Hook—— useCallback。官方文档介绍的使用方法如下:
const memoizedCallback = useCallback(callback, deps);
第一个参数 callback 就是需要记忆的函数,第二个参数就是大家熟悉的 deps 参数,同样也是一个依赖数组(有时候也被称为输入 inputs)。在 Memoization 的上下文中,这个 deps 的作用相当于缓存中的键(Key),如果键没有改变,那么就直接返回缓存中的函数,并且确保是引用相同的函数。
在大多数情况下,我们都是传入空数组 [ ] 作为 deps 参数,这样 useCallback 返回的就始终是同一个函数,永远不会更新。
3.5 useMemo
了解useCallback也就不难了解useMemo
我们知道 useCallback 有个好友叫 useMemo。还记得我们之前总结了 Memoization 的两大场景吗?useCallback 主要是为了解决函数的” 引用相等 “问题,而 useMemo 则是一个” 全能型选手 “,能够同时胜任引用相等和节约计算的任务。
实际上,useMemo 的功能是 useCallback 的超集。与 useCallback 只能缓存函数相比,useMemo 可以缓存任何类型的值(当然也包括函数)。useMemo 的使用方法如下:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
其中第一个参数是一个函数,这个函数返回值的返回值(也就是上面 computeExpensiveValue 的结果)将返回给 memoizedValue 。因此以下两个钩子的使用是完全等价的:
useCallback(fn, deps);
useMemo(() => fn, deps);
3.6 useEffect
官方文档介绍 useEffect 的使用方法如下:
useEffect(effectFn, deps)
effectFn 是一个执行某些可能具有副作用的 Effect 函数(例如数据获取、设置 / 销毁定时器等),它可以返回一个清理函数(Cleanup),例如大家所熟悉的 setInterval 和 clearInterval :
useEffect(() => {
const intervalId = setInterval(doSomething(), 1000);
return () => clearInterval(intervalId);
});
可以看到,我们在 Effect 函数体内通过 setInterval 启动了一个定时器,随后又返回了一个 Cleanup 函数,用于销毁刚刚创建的定时器。
useEffect 钩子与之前类组件的生命周期相比,有两个显著的特点:
- 将初次渲染(componentDidMount)、重渲染(componentDidUpdate)和销毁(componentDidUnmount)三个阶段的逻辑用一个统一的 API 去解决
- 把相关的逻辑都放到一个 Effect 里面(例如 setInterval 和 clearInterval),更突出逻辑的内聚性
再来看看 useEffect 的第二个参数:deps (依赖数组)。React 会在每次渲染后都运行 Effect。而依赖数组就是用来控制是否应该触发 Effect,从而能够减少不必要的计算,从而优化了性能。具体而言,只要依赖数组中的每一项与上一次渲染相比都没有改变,那么就跳过本次 Effect 的执行。
在最极端的情况下,我们可以指定 deps 为空数组 [ ] ,这样可以确保 Effect 只会在组件初次渲染后执行。
不要撒谎:关于 deps 的那些事
useEffect (包括其他类似的 useCallback 和 useMemo 等)都有个依赖数组(deps)参数,这个参数比较有趣的一点是:指定依赖的决定权完全在你手里。你当然可以选择 “撒谎”,不管什么情况都给一个空的 deps 数组,仿佛在说 “这个 Effect 函数什么依赖都没有,相信我”。
然而,这种有点偷懒的做法显然会引来各种 Bug。一般来说,所使用到的 prop 或者 state 都应该被添加到 deps 数组里面去。
注意Effect 无限循环风险:
依赖数组在判断元素是否发生改变时使用了 Object.is 进行比较,因此当 deps 中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而每次都会触发 Effect,失去了 deps 本身的意义。
3.7 useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。useRef 在 react hook 中的常见使用场景:
-
如果你将 ref 对象以形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
-
如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西。
例:Capture Value概念中的例子,如果想拿到实时数据,可以用useRef来实现。
const latestCount = useRef(count);
latestCount.current = count;
console.log('实时数据', latestCount.current)
和createRef的区别?
useRef在createRef的基础上实现了hook中的dom状态持久化,createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
4.其他hooks
4.1 useLayoutEffect
将 Effect 推迟到渲染完成之后执行是出于性能的考虑,如果你想在非服务端渲染情况下,渲染之前执行某些逻辑(不惜牺牲渲染性能),那么可使用 useLayoutEffect 钩子,使用方法与 useEffect 完全一致,只是执行的时机不同。
渲染之前:在所有的 DOM 变更之后,在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新。
4.2 useDebugValue
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签及其调试值,调试时需要的一个hooks。
useDebugValue(value)
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。不推荐向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
5.一些比较好用的hooks库
参考:
国外大佬 Dan的博客 overreacted.io/
前端不易,走过路过的兄弟姐妹,请动动小手,点个赞或关注给个鼓励,谢谢各位!