函数式组件

最近在写 RN 的项目,开始关注 函数式组件。其实上次在做 发动机搜索组件 的时候,就有接触过了,但是并没有深入了解。但是这次也并没有特别深入的学习,因为确实对 React 的使用经验不足,其中说到的一些场景和遇到的一些问题,并没有体会和了解,现在只是聊一下,在这个需求的开发中的一些开发体验,和了解到的一些相关知识。

类组件 vs 函数式组件

在 React 中,组件是”一级公民“,也是 React 的基础。类组件是在 React 比较早推出的组件创建方法,而函数式组件是这几年才登上舞台,解决了一些 类组件 的痛点,得到了很多开发者的簇拥。

类组件

从 es6 开始,JavaScript 已经支持了 Class 的语法,React 也支持通过 Class 的方式实现组件,只要继承 React.Component 就可以了,并提供了一系列的生命周期方法,最后再通过 render 方法返回视图。大致的写法如下:

class Welcome extends React.Component {
    
  componentDidMount() {
      // 组件加载完成
  }

  componentWillUnmount() {
      // 组件卸载
  }
    
  componentDidUpdate(){
      // 组件更新
  }
    
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

函数式组件

函数式组件,就更简单了,就是一个函数,直接 return JSX 视图就可以了。获取组件的时候,就只要执行这个函数就行了,这是 FP (函数式编程)的思想。

理想的函数式组件就是 纯函数 ,也就是 view = fn(props)

const Welcome = (props) =>{
    rturn(
        <h1>hello,{props.name}</h1>
    )
}

传统的 函数式组件 是没有状态(state)和 生命周期的,一般只能用在比较简单的视图。我们可以很直观的发现,函数式组件非常简单。

还有一点要特别注意,由于函数式组件是一个函数,所以在这里面创建的变量在下次重新执行的时候还会是默认值。

类组件的痛点

  • 复杂的生命周期
    跟 Android 中类似,类组件有它本身的生命周期方法,所以经常需要在一对生命周期方法中进行反操作,比如 注册 和 反注册。在 Android 中引入了 Lifecycle 相关组件来简化生命周期的管理。而且 componentDidMountcomponentDidUpdate 往往有部分相同的界面渲染方法,代码冗余。

  • 组件状态逻辑复用困难

    对于一些内部逻辑相同的组件,很难复用一套逻辑。或许 render props高阶组件 来进行处理,但是往往会使代码复杂和难以理解。

  • 难以理解的 Class
    由于 JS 本身的问题,this 对象的工作方式往往不容易理解,还容易出错。

单单是函数式组件也无法解决这些问题,但是再加上 Hook 就有了更好、更简单的处理方式。

Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

可以使用 useEffect 来模拟生命周期方法,使用useState 来模拟 state 。更高阶的用法就是使用 Hook 抽取出一套组件的状态处理逻辑,达到状态逻辑的复用。可以说是 函数式组件 加上 Hook 可以实现 类组件 的功能,而且更加简单和易于理解。

useState

函数式组件没有 state ,Hook 的解决方案是使用 useState 的方式来模拟 state。有了 useState 我们就可以像 类组件 一样使用状态了。

useEffect

useEffect 是 Hook 中的重要部分,会在 渲染 或者 更新 之后调用这个方法。使用 useEffect 可以来模拟 componentDidUpdate 的生命周期方法。

如果每次都调用 useEffect 内部的函数 其实并不是很必要。 useEffect 函数还有第二个参数,如果这个参数没有进行改变,则不会调用 useEffect 第一个参数的函数。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

这个在 类组件 中的操作大致是这样的:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

如果第二个参数传入的是一个空数组,则只会在第一次更新的时候调用第一个参数函数,因为一个空数组是不会改变的,所以只会执行一次。这样就可以模拟 componentDidMount 这个生命周期的方法。

如果在类组件中,有多个状态需要检查,componentDidUpdate 方法里面也会变得比较复杂,但是在 函数式组件 中,你可以创建多个 useEffect 使这些状态的判断相互独立,逻辑更加清晰。

如果需要在组件销毁时做一些清除工作呢?类似 componentWillUnmount 的方法。

useEffect 也能支持,在 useEffect 第一个参数函数里面返回一个函数,在这个返回函数里面进行清除工作就行了。有几个细节要注意,这个清除工作会在 第二个参数 的值进行变化是才会进行清除工作,也就是调用这个返回函数。同时,由于第二个参数发生了改变,所以会重新调用一次 第一个参数函数。可以看到,其实并不是真正的在组件销毁的时候进行清除工作,而是每次更新的时候都会进行一次 解绑 和 绑定 的操作,这两个操作是同步进行的,而且还是异步的操作。

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

关于 useEffect ,还有一些细节需要注意。

useEffect 对于外部的变量可以直接使用,比如 state ,但是要特别注意,useEffect 使用了 闭包的特性 来引用这些变量,所以如果在 useEffect 中使用一些延迟的操作,比如 setTimeOut 的方法是要注意,此时使用的 props 或者 state 都是不一定是最新的。与 类组件 不同,由于类组件是一个完整的对象,props 或者 state 是内部的一个属性,只有一个,一旦更新对其他方法来说都是同步更新的。

其他 Hook

推荐阅读:React Hook 最佳实践