TL;DR

一句话总结 React Hooks 就是在 react 函数组件中,也可以使用类组件(classes components)的 state 和 组件生命周期,而不需要在 mixin、 函数组件、HOC组件和 render props 之间来回切换,使得函数组件的功能更加实在,更加方便我们在业务中实现业务逻辑代码的分离和组件的复用。

本文将从以下几个方面介绍 hooks


Hooks 在解决什么问题 Hooks 的 api 介绍 和如何使用 hooks Hooks 是怎么实现的


????Hooks 在解决什么问题

React 一直在解决一个问题,如何实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用。

一般情况下,我们都是通过组件和自上而下传递的数据流将我们页面上的大型UI组织成为独立的小型UI,实现组件的重用。但是我们经常遇到很难侵入一个复杂的组件中实现重用,因为组件的逻辑是有状态的,无法提取到函数组件当中。这在处理动画和表单的时候,尤其常见,当我们在组件中连接外部的数据源,然后希望在组件中执行更多其他的操作的时候,我们就会把组件搞得特别糟糕:


  • 难以重用和共享组件中的与状态相关的逻辑,造成产生很多巨大的组件
  • 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 localstate 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面。
  • 复杂的模式,如渲染道具和高阶组件。
  • 由于业务变动,函数组件不得不改为类组件。

这时候,Hooks就派上用场了。 Hooks 允许我们将组件内部的逻辑,组织成为一个可复用的隔离模块。

借用 @Sunil Pai 的两张图来说明这个问题:

理解 React Hooks_自定义image.png

理解 React Hooks_自定义_02

从 React Hooks 中体验出来的是 React 的哲学在组件内部的实现,以前我们只在组件和组件直接体现 React 的哲学,就是清晰明确的数据流和组成形式。既可以复用组件内的逻辑,也不会出现 HOC 带来的层层嵌套,更加不会出现 Mixin 的弊端。

????Hooks 的 api 介绍 和如何使用 hooks

@dan_abramov 在会议上给我们介绍了 hooks 的三个关键的api,分别是 ​​State Hooks​​​ 、 ​​Effect Hooks​​​ 、 ​​Custom Hooks(自定义hooks)​

????state Hooks (useState)

useState 这个方法可以为我们的函数组件带来 local state,它接收一个用于初始 state 的值,返回一对变量。 让函数组件拥有自己的组件。

首先如果我们需要用 classes component 实现一个点击按钮 +1 组件应该怎么写呢?

import React from 'react';

class Example extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.clickBtn = this.clickBtn.bind(this);
}
clickBtn = () => {
this.setState({
count: this.state.count + 1;
});
}
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.clickBtn}>
Click me
</button>
</div>
);
}

那使用 useState 是怎么样的呢? 可以看见非常清晰明了。

// 一个简单的点击计数
import { useState } from 'react';

function Example() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

????Effect Hooks (useEffect)

Effect Hooks 用于处理一些带有副作用的操作,下面通过监听窗口宽度的变化代码为例,说明 effect hooks 的使用fangfa

import { useState } from 'react';

function windowWidth() {
const [width, setWithd] = useState(window.innerWidth);
useEffect(() => {
const handleResize = ()=>{
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
});
return (
<p> window width is {width}</p>
)
}

useEffect 可以传入第二个操作来避免性能的损耗,如果第二个参数数组中的成员变量没有变化则会跳过此次改变。如何传入一个空数组 ,那么该 effect 只会在组件 mount 和 unmount 时期执行。

import { useState } from 'react';

function windowWidth() {
const [width, setWithd] = useState(window.innerWidth);
useEffect(() => {
const handleResize = ()=>{
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
}, [width]); // width 没有变化则不处理
return (
<p> window width is {width}</p>
)
}

useEffect 中还可以通过让函数返回一个函数来进行一些取消兼容之类的清理操作,比如取消订阅等

import { useState } from 'react';

function windowWidth() {
const [width, setWithd] = useState(window.innerWidth);

useEffect(() => {
const handleResize = ()=>{
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);

return () => {
// 取消监听窗口的宽度变化
window.removeEventListener('resize');
}
});
return (
<p> window width is {width}</p>
)
}

如上所示,内置的 React Hooks 如 useState 和 useEffect 充当基本构建块。 我们可以直接在组件中使用它们,或者我们可以将它们组合到自定义Hook中,例如useWindowWidth。使用自定义Hooks感觉就像使用React的内置API一样。

????Custom Hooks 自定义组件

接着上面的监听窗口大小的代码,我们接着讲自定义 hooks, 证明 react hooks 是怎么使到组件内的逻辑可复用的。

Talk is cheap, show me the code.

// 一个显示目前窗口大小的组件
function responsiveComponent(){
// custom hooks
const width = useWindowWidth();
return (
<p>当前窗口的宽度是 {width}</p>
)
}

上面的代码只有几行,非常清晰明了说明了他的作用就是监听当前窗口的变化,这就是Hooks的目标 - 使组件真正具有声明性,即使它们包含状态和副作用。

我们来看看如何实现这个自定义Hook。我们使用React本地状态来保持当前窗口宽度,并在窗口调整大小时使用副作用来设置该状态

import { useState, useEffect} from 'react';
// custom hooks to listen window width change
function useWindowWidth(){
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
const handleResize = ()=>{
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
}, [width]); // width 没有变化则不处理

return width;
}

⚡ React Hooks 的规则

Hooks 是JavaScript函数,但它们强加了两个额外的规则:


  • 只能在顶层调用Hooks。不要在循环,条件或嵌套函数中调用Hook。
  • 仅从React功能组件调用Hooks。不要从常规JavaScript函数中调用Hook。 (还有另一个地方可以调用Hooks——你自己的定制Hooks。)

???? 其他 Hooks

这里有一些不常用的内置Hook。例如,useContext允许您订阅React上下文而不引入嵌套:

function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
// ...
}

发现一个很有趣的仓库,react-use, 包含了很多很有趣的自定义hooks

????hooks 是如何工作的

react hooks 其实只是一个数组,并不是奇妙的魔法。

如何实现 ​​useState()​​ 方法

让我们在这里通过一个例子来演示状态 hooks 的实现如何工作。

首先让我们从一个组件开始:

function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi");
const [lastName, setLastName] = useState("Yardley");

return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}

hooks API背后的想法是你可以使用一个setter函数作为hook函数中的第二个数组项返回,而setter将控制由hook管理的状态。

那么React与此有什么关系呢?

让我们了解这在React内部如何工作。 以下内容可在执行上下文中用于呈现特定组件。 这意味着此处存储的数据位于正在渲染的组件之外。 此状态不与其他组件共享,但它保留在可以随后渲染特定组件的范围内。

1)初始化

创建两个空数组:​​setters​​​和​​state​

将光标设置为 0

理解 React Hooks_复用_03image.png

初始化:两个空数组,Cursor为0

2) 首次渲染

首次运行组件功能。

每次useState()调用,当在第一次运行时,将setter函数(绑定到光标位置)推送到setter数组,然后将某个状态推送到state数组。

理解 React Hooks_数组_04image.png

第一次渲染:作为光标增量写入数组的项目。

3) 后续渲染

每个后续渲染都会重置光标,并且只从每个数组中读取这些值。

理解 React Hooks_数组_05image.png

后续渲染:从数组中读取的项目为光标增量

4) 事件处理

每个setter都有一个对它的光标位置的引用,因此通过触发对任何setter的调用,它将改变状态数组中该位置的状态值。

理解 React Hooks_数组_06image.png

Setters“记住”他们的索引并根据它设置内存。

通过伪代码实现 useState 功能

这是一个演示实现的代码示例:

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
return function setterWithCursor(newVal) {
state[cursor] = newVal;
};
}

// useState的伪代码实现
export function useState(initVal) {
if (firstRun) {
state.push(initVal);
setters.push(createSetter(cursor));
firstRun = false;
}

const setter = setters[cursor];
const value = state[cursor];

cursor++;
return [value, setter];
}

// 模拟使用useState
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
const [lastName, setLastName] = useState("Yardley"); // cursor: 1

return (
<div>
<Button onClick={() => setFirstName("Richard")}>Richard</Button>
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
</div>
);
}

// 模拟Reacts渲染周期
function MyComponent() {
cursor = 0; // 重置光标的位置
return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // 首次渲染: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // 后续渲染: ['Rudi', 'Yardley']

// 点击'Fred' 按钮

console.log(state); // 点击后: ['Fred', 'Yardley']