前言
之前写了一篇重新认识React组件的文章,里面提到了关于类组件 和函数式组件 的优劣势,类组件提供了强大的API能力和状态管理,但在一些情况下显得太重 ,而函数式组件轻量并且灵活,但由于能力有限又处于尴尬地步,由此,React Hooks
应用而生,给函数式组件插上的翅膀,有了更广阔的用武之地,本文就来梳理关于React Hooks
的前世今生。
React Hook解决了什么问题
万事万物存在因果轮回,Hooks
诞生的动机是什么?它又解决了什么问题呢?
逻辑拆分
在类组件中,我们平时最常写的代码风格是业务逻辑
与生命周期函数
混合在一起,尤其是在大型的业务中,它会存在以下问题:
- 一个功能逻辑可能出现在多个生命周期函数中,如定时器的创建与销毁、窗口事件的创建与销毁
- 一个生命周期函数中存在多种业务逻辑,如获取页面数据、获取路由信息、监听DOM的事件
来看看下面的例子:
js
class ListComponent extends Component {
state = {
list: [],
};
fetchData = () => {
// 获取数据
// ...
};
resetData = () => {
this.setState({ list: [] });
}
componentDidMount() {
// 获取路由参数并进行格式化
// ...
// 获取数据
this.fetchData();
// 创建 监听窗口 事件
window.addEventListener('resize', () => {})
}
componentDidUpdate() {
// 获取某个props参数进行更新DOM
}
componentWillUnmount() {
// 销毁事件
window.removeEventListener('resize', () => {})
}
render() {
return (
<div>
<button onClick={this.resetData}>重置数据</button>
</div>
);
}
}
写过Vue2
的同学看上面代码也深有同感,在一个生命周期里面处理众多零碎的业务逻辑,造成函数体积过大,给阅读和维护者带来很多麻烦。重要的是,这些看似毫无关联的逻辑,它们被打碎
之后放到不同的生命周期函数中去处理,比如监听
和销毁
窗口事件以及示例中没有提到的定时器
的创建与销毁,它们本质上做的是同一件事情,但却被分配到不同的钩子中去处理,显然在设计上并不属于最佳实践吧。
既然这样,那使用Hooks
可以怎么写呢?
js
// 自定义 Hook 1:用于数据获取
function useDataFetching(url) {
// ...
}
// 自定义 Hook 2:用于数据筛选
function useDataFilter(data, filter) {
// ...
}
// 自定义 Hook 3: 用于处理页面副作用
function usePageEffect() {
// ...
useEffect( () => {
// ...
// 创建 监听窗口 事件
window.addEventListener('resize', () => {})
return () => {
// 销毁事件
window.removeEventListener('resize', () => {})
}
})
}
// 组件:使用数据获取和筛选 Hook
function MyComponent() {
const { data, isLoading } = useDataFetching("https://api.example.com/data");
const filteredData = useDataFilter(data, "filterCriteria");
// ...
usePageEffect()
return (
<div/>
)
}
在Hooks
的加持下,我们有能力把这些复杂的业务逻辑进行分治
,通过不同的函数组件或者自定义Hooks
进行维护,形成有专门处理数据获取的函数组件、有专门处理数据转换或过滤的函数组件、有专门处理副作用的函数组件等,这就是分
;通过合理的方式把这些函数组件聚合起来,形成搭积木过程,这就是治
。
逻辑复用
在类组件中,我们在复杂的业务逻辑中,复杂的逻辑会让组件变的难以维护,通常借助诸如HOC高阶组件
和Render Props渲染属性
进行封装公共逻辑,但它们容易形成嵌套地狱
等问题
为什么这么说呢?
假设你有一个需要进行身份验证的组件 AuthenticatedComponent
,它需要检查用户是否已登录,如果未登录,则重定向到登录页面。你需要使用一个HOC
来处理身份验证逻辑,如下所示:
jsx
function withAuth(Component) {
class WithAuth extends React.Component {
componentDidMount() {
if (!userIsLoggedIn()) {
this.props.history.push('/login');
}
}
render() {
return <Component {...this.props} />;
}
}
return withRouter(WithAuth);
}
现在,你需要有另一个HOC
,用于处理主题切换的逻辑,如下所示:
jsx
function withTheme(Component) {
class WithTheme extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: 'light',
};
}
toggleTheme() {
this.setState((prevState) => ({
theme: prevState.theme === 'light' ? 'dark' : 'light',
}));
}
render() {
return (
<Component
{...this.props}
theme={this.state.theme}
toggleTheme={this.toggleTheme}
/>
);
}
}
return WithTheme;
}
现在,你可能想要将这两个HOC应用于你的组件 MyComponent,以进行身份验证和主题切换。你的代码可能如下所示:
jsx
const xxxComponent = withAuth(withTheme(MyComponent));
这柯里化看起挺优雅的呀,没错!虽然看起来很方便,但如果你有更多的HOC
需要应用,代码可能会变得混乱,难以阅读和维护,这在React
设计者看来并不是最佳的。
自从有了 Hooks
以后,我们可以通过自定义 Hooks
,达到既不破坏组件结构、又能够实现逻辑复用的效果,同时,函数一等公民的身份也让单元测试变的简单。还是上面的例子:
jsx
// 自定义 Hook 1:用于数据获取
function useDataFetching(url) {
// ...
}
// 自定义 Hook 2:用于数据筛选
function useDataFilter(data, filter) {
// ...
}
// 自定义 Hook 3: 用于处理页面副作用
function usePageEffect() {
// ...
useEffect( () => {
// ...
// 创建 监听窗口 事件
window.addEventListener('resize', () => {})
return () => {
// 销毁事件
window.removeEventListener('resize', () => {})
}
})
}
this指向问题
this
的指向问题一直在JavaScript
中都是谜一样的存在,也让众多的初级开发者头疼,来看看下面类组件代码:
jsx
class TransactionTable extends Component {
state = {
list: [],
};
fetchData = () => {
transactionList().then((response) => {
const list = response.data.data.items.slice(0, 13);
if (this._isMounted) {
this.setState({ list });
}
});
};
resetData() {
this.setState({ list: [] });
}
componentDidMount() {
this.fetchData();
// 创建 监听窗口 事件
window.addEventListener('resize', () => {})
}
componentWillUnmount() {
// 销毁事件
window.removeEventListener('resize', () => {})
}
render() {
return (
<div>
<button onClick={this.resetData}>重置数据</button>
</div>
);
}
}
export default TransactionTable;
执行上面的代码,点击重置数据
调用resetData
会报错

是因为resetData
为普通函数,它存在自己的函数作用域,我们需要在编码借助bind
来修正this
指向类组件实例或者通过箭头函数
定义resetData
,让它成为词法作用域。不管什么招数,这些都是从实践的层面约束来解决设计层面的问题。
明明我是按照生命周期函数的写法来写的呀,在这个层面上开发者存在一定的心智负担,告诉自己必须要留意this
指向问题。
好,现在有Hooks
了,我们可以将上面的类组件改为函数组件,就不用关心this
问题。
React Hooks的注意点
既然你说Hooks
有这么多好处,那使用Hooks
就意味着可以高枕无忧了吗?并不是,凡事有利有弊,我们需要清楚它的局限性:
Hooks
暂时还不能完全代替类组件:比如getSnapshotBeforeUpdate
、componentDidCatch
这些生命周期,目前都还是强依赖类组件的。Hooks
在使用层面有着严格的规则:只能在React
函数中调用Hook
;不要在循环、条件或嵌套函数中调用Hook
。
好家伙,你说类组件有心智负担,这玩意不也一样有吗?那不是自相矛盾?
这是一个成本与收益之前的博弈问题,而针对这个问题,React
也给我们提供了eslint-plugin-react-hooks
来帮助开发者减少这类问题引发的困难或bug
,在项目配置中引入即可。
bash
npm install eslint-plugin-react-hooks --save-dev
json
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
总结
React
为了增强函数组件的能力, React Hooks
应运而生,为我们解决了从前开发中的一些痛点,如类组件的实例 this
指向、业务逻辑拆分和复用等,但也对开发者的水平提出了更高的要求,在使用Hooks
的过程中需要注意遵守相关规则,好在React
提供了工具帮助开发者减少这类负担。
最后,关于为什么React
会提出上面的规则进行约束呢?下一篇从源码的角度来分析。