hooks和类组件的区别

为什么我么要使用hooks呢,这一点很多人可能觉得没必要去探究,觉得官方初的东西只要好用就行,对于这一点我觉得最重要的还是要去学习大佬们为什么去设计hooks,解决了什么问题,设计的思路是什么?React团队在设计层面的思路能够在一定程度上代表着当前业界在框架设计领域上的最佳实践。

函数组件的写法更轻量、灵活

在函数组件中我们不需要去继承一个class对象,不需要去记忆那些生命周期,不需要把数据定义在state中。函数作为js中的一等公民,可以让我们更加高效更加灵活的去组织代码。

类组件的自身缺陷

1、如果我们需要一个只跟着视图走的数据,我们不能直接使用props或者state。这个我们可以通过一个实例来看看。

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

上面用类组件和函数组件实现了同一个逻辑,两个组件都会接受到来自父组件传过来的props.user,在点击按钮之后会在3秒之后alert一条消息。

假如我一开始传入的props值是小红,然后在三秒之内去改变props.user的值,变成小绿,这两个组件会分别输出小绿小红。为什么会这样呢?这里我先只介绍类组件, 在React的类组件中,props虽然是不变的,但是this永远是可变。当有异步的事件触发,它获取到的props或者state永远都是最新的。

2、使用bind或者箭头函数去约束我们函数中this的作用域

3、状态逻辑的难以复用以及复杂组件变得难以理解

对于状态逻辑的复用,虽然在类组件中也可以使用高阶组件+继承解决,但hooks似乎有更好的解决方案。而对于复杂组件难以理解是在平时写代码的时候最常见的一个问题,一个组件写着写着状态越来越多,如果抽成子组件的话props和state又要传来传去,最后自己也看不懂,下面也举个实例。


对于状态逻辑的复用这种场景只要页面中有复用的组件,且这个组件又有较为复杂的状态逻辑,就会有这样的需求,举个常见的例子:在做后台系统中经常需要去做各种展示的列表,表格的内容各不相同,但是又都要有分页的行为,于是分页组件 就需要去抽象。


传统类组件

最开始我们可能不会想着通用,就直接写一个列表+分页的组件,

import { Component } from 'react';
export default class ListWithPagination extends Component {
  state = {
    page: 1,
    data: [],
}

componentDidMount() {
    this.fetchListData(this.setState);
}

handlePageChange = newPage =>
this.setState({ page: newPage }, this.fetchListData)

fetchListData = () => {
    const { page } = this.state;
    //模拟请求数据的函数,传入页数和size
    fetchList(page,20).then(data => this.setState({ data }));
}
  
render() {
    const { data, page } = this.state;
    return (
      <div>
        <ul className="list">
          {data.map((item, key) => (
            <li key={key}>{item}</li>
          ))}
        </ul>
        <div className="nav">
          <button type="button" onClick={() => this.handlePageChange(page - 1)}>
            上一页
          </button>
          <label>当前页: {page}</label>
          <button type="button" onClick={() => this.handlePageChange(page + 1)}>
            下一页
          </button>
        </div>
      </div>
    );
  }
}

这样就实现了基本的列表和分页组件,然后我们会想,每个地方都要有分页,唯一不一样的就是列表渲染跟数据请求api,那我们就可以抽象成高阶组件。

高阶组件

定义:高阶组件其实就是高阶函数,我们定义一个函数里面返回一个有状态组件,高阶组件的好处就是让我们的业务逻辑层和UI层分离,更加好维护。

方式:

  • 属性代理方式

    属性代理是最常见的一个丐姐组建的使用方式之一,他通过一些操作将被包裹的组件的props和新生成的props一起传递给此组件(这两个props一个是调用HOC函数传入的参数,一个是将写入HOC返回的组件中的参数)

  • 反向继承方式

    这种方式返回的组件继承了被传入的组件,所以他能访问的区域、权限更多比如可以直接访问传入组件的state数据

接着上面的说变成高阶函数会怎么样?

export default function ListHoc(ListComponent) {
  return class ListWithPagination extends Component {
    // ...同上述code,省略

    // 数据请求方法,从props中传入
    fetchListData = () => {
      const { fetchApi } = this.props;
      const { page } = this.state
      return fetchApi({ page }).then(data => this.setState({ data }));
    }

    render() {
      const { data, page } = this.state;
      return (
        <div>
          <ListComponent data={data} />
          <div className="nav">...省略</div>
        </div>
      );
    }
  };
}

这么一来,后面再写列表时,使用高阶组件包裹一下再把数据请求方法以props传入,达到一个复用状态逻辑与分页组件的效果。

我们在得意之际,又来了一个新的需求,说有一个列表分页导航,需要在列表上面。想一想有几个方案。

  • 传递一个props叫做"theme"的变量,控制不同的样式,这一看还行,但是到后面两种列表的风格越来越远,那高阶组件就会越来越重。
  • 再写一个类似的高阶组件,结构不一样其他一模一样,这样的话代码重复度太高了。
  • 再写一个组件,继承这个高阶组件,重写render,这样好像是可以的,但是这里继承就显得有些奇怪,这应该是兄弟关系,当然我们完全可以再抽出一层包含状态逻辑的组件,这两种表达形式都继承这个组件, 但是即使如此,通过继承来复写render的方式,无法清晰感知组件到底有哪些状态值,尤其在状态较多,逻辑较为复杂的情况下。这样日后维护,或者拓展render时,就举步维艰 。

HOOKS改造

首先改写最开始的类组件

import { useState, useEffect } from 'react';

export default function List() {
  const [page, setPage] = useState(1); // 初始页码为: 1
  const [list, setList] = useState([]); // 初始列表数据为空数组: []

  useEffect(() => {
    fetchList({ page }).then(setList);
  }, [page]); // 当page变更时,触发effect

  const prevPage = () => setPage(currentPage => currentPage - 1);
  const nextPage = () => setPage(currentPage => currentPage + 1);

  return (
    <div>
      <ul>
        {list.map((item, key) => (
          <li key={key}>{item}</li>
        ))}
      </ul>
      <div>
        <button type="button" onClick={prevPage}>
          上一页
        </button>
        <label>当前页: {page}</label>
        <button type="button" onClick={nextPage}>
          下一页
        </button>
      </div>
    </div>
  );
}

这里的运行机制就不说了,下面就用hooks来抽离我们的逻辑。

首先将我们的分页抽离出来:

const usePagination = (fetchApi) => {
  const [page, setPage] = useState(1);
  const [list, setList] = useState([]);

  useEffect(() => {
    fetchApi({ page }).then(setList);
  }, [page]);

  const prevPage = () => setPage(currentPage => currentPage - 1);
  const nextPage = () => setPage(currentPage => currentPage + 1);

  return [list, { page }, { prevPage, nextPage }];
};

export default function List() {
  const [list, { page }, { prevPage, nextPage }] = usePagination(fetchList);//获取处理好的数据结果
  return (
    <div>...省略</div>
  );
}

如果你希望分页的dom结构也想复用,那就再抽个函数便好。

function renderCommonList({ ListComponent, fetchApi }) {
  const [list, { page }, { prevPage, nextPage }] = usePagination(fetchApi);
  return (
    <div>
      <ListComponent list={list} />
      <div>
        <button type="button" onClick={prevPage}>
          上一页
        </button>
        <label>当前页: {page}</label>
        <button type="button" onClick={nextPage}>
          下一页
        </button>
      </div>
    </div>
  );
}

export default function List() {
  function ListComponent({ list }) {
    return (
      <ul>
        {list.map((item, key) => (
          <li key={key}>{item}</li>
        ))}
      </ul>
    );
  }
  return renderCommonList({
    ListComponent,
    fetchApi: fetchList,
  });
}

这样就实现了我们组件抽离的效果,如果希望有一个新的列表或者分页效果那完全可以再重写一个结构,总之最核心的状态已经抽离出来,我们爱放哪放哪, 这么一来,数据层与dom更加的分离,react组件更加的退化成一层UI层,进而更易阅读、维护、拓展。

HOOKS自身的不足

1、比较大的心智负担,我们需要时刻注意是否已经给hooks添加了必要的依赖项,在一些功能相对复杂的组件中,useEffect的重复渲染问题有时会非常棘手,而且不容易调试。

这个特性在对函数组件进行性能优化时也是会带来很大的麻烦,因为每次propsstate数据变化,都会导致函数组件中所有内容的重新渲染。我们需要通过memouseMemouseCallback这些方法手动去减少组件的render。当一个组件结构比较复杂,嵌套较多时,依赖项问题的处理也很让人头疼。

2、状态不同步,在一次渲染中组件的propsstate是保持不hooks变的,这个特性导致的闭包陷阱,是我们在开发中最常见的问题,因为函数的运行是独立的,每个函数都有自己的作用域,函数变量是保存在运行时的作用域里面,当我们有异步操作时会看到回调函数中的变量引用的是之前的也就是旧的。可以看看下面这个例子。

import React, { useState } from "react";
const Counter = () => {
  const [counter, setCounter] = useState(0);
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert(counter);
    }, 3000);
  };
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={() => setCounter(counter + 1)}>change count</button>
      <button onClick={onAlertButtonClick}>show count</button>
    </div>
  );
};
export default Counter;

当我点击完show count后立马去点击change count这一定要在3s内,三秒后我们看到结果竟然是0而不是1, 这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxxthis.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容