理解 React Hooks

上周,Sophie Alpert 和 Dan Abramov 在 React Conf 2018 中 提出了 hooks 这个概念,让我们一起来看看 Hooks 在解决一个什么问题。

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 的两张图来说明这个问题:

image.png
image.png

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

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

@dan_abramov 在会议上给我们介绍了 hooks 的三个关键的api,分别是 State HooksEffect HooksCustom 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-not-magic-just-arrays.

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)初始化

创建两个空数组:settersstate

将光标设置为 0

image.png

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

2) 首次渲染

首次运行组件功能。

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

image.png

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

3) 后续渲染

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

image.png

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

4) 事件处理

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

image.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']

总结

Hooks 还处于早期阶段,但是给我们复用组件的逻辑提供了一个很好的思路,大家可以在 react-16.7.0-alpha.0 中体验。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

前端探索

2 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏前端儿

React 基础实例教程

首先,需要核心库react.js与React的DOM操作组件react-dom.js

702
来自专栏lonelydawn的前端猿区

一款轻量级树形控件EasyTreeview

使用方法 引入 <link rel="stylesheet" type="text/css" href="./css/index.min.css"> <div ...

3219
来自专栏糊一笑

IScroll的那些事——内容不足时下拉刷新

之前项目中的列表是采用的IScroll,但是在使用IScroll有一个问题就是:当内容不足全屏的时候,是木有办法往下拉的,这样就达不到刷新的目的了。【这是本人工...

35611
来自专栏菩提树下的杨过

Flash/Flex学习笔记(6):制作基于xml数据源的flv视频播放器

今天折腾了大半天,总算搞出了一个功能简单的视频播放器,可以向公司领导交差了 :) 步骤: 1.Flash CS4 中 先拖一个"FLVPlayback"组件到舞...

2025
来自专栏贾鹏辉的技术专栏@CrazyCodeBoy

React Native之React速学教程(中)

React Native之React速学教程(中) 本文出自《React Native学习笔记》系列文章。 React Native是基于React的,在开发R...

2988
来自专栏QQ会员技术团队的专栏

深入理解React(二) :数据流和事件原理

在React中,数据流是自上而下单向的从父节点传递到子节点,所以组件是简单且容易把握的,他们只需要从父节点提供的props中获取数据并渲染即可。如果顶层组件的某...

4.3K0
来自专栏IMWeb前端团队

React16中的服务端渲染(译)

本文作者:IMWeb zzbozheng 原文出处:IMWeb社区 未经同意,禁止转载 React 16发布了。 React 16有很多令人兴奋的新东...

7069
来自专栏MasiMaro 的技术博文

Windows程序设计学习笔记(四)自绘控件与贴图的实现

Windows系统提供大量的控件供我们使用,但是系统提供的控件样式都是统一的,不管什么东西看久了自然会厌烦,为了使界面更加美观,添加一些新的东西我们需要自己绘制...

742
来自专栏web前端

JavaScript基础学习--02属性操作

一、思路 1、模拟手机聊天思路:      a.静态页面html+css,包括双发短信发送成功后的基本样式。      b.获取头像、输入框、发送按钮和聊天内...

1889
来自专栏转载gongluck的CSDN博客

MFC自定义控件

需要在MFC实现自定义控件功能,网上搜集找的如下方法实现: 以下是步骤说明。 一、自定义一个空白控件  1、先创建一个MFC工程        NEW Pr...

3607

扫码关注云+社区