自从我深入了解了 react hooks,被其简洁的编写方式和强大的扩展性深深吸引,然后我就迫不及待地想把它应用到实际项目中。

使用下来发现,真香!但是坑也不少。react hooks 使用起来代码确实简洁不少,甚至说有点黑魔法的味道。如果使用者不怎么了解 hooks 实现原理和函数组件的渲染过程,很容易导致使用 hooks 编写组件时出现一些很离奇的 bug,所以使用好 hooks 还是需要一定的学习成本。

这篇文章主要讨论 react 中类组件和函数组件的一些区别。

函数组件和 class 组件渲染过程的区别

先来看看测试代码:

import * as React from "react";
import { render } from "react-dom";
const { Component } = React;

interface ClassComponentProps {
  color: string;
}

// 类组件
class ClassComponent extends Component<ClassComponentProps> {
  constructor(props) {
    super(props);
    console.log('create a class component!');
  }

  render() {
    console.log('re-render class component!');
    const { color } = this.props;
    return (
      <div className="class-component" style={{ color: color }}>class component</div>
    );
  }
}

interface FuncComponentProps {
  color: string;
}

// 函数组件
const FuncComponent = ({ color }: FuncComponentProps) => {
  console.log('execute the function component!');
  
  return (
    <div className="func-component" style={{ color: color }}>function component</div>
  );
};

// 函数组件
function App() {
  const [color, setColor] = React.useState<string>("black");
  const [inputColor, setInputColor] = React.useState<string>('');
  const handleInputChange = (event) => setInputColor(event.target.value);
  const handleSubmit = (event) => setColor(inputColor);
  
  return (
    <div className="App">
      <ClassComponent color={color} />
      <FuncComponent color={color} />
      <input type="text" value={inputColor} onChange={handleInputChange}/>
      <button onClick={handleSubmit}>修改颜色</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);
console.log('Init the App!');
复制代码

上面的代码总共定义了3个组件,一个 class 组件即 ClassComponent,另2个都是函数组件:FuncComponent 和 App 组件。整个代码的效果就是你可以通过在输入框输入一个颜色值来改变 ClassComponent 和 FuncComponent 组件的文本颜色。

总结:

class 组件的整个生命周期就是其实例创建(new)到销毁(unmount)的过程。

函数式组件在每次渲染时则是直接执行一次函数式组件本身这个函数

看一下面的测试截图:

从截图可以看出一些信息,项目初始化完成时创建了一个 ClassComponent 组件实例,执行了构造器, render 函数。执行了一次函数式组件 FuncComponent 这个函数。初始化完毕,输出前四句:

create a class component!
re-render class component!
execute the function component!
Init the App!
复制代码

然后在输入blue的过程中由于调用了 setInputColor,并且新的 inputputColor和之前的 color 不一样,所以会重新渲染。重新渲染过程中不会创建新的 ClassComponent 实例,只是执行了先前实例的 render 函数,这样的话,重新渲染的时候在 render 函数中使用的变量是之前就绑定到 ClassComponent 实例上的变量。

而对于函数式组件,只是纯粹的暴力的重新执行 FuncComponent这个函数,渲染它输出的结果,这里我们要注意,因为函数式组件重新渲染只是重新执行函数式组件本身,所以函数式组件是无法保存上次渲染过程中的变量了,也就是无法使用 state。所以在 react hooks API 出来之前,我们使用函数式组件都是在不使用 state 的情况下作为一个无状态的 UI 组件使用(不考虑使用其它工具库,如: mobx)。

因为 blue 输入每个字符时都触发了 input 事件,所以重复输出了4次,再加上最后点击修改颜色又触发重新渲染所以总共是重复输出5次。

re-render class component! 
execute the function component! 
re-render class component! 
execute the function component! 
re-render class component! 
execute the function component! 
re-render class component! 
execute the function component! 
re-render class component! 
execute the function component! 
复制代码

react hooks 为 function component 带来 state

上面的测试代码中, App 组件也是函数式组件,但是我们看到有了 react hooks 之后,函数式组件也有了 state

写个喜闻乐见的计数器:

import * as React from "react";
import { render } from "react-dom";

function App() {
  const [count, setCount] = React.useState<number>(0);
  const increate = () => setCount(count + 1)
  
  return (
    <div className="App">
      <h3 className="count">{count}</h3>
      <button onClick={increate}>+</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);
复制代码

想想看,换成 class 组件要多好几行代码😂。我们来分析一下,我们点击 + 按钮后发生了啥:

点击了 + 按钮触发点击事件,调用 setCount(count + 1),由于新的 count 比原来不一样,导致 App 组件重新渲染,重新执行 App 函数,执行到 const [count, setCount] = React.useState<number>(0) 这一行的时候,我们拿到 count这个变量,同样是通过 useState(0)。我们知道对于 App 函数,第一次初始化和此时的重新渲染,每次函数执行过程中的变量都是重新定义的。为什么这个时候返回的却是更新后的 count 呢?这就有点黑魔法的味道了,为什么第二次执行 use state 拿到的是更新后的 count。其实网上讲 hooks 原理的文章已经有不少了。这里我简单叙述下我当前的理解:

对于 useState 这个 hook。因为我们是可以多次调用 useState 的,所以需要一个数组 stateArray 来保存多个 state。然后还需要一个游标变量 cursor 来保存当前 useState 的次序,默认为 0.

在函数组件初始化时也就是第一次执行函数组件时,每调用一次 useState(initState),就会把 initState保存到stateArray[cursor]。然后将游标 cursor 加一,函数组件执行结束时将游标置 0。

当你在组件的事件回调中 setState(newState) 了,会将 newSatte 直接替换原有的 state。

再次渲染函数组件时,也就是重新执行函数组件,因为游标被置 0,所以从 stateArray可以取到对应的更新后的 state。

除了第一次渲染和 useState(initState) 中的 initState 有关系外,后续的渲染和 initState 都没关系。

关于 react hooks 的第一篇文章就到这里了,下一篇将介绍如何使用 hooks 让函数组件能达到以前只有使用 class 组件才能做到的功能以及 custom hooks。

本文为原创内容,首发于个人博客,转载请注明出处。