likes
comments
collection
share

和React Hook相比class component到底输在了哪里?

作者站长头像
站长
· 阅读数 33

背景介绍

这是设计模式系列的第十二节,学习的是里设计模式中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>
    );
  }
}

上面的代码,可以可视化的分析如下:

和React Hook相比class component到底输在了哪里?

尽管这是个小例子,但逻辑已经混杂在一起了,随着业务的增长,各种逻辑代码混杂的更为繁琐。

除了逻辑混杂,生命周期中的逻辑也多有重复,比如说上面的例子,在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的 componentDidMountcomponentDidUpdate, 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官方还提供除前面讲的useStateuseEffect,其他hook有, useReduceruseRefuseContextuseMemouseImperativeHandleuseLayoutEffectuseDebugValueuseCallback,下面调重点简单介绍下。

  • 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;
}

下面来看一下怎么使用,比如监听ql 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的层级时,避免父级子组件重新渲染问题

相关活动