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

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

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

  return (
    
  );
}

上面用类组件和函数组件实现了同一个逻辑,两个组件都会接受到来自父组件传过来的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 (
      
    {data.map((item, key) => (
  • {item}
  • ))}
); } }

这样就实现了基本的列表和分页组件,然后我们会想,每个地方都要有分页,唯一不一样的就是列表渲染跟数据请求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 (
        
...省略
); } }; }

这么一来,后面再写列表时,使用高阶组件包裹一下再把数据请求方法以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 (
    
    {list.map((item, key) => (
  • {item}
  • ))}
); }

这里的运行机制就不说了,下面就用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 (
    
...省略
); }

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

function renderCommonList({ ListComponent, fetchApi }) {
  const [list, { page }, { prevPage, nextPage }] = usePagination(fetchApi);
  return (
    
); } export default function List() { function ListComponent({ list }) { return (
    {list.map((item, key) => (
  • {item}
  • ))}
); } 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 (
    

You clicked {counter} times.

); }; export default Counter;

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

你可能感兴趣的:(hooks和类组件的区别)