React Hooks 核心原理与实战

1,240 阅读10分钟

一、理解 Hooks: React 为什么要发明 Hooks

react 组件得本质

Modal => View 得映射,其中 Modal 对应 react 中的 state + props。

react 通过 JSX 将 Modal 与 View 进行 数据绑定react Modal => view

1)Class 组件的问题

在于 类 最主要的特性并没有被利用。

  • React 组件之间是不会互相继承的。 比如说,你不会创建一个 Button 组件,然后再创建一个 DropdownButton 来继承 Button。所以说,React 中其实是没有利用到 Class 的继承特性的。

  • 因为所有 View 都是由状态驱动 的,因此很少会在外部去调用一个类实例(即组件)的方法。要知道,组件的所有方法都是在内部调用,或者作为生命周期方法被自动调用的。

因此,相比较 Class 组件,函数组件反而更适合去描述 Modal => View 的映射。但是函数组件没有 state,也没有 生命周期方法。

2)Hooks的诞生

为了解决 函数组件 没有 状态 和 监听状态变化,从而能够触发 函数组件的重新渲染。

  • 定义: 把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

Hooks的好处

  • 简化了逻辑复用 在 Class组件 中 要实现逻辑复用,必须借助于 高阶组件 等设计模式。

  • 有助于关注分离: 即 针对同一个业务逻辑的代码 尽可能的 聚合到一块。把业务逻辑清晰的分离开。 比如: useEffect() 集合了 Class组件中的 componentDidMount 、 componentWillUnmount、componentDidUpdate 三个生命周期得功能。

二、内置 Hooks:

内置Hook得特性如下: 各Hooks 得具体描述和用法,请参照官网

1)useEffect: 每次组件 render 完后判断依赖并执行。

执行时机:

  • 每次 render 后执行:不提供第二个依赖项参数。
useEffect(() => {});
  • 仅第一次 render 后执行:提供一个空数组作为依赖项
useEffect(() => {}, []);
  • 第一次以及依赖项发生变化后执行:提供依赖项数组
useEffect(() => {}, [deps]);
  • 组件 unmount 后执行:返回一个回调函数
useEffect(() => {
  return () => {}
}, []);

2)useCallback: 缓存回调函数。

用法:

const memoizedCallback = useCallback(fn, deps);

fn: 定义的回调函数

deps:依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。

举例说明问题:

import React , { useState } from 'react';

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

  const handleCountChange = () => setCount(count + 1);

  return <Button onClick={handleCountChange}>{count}</Button>
}

每一次的 UI 变化,都是通过重新执行整个函数来完成的。这和 Class组件 的区别在于:函数组件没有一个直接的方式在多次渲染之间维持一个状态。 这就导致了:即使 count 没有发生变化,但是函数组件 每次因为其他状态的变化而重新渲染时,都会创建一个新的事件处理函数 handleCountChange,从而导致,接收事件处理函数的组件,需要重新渲染。

上面例子的优化:

import React , { useState, useCallback } from 'react';

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

  const handleCountChange = useCallback(() => setCount(count + 1), [count]);

  return <Button onClick={handleCountChange}>{count}</Button>
}

3)useMemo:缓存计算结果

用法:

const memoizedValue = useMemo(fn, deps);

fn: 产生所需数据的一个计算函数

deps:通常来说,fn 会使用 deps 中声明的一些变量来生成一个结果,用来渲染出最终的UI

使用场景:如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,即依赖的数据发生变化的时候,才需要重新计算。

好处

  • 可以避免在用到的数据没有发生变化时进行重新计算。
  • 可以当数据未发生变化时,避免子组件的重复渲染。

举个例子:搜索关键词,展示用户列表

1、未使用 useMemo

import React, { useState, useEffect } from "react";

export default function SearchUserList() {
  const [users, setUsers] = useState(null);
  const [searchKey, setSearchKey] = useState("");

  useEffect(() => {
    const doFetch = async () => {
      // 组件首次加载时发请求获取用户数据
      const res = await fetch("https://reqres.in/api/users/");
      setUsers(await res.json());
    };
    doFetch();
  }, []);


  let usersToShow = null;
  if (users) {
    // 无论组件为何刷新,这里一定会对数组做一次过滤的操作
    usersToShow = users.data.filter((user) =>
      user.first_name.includes(searchKey),
    );
  }

  return (
    <div>
      <input
        type="text"
        value={searchKey}
        onChange={(evt) => setSearchKey(evt.target.value)}
      />
      <ul>
        {usersToShow &&
          usersToShow.length > 0 &&
          usersToShow.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}

2、使用 useMemo


//...
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
    if (!users) return null;
    return users.data.filter((user) => {
      return user.first_name.includes(searchKey));
    }
  }, [users, searchKey]);
//...

4)useRef:

1、在多次渲染之间共享数据

2、保存某个 DOM 节点的引用

用法

const myRefContainer = useRef(initialValue);

通过 myRefContainer.current 在多次渲染之间共享这个值。

举个例子 1、在多次渲染之间共享数据

import React, { useState, useCallback, useRef } from "react";

export default function Timer() {
  // 定义 time state 用于保存计时的累积时间
  const [time, setTime] = useState(0);

  // 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
  const timer = useRef(null);

  // 开始计时的事件处理函数
  const handleStart = useCallback(() => {
    // 使用 current 属性设置 ref 的值
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1);
    }, 100);
  }, []);

  // 暂停计时的事件处理函数
  const handlePause = useCallback(() => {
    // 使用 clearInterval 来停止计时
    window.clearInterval(timer.current);
    timer.current = null;
  }, []);

  return (
    <div>
      {time / 10} seconds.
      <br />
      <button onClick={handleStart}>Start</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
}

总结: 使用 useRef 保存的数据一般与 UI 的渲染无关,所以当 ref 的值发生变化时,不会触发组件的重新渲染。

2、保存某个 DOM 节点的引用


function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

总结:结合 React 的 ref 属性和 useRef(), 就可以获取到真实的 DOM 节点,并对这个节点进行操作。

3、自定义Hooks

1)定义:声明一个名字以 use 开头的函数,且在其内部使用了其他 Hooks。

其他 Hooks: 可以是内置 Hooks,也可以是其他自定义 Hooks。这样才能够让组件刷新,或者去产生副作用。

与 工具类 的区别比较

Hooks 可以管理当前组件的state,而在 工具类 中无法直接修改组件的 state,也就无法在数据改变的时候触发组件的重新渲染。 工具类 必须在数据改变后,调用setState 更新state之后 组件才会重新渲染。

2)三种典型的使用场景

  • 封装通用逻辑

    比如:发起异步请求获取数据并显示界面UI (请求成功、请求失败、loading 三种UI界面的显示)

  • 监听浏览器状态

    比如:

    • 界面需要根据窗口大小的变化重新布局
    • 在页面滚动时,需要根据滚动条的位置,来决定是否显示一个“返回顶部”的按钮
  • 拆分复杂组件

    尽量将相关的逻辑做成独立的 Hooks,然后再函数组件中使用这些 Hooks,通过参数传递和返回值让 Hooks之间完成交互。

这里拆分的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。

4、全局状态管理:Redux

特点

  • Redux Store 是全局唯一的:即整个应用程序一般只有一个 Store。

  • Redux Store 是树状结构:可以更天然地映射到组件树的结构,虽然不是必须的。

两个典型使用场景

  • 跨组件的状态共享:当某个组件发起一个请求时,将某个 Loading 的数据状态设为 True,另一个全局状态组件则显示 Loading 的状态。

  • 同组件多个实例的状态共享:某个页面组件初次加载时,会发送请求拿回了一个数据,切换到另外一个页面后又返回。这时数据已经存在,无需重新加载。设想如果是本地的组件 state,那么组件销毁后重新创建,state 也会被重置,就还需要重新获取数据。

React 与 Redux 是如何建立联系的呢?

主要是两点:

  • React 组件能够在依赖的 Store 的数据发生变化时,重新 Render;

  • 在 React 组件中,能够在某些时机去 dispatch 一个 action,从而触发 Store 的更新。

要实现这两点,就需要 react-redux 库 建立桥梁,实现 react 与 redux 的互通。

在 react-redux 的实现中,为了确保需要绑定的组件能够访问到全局唯一的 Redux Store,利用了 React 的 Context 机制去存放 Store 的信息。通常我们会将这个 Context 作为整个 React 应用程序的根节点。因此,作为 Redux 的配置的一部分,我们通常需要如下的代码:


import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

异步Action:通过 Middleware 机制来实现拦截器。在 reducer 处理 action 之前被调用。在这个拦截器中,可以自由处理获得的 action,无论是把这个 action 直接传递到 reducer,或者构建新的 action 发送到 reducer,都是可以的。 action -> Middleware -> reducer

总结:利用这个机制,Redux 提供了 redux-thunk 这样一个中间件,它如果发现接受到的 action 是一个函数,那么就不会传递给 reducer,而是执行这个函数,并把 dispatch 作为参数传给这个函数,从而在这个函数中你可以自由决定 何时,如何发送 action。

受控组件和非受控组件

受控组件:组件的展示完全由传入的属性决定。

比如: 在HTML中,标签<input><textarea><select>的值的改变通常是根据用户输入进行更新。在React中,可变状态通常保存在组件的状态属性中,并且只能使用 setState() 更新,而呈现表单的React组件也控制着在后续用户输入时该表单中发生的情况,以这种由React控制的输入表单元素而改变其值的方式,称为:“受控组件”。

总结受控组件值的变化 是通过 React 组件的 状态属性的更新实现的。

对于每一个表单元素,的受控组件处理都遵循两个步骤: 1、设置一个 State 用于绑定到表单元素的 value 2、监听表单元素的 onChange 事件,将表单值同步到 value 这个 state。

非受控组件: 表单数据由 DOM 本身处理。不受 React 组件的 state 控制。

比如: input 输入就显示最新的值。(可以使用 ref 获取 表单值)

用 Hooks 实现 维护整个表单的状态,并提供根据名字去取值和设值的方法,从而方便表单在组件中的使用。

import { useState, useCallback } from 'react';

const useForm = (initialValues = {}) => {
  // 设置整个 form 的状态 values
  const [values, setValues] = useState(initialValues);
  // 定义一个 errors 状态
  const [errors, setErrors] = useState({});

  // 提供方法 用于设置 form 表单上某个字段的值
  const setFieldValues = useCallback((name, value) => {
    setValues((values) => ({
      ...values,
      [name]: value
    }))

    // 如果存在验证函数,则调用验证用户输入
    if (validators[name]) {
      const errMsg = validators[name](value);
      setErrors((errors) => ({
        ...errors,
        // 如果返回错误信息,则将其设置到 errors 状态,否则清空错误状态。
        [name]: errMsg || null
      }))
    }
  }, [validators]);

  return { values, errors, setFieldValues }
}

5、React 中的事件处理机制

事件分为两种:

1、原生 DOM 事件:只要原生 DOM 有的事件,在 React 中基本都可以使用,只是写法上采用驼峰法。

React 原生事件的原理: 合成事件。

由于虚拟 DOM 的存在,在 React 中即使绑定一个事件到原生的 DOM 节点,事件也并不是绑定在对应的节点上,而是所有的事件都是绑定在根节点上。然后由 React 统一监听和管理,获取事件后再分发到具体的虚拟 DOM 节点上。

在 React17 之前,所有的事件都是绑定在 document 上的,而从 React17 开始,所有的事件都绑定在整个 App 上的根节点上(利用浏览器事件的冒泡模型实现根节点接收所有的事件),这主要是为了以后页面上可能存在多版本 React 的考虑。

具体来说,React 这么做的原因主要有两个。

  • 虚拟 DOM render 的时候,DOM 可能还没有真实的 render 到页面上,所以无法绑定事件。

  • React 可以屏蔽底层事件的细节,避免浏览器的兼容问题。同时,对于 React Native 这种不是通过浏览器 render 的运行时,也能提供一致的 API。

2、自定义事件:利用属性传递回调函数给子组件,实现事件的触发。是纯组件实现的一种机制。