先说明一下,React Hooks,目前,还处于,RFC(请求评论)阶段,尚未,正式发布。本文,是,基于,目前,已经,公开的,RFC文档,和,社区讨论,对,React Hooks,进行的,详细解读。正式发布时,API,可能,会有,一些,调整。
虽然,Hooks,还,未正式发布,但是,从,已经,公开的,信息来看,Hooks,将会,彻底,改变,我们,编写,React组件的,方式。它,让,函数组件,也,能拥有,状态,和,生命周期,不再,需要,类组件。
今天,我就,基于,目前,公开的,RFC,和,讨论,对,React Hooks,进行,详细的,解读,从基础,到高级。希望,能帮助大家,提前,了解,这个,重要的,新特性,更好地,准备,迎接,React的未来。
一、为什么,需要,Hooks?
在,介绍,Hooks,之前,先,说说,为什么,React团队,要,开发,Hooks。
在,Hooks,之前,React组件,有,两种,写法:类组件,和,函数组件。
类组件,功能,强大,可以,拥有,状态(state),和,生命周期方法(componentDidMount,componentDidUpdate,等)。但是,类组件,也,有,一些,问题:
- this指向问题:类组件中,经常,会,遇到,this指向的,问题。比如,事件处理器中,this,不是,组件实例。需要,用,bind,或者,箭头函数,来,解决。
- 生命周期方法,逻辑分散:相关的,逻辑,经常,分散在,不同的,生命周期方法中。比如,订阅,在,componentDidMount中,取消订阅,在,componentWillUnmount中。更新逻辑,在,componentDidUpdate中。这,使得,代码,难以,理解,和,维护。
- 逻辑复用困难:在,类组件中,复用,状态逻辑,比较困难。通常,需要,用,高阶组件(HOC),或者,render props,来,实现。但是,这,两种,方式,都会,导致,组件树,嵌套,过深,出现,"嵌套地狱"。
- 类组件,难以,优化:类组件,对于,编译器,的,优化,不太,友好。比如,组件,预编译,内联,等,优化,在,类组件中,比较,难,实现。
函数组件,简洁,优雅,没有,this指向问题,也,容易,优化。但是,函数组件,没有,状态,和,生命周期,只能,用于,简单的,展示组件。
为了,让,函数组件,也,能,拥有,状态,和,生命周期,同时,解决,类组件的,上述,问题,React团队,提出了,Hooks。
Hooks,是,一些,特殊的,函数,它们,可以,让你,在,函数组件中,"钩入",React的,状态,和,生命周期,等,特性。
有了,Hooks,你,就,可以,完全,用,函数组件,来,编写,所有的,React组件,不再,需要,类组件。
二、Hooks的,基本规则
在,介绍,具体的,Hooks,之前,先,说说,Hooks的,两个,基本规则:
1. 只在,最顶层,调用,Hooks
不要,在,循环,条件,嵌套函数,中,调用,Hooks。只在,React函数组件的,最顶层,调用,Hooks。
这是因为,React,是,根据,Hooks,被,调用的,顺序,来,关联,状态,和,Hooks的。如果,在,循环,条件,中,调用,Hooks,可能,会,导致,调用顺序,变化,从而,导致,bug。
2. 只在,React函数组件,中,调用,Hooks
不要,在,普通的,JavaScript函数,中,调用,Hooks。只在,React函数组件,或者,自定义Hooks,中,调用,Hooks。
遵循,这,两个,规则,就能,正确地,使用,Hooks。React团队,还,提供了,一个,ESLint插件,来,自动,检查,是否,遵循了,这,两个,规则。
三、useState:状态钩子
useState,是,最基础的,一个,Hook。它,让,函数组件,也,能,拥有,状态。
useState,接收,一个,初始值,返回,一个,数组。数组的,第一个,元素,是,当前的,状态值。第二个,元素,是,一个,更新状态的,函数。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>你点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>
点击我
</button>
</div>
);
}上面的,代码,用,useState,创建了,一个,名为,count的,状态,初始值,是,0。setCount,是,更新,count的,函数。点击按钮,调用,setCount(count + 1),就,会,把,count,加1,并且,重新,渲染,组件。
和,类组件的,setState,不同,useState的,更新函数,不会,自动,合并,状态。如果你,有,多个,状态,需要,分别,调用,useState。
function Example() {
const [name, setName] = useState('张三');
const [age, setAge] = useState(20);
const [address, setAddress] = useState('北京');
// ...
}当然,你,也,可以,把,相关的,状态,放在,一个,对象里,用,一个,useState。但是,更新的时候,需要,手动,合并。
function Example() {
const [user, setUser] = useState({
name: '张三',
age: 20,
address: '北京',
});
const updateName = (newName) => {
setUser(prev => ({
...prev,
name: newName,
}));
};
// ...
}useState的,更新函数,还,支持,函数式更新。如果,新的,状态,依赖于,之前的,状态,推荐,用,函数式更新。
setCount(prev => prev + 1);这,和,类组件的,setState的,函数式更新,类似。
四、useEffect:副作用钩子
useEffect,是,另一个,非常,重要的,Hook。它,让,函数组件,也,能,拥有,副作用。
副作用,指的是,数据获取,订阅,定时器,手动,修改DOM,等,操作。在,类组件中,这些,操作,通常,放在,componentDidMount,componentDidUpdate,componentWillUnmount,等,生命周期方法中。
useEffect,把,这些,生命周期方法,统一了起来。
useEffect,接收,两个,参数。第一个,参数,是,一个,函数,就是,副作用,的,逻辑。第二个,参数,是,一个,依赖数组,可选。
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `你点击了 ${count} 次`;
});
return (
<div>
<p>你点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>
点击我
</button>
</div>
);
}上面的,代码,用,useEffect,在,组件,渲染后,更新,document.title。每次,组件,重新渲染,useEffect,都会,执行。
这,相当于,类组件的,componentDidMount,和,componentDidUpdate,的,组合。
1. 清理副作用
有些,副作用,需要,清理。比如,订阅,定时器,等。在,类组件中,通常,在,componentDidMount中,订阅,在,componentWillUnmount中,取消订阅。
在,useEffect中,你,可以,在,副作用函数中,返回,一个,清理函数。这个,清理函数,会,在,组件,卸载时,或者,下一次,useEffect,执行前,调用。
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(timer);
};
});上面的,代码,创建了,一个,定时器。返回的,清理函数,会,清除,这个,定时器。
这,相当于,类组件的,componentDidMount,和,componentWillUnmount,的,组合。
2. 依赖数组
默认情况下,useEffect,每次,组件,渲染后,都会,执行。但是,有时候,我们,只希望,在,某些,值,变化时,才,执行,useEffect。
这,就,需要,用到,useEffect的,第二个,参数,依赖数组。
useEffect(() => {
document.title = `你点击了 ${count} 次`;
}, [count]);上面的,代码,只有,在,count,变化时,才,会,执行,useEffect。如果,count,没有,变化,useEffect,就,不会,执行。
如果,依赖数组,是空数组,[],那么,useEffect,只,会,在,组件,挂载时,执行,一次。这,相当于,类组件的,componentDidMount。
useEffect(() => {
// 只在挂载时执行一次
fetchData();
}, []);依赖数组,是,useEffect,中,非常,重要的,一个,概念。正确地,使用,依赖数组,可以,避免,不必要的,副作用执行,提升,性能。
但是,也要,注意,依赖数组,中,的值,一定要,正确。如果,遗漏了,某个,依赖,可能,会,导致,bug。React团队,推荐,用,ESLint插件,来,检查,依赖数组,是否,正确。
五、useContext:上下文钩子
useContext,让,函数组件,也,能,方便地,使用,React Context。
在,Hooks,之前,使用,Context,通常,需要,用,Consumer组件,或者,static contextType。这,两种,方式,都,比较,繁琐。
useContext,让,使用,Context,变得,非常,简单。
useContext,接收,一个,Context对象,返回,当前的,Context值。
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return (
<div>
当前主题:{theme}
</div>
);
}上面的,代码,用,createContext,创建了,一个,ThemeContext。在,App组件中,用,Provider,提供了,Context值,"dark"。在,Toolbar组件中,用,useContext,获取了,当前的,Context值。
当,Context值,变化时,使用,useContext的,组件,会,自动,重新渲染。
useContext,让,跨层级的,数据传递,变得,非常,简单。不再,需要,一层一层地,传递props。
六、useReducer:状态管理钩子
useReducer,是,另一个,用于,状态管理的,Hook。它,类似于,Redux的,reducer。
如果,你的,状态逻辑,比较复杂,包含,多个,子值,或者,下一个,状态,依赖于,之前的,状态,那么,useReducer,可能,比,useState,更,合适。
useReducer,接收,三个,参数:
- reducer:一个,函数,接收,当前的,state,和,action,返回,新的,state。
- initialState:初始状态。
- init:可选,一个,函数,用于,惰性,初始化,状态。
useReducer,返回,一个,数组。第一个,元素,是,当前的,state。第二个,元素,是,dispatch函数,用于,发送,action。
import { useReducer } from 'react';
const initialState = { count: 0 };
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() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}上面的,代码,用,useReducer,实现了,一个,计数器。reducer函数,根据,action的,type,来,更新,状态。dispatch函数,用于,发送,action。
useReducer,和,useState,类似,但是,更,适合,复杂的,状态逻辑。而且,useReducer,还,方便,测试。因为,reducer,是,一个,纯函数,可以,独立,测试。
另外,useReducer,和,useContext,结合,可以,实现,简单的,状态管理,替代,Redux,在,一些,中小规模的,应用中。
七、useMemo:记忆化钩子
useMemo,用于,记忆化,计算结果。它,会,记住,上一次的,计算结果,只有,当,依赖,变化时,才,重新,计算。
这,对于,昂贵的,计算,非常,有用,可以,避免,不必要的,重复计算,提升,性能。
useMemo,接收,两个,参数:
- factory:一个,函数,返回,要,记忆化的,值。
- deps:依赖数组。
useMemo,返回,记忆化的,值。
import { useMemo } from 'react';
function Example({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}上面的,代码,用,useMemo,记忆化了,过滤后的,items。只有,当,items,或者,filter,变化时,才,重新,过滤。否则,直接,返回,上一次的,结果。
这,在,items,数量,很大,过滤,很耗时的,情况下,能,显著,提升,性能。
需要,注意的是,useMemo,只是,一种,性能优化的,手段。不要,过度,依赖,它。React,可能,会,在,某些,情况下,丢弃,记忆化的,值,重新,计算。所以,你的,代码,应该,在,没有,useMemo的,情况下,也,能,正确,运行。
八、useRef:引用钩子
useRef,用于,创建,一个,可变的,引用对象。这个,引用对象,的,current属性,指向,你,传入的,初始值。
useRef,和,useState,类似,但是,有,一个,重要的,区别:修改,ref的,current属性,不会,触发,组件,重新渲染。
useRef,有,两个,主要的,用途:
1. 访问DOM元素
这,是,useRef,最常见的,用途。你,可以,用,useRef,创建,一个,ref,然后,把它,附加到,一个,DOM元素上。这样,你,就,可以,通过,ref.current,访问,这个,DOM元素。
import { useRef, useEffect } from 'react';
function Example() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<input ref={inputRef} type="text" />
);
}上面的,代码,用,useRef,创建了,一个,inputRef,附加到,input元素上。在,组件,挂载后,调用,inputRef.current.focus(),让,input,自动,获取焦点。
这,相当于,类组件的,React.createRef。
2. 保存,可变值
useRef,还,可以,用于,保存,任何,可变值。因为,修改,ref的,current属性,不会,触发,重新渲染,所以,它,适合,保存,一些,不需要,触发,渲染的,值。
比如,定时器ID,上一次的,props,等。
import { useRef, useEffect } from 'react';
function Example() {
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(timerRef.current);
};
}, []);
// ...
}上面的,代码,用,useRef,保存了,定时器ID。在,清理函数中,清除,定时器。
九、自定义Hooks
除了,React,内置的,Hooks,你,还,可以,创建,自己的,自定义Hooks。
自定义Hooks,就是,一个,普通的,JavaScript函数,但是,它的,名字,以,"use",开头,并且,它,调用了,其他的,Hooks。
自定义Hooks,让,你,可以,把,组件,逻辑,提取出来,复用。
比如,我们,可以,创建,一个,useWindowSize的,自定义Hook,用于,获取,窗口的,大小。
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return size;
}然后,在,组件中,使用,这个,自定义Hook。
function MyComponent() {
const size = useWindowSize();
return (
<div>
窗口宽度:{size.width},窗口高度:{size.height}
</div>
);
}自定义Hooks,是,Hooks,中,最,强大的,部分之一。它,让,状态逻辑的,复用,变得,非常,简单。不再,需要,高阶组件,或者,render props,也就,不会,出现,"嵌套地狱"。
你,可以,把,任何,可复用的,组件逻辑,提取成,自定义Hooks。比如,数据获取,表单处理,订阅,定时器,等。
十、Hooks的,注意事项,和,最佳实践
最后,说说,Hooks的,一些,注意事项,和,最佳实践。
1. 遵循,Hooks的,基本规则
只在,最顶层,调用,Hooks。只在,React函数组件,或者,自定义Hooks,中,调用,Hooks。
推荐,使用,React团队,提供的,ESLint插件,来,自动,检查。
2. 正确,使用,依赖数组
useEffect,useMemo,useCallback,等,Hooks,都,有,依赖数组。一定要,正确地,填写,依赖数组。不要,遗漏,依赖,也,不要,添加,不必要的,依赖。
如果,遗漏了,依赖,可能,会,导致,闭包,捕获了,旧的值,从而,导致,bug。
3. 不要,过度,使用,useMemo,和,useCallback
useMemo,和,useCallback,是,性能优化的,手段。但是,它们,本身,也,有,开销。不要,在,所有的,地方,都,用,useMemo,和,useCallback。只,在,确实,有,性能问题的,地方,使用。
而且,React,未来,可能,会,自动,进行,一些,记忆化,优化。到时候,手动的,useMemo,和,useCallback,可能,就,不需要了。
4. 自定义Hooks,命名,以,"use",开头
自定义Hooks的,名字,一定要,以,"use",开头。这样,React的,ESLint插件,才能,识别,它,是,一个,Hook,并且,检查,它,是否,遵循了,Hooks的,规则。
5. 渐进式,采用,Hooks
Hooks,和,类组件,可以,共存。你,不需要,一下子,把,所有的,类组件,都,改成,函数组件。你,可以,在,新的,组件中,使用,Hooks。然后,逐步地,把,旧的,类组件,重构,成,函数组件。
React团队,也,承诺,会,继续,支持,类组件。所以,你,不用担心,类组件,会,被,废弃。
十一、写在最后
以上,就是,基于,目前,公开的,RFC,和,讨论,对,React Hooks,的,详细解读。
虽然,Hooks,还,未正式发布,API,可能,还,会,有,一些,调整。但是,整体的,设计,和,思想,应该,不会,有,太大的,变化。
Hooks,是,React,自,诞生以来,最重要的,一次,变革。它,彻底,改变了,我们,编写,React组件的,方式。它,让,函数组件,拥有了,类组件的,所有,能力,同时,解决了,类组件的,很多,问题。
我,相信,在,不久的,将来,Hooks,会,成为,React组件的,主流,写法。类组件,会,逐渐,被,函数组件,取代。
所以,提前,了解,和,学习,Hooks,是,非常,有必要的。希望,这篇文章,能,帮助大家,更好地,理解,Hooks,更好地,准备,迎接,React的未来。
当然,因为,Hooks,还,未正式发布,本文,中的,一些,内容,可能,会,和,正式版,有,出入。如果,有,错误,或者,不准确的,地方,欢迎,大家,在,评论区,指正。
也,欢迎,大家,在,评论区,分享,你,对,React Hooks,的,看法,和,期待。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录