和React Hook相比class component到底输在了哪里?
背景介绍
这是设计模式系列的第十二节,学习的是里设计模式中Hooks模式内容,由于是资料是英文版,所以我的学习笔记就带有翻译的性质,但并不是翻译,记录的是自己的学习过程和理解。
关于设计模式前九节的内容,在文末会有直达链接。
写在前面
React 16.8 引入了一个叫Hooks新特性,它允许我们在Function组件中使用state和生命周期,这简直是革命性的,以前我们只能在class组件中使用state和生命周期,Function组件只能是stateless组件。
虽然确切地说Hooks并不算是设计模式,但是它在程序设计中确实很重要的,并且很多设计模式可以被Hooks替代。所以我们还是很有必要学习Hooks的。
Class Component
相信大家都还记得在Hooks出现之前,我们在class中使用state和生命周期的场景,大概就是下面这个样子:
class MyComponent extends React.Component {
/* Adding state and binding custom methods */
constructor() {
super()
this.state = { ... }
this.customMethodOne = this.customMethodOne.bind(this)
this.customMethodTwo = this.customMethodTwo.bind(this)
}
/* Lifecycle Methods */
componentDidMount() { ...}
componentWillUnmount() { ... }
/* Custom methods */
customMethodOne() { ... }
customMethodTwo() { ... }
render() { return { ... }}
}
通常一个class Component 会在构造函数里声明state,并在诸如componentDidMount和componentWillUnmount的生命周期方法中处理一些异步副作用逻辑,也会在class中自定义一些其他方法。
当然React在引入Hooks之后,使用class Component的比例已经大大降低了。
下面让我们来总结下使用class Component面临的一些问题。
理解class Component的缺点
正因为在Hooks之前,要想使用state和生命周期我们只能选择class Component;在需求变更时,有时我们不得不把Function Component重构成Class component,从而实现新的需求,这往往是一个痛苦的过程。
比如,有这样一个Function Component,只是包含一个简单的div的Button组件:
function Button() {
return <div className="btn">disabled</div>;
}
由于需求变更,需要在用户点击Button时,文案变成enabled,并且需要添加一些额外的样式。
因此,我们必须要知道Button最新的状态,disabled还是enabled,为此我们就不得不完全重构这个组件,首先把这个组件改造成class Component,只是为了记录按钮的状态。
export default class Button extends React.Component {
constructor() {
super();
this.state = { enabled: false };
}
render() {
const { enabled } = this.state;
const btnText = enabled ? "enabled" : "disabled";
return (
<div
className={`btn enabled-${enabled}`}
onClick={() => this.setState({ enabled: !enabled })}
>
{btnText}
</div>
);
}
}
当然,这样也实现了需求。在这简单的示例中,重构的代码也不大。但是在实际的项目中,组件的代码行数可能有很多,逻辑可能很复杂,这无疑会增加的重构的难度。
在重构过程中,我们不得不十分小心,生怕修改了原有的逻辑,同时还必须要理解es2015 class的工作原理:
- 为什么我们要使用bind?
- 构造函数是干啥的?
- this关键词从哪里来,在哪里可以用?
并且要保证在重构过程中不会意外地修改数据流,这些都是十分复杂和困难的。
结构调整
在多个组件中复用代码,常用的方法是高阶组件和传递render函数这两个设计模式,当然这两个模式是很有效的,也是比较好的做法。如果这些设计模式添加的比较晚的话,就不得不调整结构,重构整个功能模块。
在重构时,代码越多,就越是要小心。当项目为了共享功能而嵌套过多,要小心嵌套地狱发生。
<WrapperOne>
<WrapperTwo>
<WrapperThree>
<WrapperFour>
<WrapperFive>
<Component>
<h1>Finally in the component!</h1>
</Component>
</WrapperFive>
</WrapperFour>
</WrapperThree>
</WrapperTwo>
</WrapperOne>
嵌套地狱会让程序的数据流变得难以理解,并且会难以查找异常。
复杂性
随着class Component的功能越来越多,组件也变得越来越臃肿,很多不相关的逻辑混合在生命周期函数中,这时组件就变得杂乱而没有条理,查找确切的逻辑使用范围就变得困难,调试和优化也变得越来越难。
下面我们来看一个例子:
import { Count } from "./Count";
import { Width } from "./Width";
export default class Counter extends React.Component {
constructor() {
super();
this.state = {
count: 0,
width: 0
};
}
componentDidMount() {
this.handleResize();
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
increment = () => {
this.setState(({ count }) => ({ count: count + 1 }));
};
decrement = () => {
this.setState(({ count }) => ({ count: count - 1 }));
};
handleResize = () => {
this.setState({ width: window.innerWidth });
};
render() {
return (
<div className="App">
<Count
count={this.state.count}
increment={this.increment}
decrement={this.decrement}
/>
<div id="divider" />
<Width width={this.state.width} />
</div>
);
}
}
上面的代码,可以可视化的分析如下:

尽管这是个小例子,但逻辑已经混杂在一起了,随着业务的增长,各种逻辑代码混杂的更为繁琐。
除了逻辑混杂,生命周期中的逻辑也多有重复,比如说上面的例子,在componentDidMount和componentWillUnmount里都有关于window resize方法。
Hooks
由此得出结论,在React中class Component使用已经遇到了瓶颈。正是为了解决class Component的瓶颈,React官方创造性地引入了Hooks,Hooks简单来说就是可以在Function Component中使用state和生命周期方法。
React Hooks具体能做什么呢?
- 在Function Component中使用state;
- 在Function Component中管理组件的生命周期,并不使用componentWillMount和componentDidMount;
- 在全局多个组件中复用包含相同state或逻辑的代码;
接下来,我们来看一下,怎么在Function中使用state吧。
State Hooks
React Hooks为在Function component中使用state,提供了一个hook, 叫useState。
下面让我们来看看使用useState怎么把一个class Component改组成为一个Function Component。 这里我们假设class Component是一个双向绑定的输入框,代码如下:
class Input extends React.Component {
constructor() {
super();
this.state = { input: "" };
this.handleInput = this.handleInput.bind(this);
}
handleInput(e) {
this.setState({ input: e.target.value });
}
render() {
<input onChange={handleInput} value={this.state.input} />;
}
}
在着手改造之前,我们先来学习下useState基础知识:
首先useState接收一个初始值作为参数,在这里的示例中就是空字符串;同时我们可以从useState的返回值中解构出两个属性:
- 代表当前值的state变量;
- 能改变state的方法;
const [value, setValue] = React.useState(initialValue);
第一个返回值,代表state变量的value相当于class Component里的this.state[value]; 而第二个修改state的方法可以类比class Component里的this.setState方法。
现在就可以开始改造前面提到的Input这个组件了,由于功能比较简单,改造后代码如下:
function Input() {
const [input, setInput] = React.useState("");
return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}
这里的内容比较简单,就不再过多解释。其中有两点要特别提一嘴:
- useState第二个返回值,setValue方法按照命名规范,要采用set开头,并且驼峰命名————setXxxx这种格式;
- useState第二个返回值,setValue方法除了接受具体值,还可以接收一个方法,这个方法的作用是手动合并state的其他属性;
根据官方文档提示:useState的第二个返回值更新方法没有像class Component里的setState自动合并state逻辑,所以可以用传递一个方法来手动合并;同时官方文档也推荐使用useReducer来管理多属性的state对象
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Effect Hook
现在我们已经知道可以通过useState在Function Component组件里使用state,那么怎么在Function Component里使用生命周期呢?
通过useEffect我们可以用钩子实现生命周期函数,useEffect函数可以有效地实现类似class Component的
componentDidMount
, componentDidUpdate
, and componentWillUnmount
生命周期函数。
componentDidMount() { ... }
useEffect(() => { ... }, [])
componentWillUnmount() { ... }
useEffect(() => { return () => { ... } }, [])
componentDidUpdate() { ... }
useEffect(() => { ... })
useEffect可以多次调用,这里主要讲解useEffect二个参数:
- 第二个参数为空数组只会执行一次,可以实现类似componentDidMount生命周期;
- 第一个参数接受一个方法,这个方法可以返回一个函数,这个函数在组件销毁前调用,可以实现类似componentWillUnmount生命周期;
- 第二个参数不传,组件每次更新执行一次,可以实现类似componentDidUpdate生命周期;
使用上面input的例子:
import React, { useState, useEffect } from "react";
export default function Input() {
const [input, setInput] = useState("");
useEffect(() => {
console.log(`The user typed ${input}`);
}, [input]);
return (
<input
onChange={e => setInput(e.target.value)}
value={input}
placeholder="Type something..."
/>
);
}
通过log我们可以知道,这个useEffect在input每次改变的时候调用;
自定义Hook
react官方还提供除前面讲的useState
, useEffect
,其他hook有, useReducer
, useRef
, useContext
, useMemo
, useImperativeHandle
, useLayoutEffect
, useDebugValue
, useCallback
,下面调重点简单介绍下。
- useReducer会自动合并state里的其他属性,等同于class Component里的setState;
- useContext用来跨层级传递参数,前面供应商模式中有提到;
- useRef用来缓存dom对象,操作真实dom;
- useMemo用来缓存方法返回值;
- useCallback用来缓存方法;
这些都是比较常用的,都可以用来优化Function Component的性能。
当然我们也可以自定义hooks,需要注意的是:
- 只有use命名开头的方法,才会被React识别为hook;
- 另外自定义的hook里要使用官方提供的hook, 满足这两个条件才是一个合格的自定义hook。
比如说我们要监听键盘某个按键的事件,我们可以自定义一个hook:
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = React.useState(false);
function handleDown({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
function handleUp({ key }) {
if (key === targetKey) {
setKeyPressed(false);
}
}
React.useEffect(() => {
window.addEventListener("keydown", handleDown);
window.addEventListener("keyup", handleUp);
return () => {
window.removeEventListener("keydown", handleDown);
window.removeEventListener("keyup", handleUp);
};
}, []);
return keyPressed;
}
下面来看一下怎么使用,比如监听q
, l
or w
键:
import React from "react";
import useKeyPress from "./useKeyPress";
export default function Input() {
const [input, setInput] = React.useState("");
const pressQ = useKeyPress("q");
const pressW = useKeyPress("w");
const pressL = useKeyPress("l");
React.useEffect(() => {
console.log(`The user pressed Q!`);
}, [pressQ]);
React.useEffect(() => {
console.log(`The user pressed W!`);
}, [pressW]);
React.useEffect(() => {
console.log(`The user pressed L!`);
}, [pressL]);
return (
<input
onChange={e => setInput(e.target.value)}
value={input}
placeholder="Type something..."
/>
);
}
现在我们可以监听键盘上任意按键,而不用一遍又一遍地写监听事件,解绑事件了。
另一个关于Hook的好消息就是社区里已经有很多优秀的自定义hook集合,而不用我们自己实现了,下面给他家推荐一些:
当然国内也有阿里开源的高质量的hooks:
总结
使用Hook主要有以下几点好处:
- 代码行能减少很多;
- 复用state逻辑;
- 复用非可视化逻辑;
当然hook也有一些缺点需要注意:
- hook的规范不好执行,不使用lint插件,难以排查;
- 需要一定的学习成本和实践经验,比如useEffect;
- 可能会错误使用hook,比如useMemo和useCallback;
随着hook的引入,带来一个新的问题,怎么选择是该使用hook还是该使用class呢?
总的来说hook有更浅的dom层级嵌套和更清晰的逻辑。所以除非一些需要用到class独有的类似继承之类的属性,其他场景都可以使用hook来减少工作量,从而有更多的摸鱼时间。
相关推荐
第一节:单例模式:高并发造成的数据统计困难?看我单例模式一招制敌
第二节:替身模式:JS和迪丽热巴一样有专业替身?没听过的快来补补课...
第三节:供应商模式:还在层层传递props?来学学非常实用的供应商模式吧
第四节:原型模式:都知道JavaScript原型,但设计模式里的原型模式你会用吗?
第五节:视图和逻辑分离模式:React Hooks时代,怎么实现视图与逻辑分离呢?
第六节:观察者模式:是时候拿出高级的技术了————观察者模式
第七节:模块化模式:前端性能优化进阶篇——动态加载模块基础补遗
第八节:混合模式:在React Hook时代,Object.assign这种混合写法还要用吗?
第九节:中间件模式:如何使用中间件优化多对多通信?
第十节:高阶组件模式:在React Hooks时代,高阶组件只能感叹:既生瑜何生亮?
第十一节:传递render方法模式:如何在提升state的层级时,避免父级子组件重新渲染问题
相关活动
转载自:https://juejin.cn/post/7208571916146212921