自从深入了解了 react hooks(后面简称 hooks),被其简洁的编写方式和强大的扩展性深深吸引,然后我就迫不及待地想把它应用到实际项目中。
使用下来发现,真香!但是坑也不少,react hooks 使用起来确实简洁不少,甚至说有点黑魔法的味道。这就容易导致不怎么了解 hooks 实现原理和函数组件的渲染过程的人很容易在使用 hooks 编写组件时出现一些很离奇的 bug。
这篇文章主要讨论 react 中 class 组件和函数组件的一些区别,为后续的文章分析在函数组件中使用 hooks 时由于函数式组件的变量作用域造成的 bug 做铺垫。
函数式组件和 class 组件渲染上区别
先来看看测试代码:
import * as React from "react";
import { render } from "react-dom";
const { Component } = React;
interface ClassComponentProps {
color: string;
}
// class 组件
class ClassComponent extends Component {
constructor(props) {
super(props);
console.log('create a class component!');
}
render() {
console.log('re-render class component!');
const { color } = this.props;
return (
"class-component" style={{ color: color }}>class component
);
}
}
interface FuncComponentProps {
color: string;
}
const FuncComponent = ({ color }: FuncComponentProps) => {
console.log('execute the function component!');
return (
"func-component" style={{ color: color }}>function component
);
};
function App() {
const [color, setColor] = React.useState("black");
const [inputColor, setInputColor] = React.useState('');
const handleInputChange = (event) => setInputColor(event.target.value);
const handleSubmit = (event) => setColor(inputColor);
return (
"App">
type="text" value={inputColor} onChange={handleInputChange}/>
);
}
const rootElement = document.getElementById("root");
render( , rootElement);
console.log('Init the App!');
复制代码
上面的代码总共定义了3个组件,一个 class 组件即 ClassComponent
,另2个都是函数式组件包括 App 组件。通过输入一个颜色值改变 ClassComponent 和 FuncComponent 组件的文本颜色。
class 组件的整个生命周期就是其实例创建(new)到销毁的过程。
函数式组件在每次渲染时则是直接执行一次其函数式组件这个函数本身。
看一下面的测试截图:
从截图可以看出一些信息,项目初始化完成时创建了一个 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)。
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(0);
const increate = () => setCount(count + 1)
return (
"App">
"count">{count}
);
}
const rootElement = document.getElementById("root");
render( , rootElement);
复制代码
想想看,换成 class 组件要多好几行代码?。我们来分析一下,我们点击 +
按钮后发生了啥:
点击了 +
按钮触发点击事件,调用 setCount(count + 1)
,由于新的 count 比原来不一样,导致 App 组件重新渲染,重新执行 App 函数,执行到 const [count, setCount] = React.useState
这一行的时候,我们拿到 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。