React Hooks 从入门到放弃(一)

avatar
@腾讯科技(深圳)有限公司

本文默认读者对 React Hooks 已经有一定的了解。 因此不再赘述 Hooks API 的使用了, 还未了解的同学可以去官网阅读React Hooks API Reference

React Hooks 基础

使用 React Hooks 就不得不提到 React 函数组件。

Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen.

从官网上的介绍来看, 组件就像 JavaScript 的函数, 它接收一些入参(props), 并返回 React 元素。

在我们最初学习使用 React 框架时, 首先了解的是通过Class书写组件, 这样可以很好地理解并规划类组件的数据与方法, 而且生命周期可以让我们更清晰地了解组件的加载以及更新状态的触发时机。

函数组件呢? 函数组件本身解决了从数据到 React 元素的映射, 而Hooks则在此基础上提供了数据存储以及 Side Effect(副作用)的处理。

此时此刻, 让我们忘记已经熟知的Class生命周期, 来重新认识函数组件 + Hooks的 React。

0. 函数组件

函数组件是用来处理一些简单的 UI 组件, 通过 Props 传入的数据, 抽象封装一些组件。

function Header(props) {
    const { title, description, avatar } = props;
    return (
        <div>
            <img src={avatar} />
            <h1>{title}</h1>
            <p>{description}</p>
        </div>
    );
}

限制了函数组件使用的原因一方面在于它没有内部数据, 另一方面就是没有生命周期, 因此无法实现具有一定交互与逻辑的功能。

1. 使用 React Hooks 时的数据存储

函数组件内定义的变量不是固化的, 执行完成后, 函数内的变量就会被清理掉了(非闭包情况)。所以, 我们需要通过 Hooks 固化组件需要的一些数据。

Edit on CodeSandbox

上面是 React Hooks 之 useState 的使用例子, 如下是浅尝辄止的理解:

通过useState, 我们在某个地方定义了一个对象, 并挂载到某个不会让它消失的地方。useState方法返回的第一个值就是我们要的数据。useState方法返回的第二个值是一个函数, 可以设置这个对象的值, 然后会触发函数重新渲染。

上面这坨话说的很模糊, 某个地方到底是哪个地方, 定义了一个什么样的对象, 又挂载到了什么地方。设置了新值后又是怎么触发渲染的???

如果想从根源上填上这个坑, 大概是需要阅读源码了解 Hooks 的基础实现的Github 源码链接

在了解Hook加载前还是要了解一下 Hook 的定义和存储的。

下面是Hook的 type 定义

export type Hook = {
  // 数据
  memoizedState: any,
  // 下面这些暂时不需要了解
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  // 链表下一个节点的指针
  next: Hook | null,
};

我们需要了解的是Hook是通过链表存储的

本文尝试通过流程图来简述下这里的逻辑。(下面的流程图是不完整的!没有对比依赖更新)

  • 函数组件第一次执行时

第一个 Hook 会挂载到React Fiber Node上, 之后在函数执行遇到的 Hook 会依次按顺序挂载到 Hook 节点后。

  • 函数组件更新渲染时

函数执行遇到的 Hooks 会按照顺序读取React Fiber Node上的 Hook 节点。

如上图所示, 函数组件重新执行时是依赖 Hooks 队列的顺序的。如果在条件判断中使用 Hook 就会让这个队列错乱。

因此也就会有如下官网提示:

Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.

2. 使用 React Hooks 处理副作用

副作用(Side Effect), 个人理解, 是在数据->视图的转化过程中, 出现的一些特别的时机。我们可以利用这些时机去处理一些业务逻辑。

以下是使用useEffect的一个简单的计数 Demo.

Edit on CodeSandbox

使用useEffect去实现在渲染完成之后要去处理的一些事情。 大家也可以在官网的 API Reference 中找到 React useEffect 说明。

我们可以通过流程图加深理解useEffect这个 Hook。

以上是useEffectHook 的大致执行逻辑, 通过此图会发现useEffect多了一步数据比对的过程, 只有当以下条件成立时, SideEffect 才会触发调用。

  1. useEffect 第一次执行之时
  2. useEffect 的依赖存在变更之时

我们修改上面计数的例子就可以更好的理解两个 Hook。

Edit on CodeSandbox

function Demo(props) {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    const timer = setTimeout(() => {
      setCount(step + count);
    }, 1000)
    return () => {
      clearTimeout(timer);
    }
  }, [count]);

  return (
    <div>
    <div>Count: {count}</div>
    <div>Step: {step}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click
      </button>
      </div>);
}

在 Gif 图中, 在更新 Step 步长时, 计数第一次仍然是+1, 随后才会+2

  1. step更新时, useEffectcount没有变化, 所以没有触发更新
  2. useEffect触发时, 使用的是 Hook 中暂存的数据, 所以步长step仍然是 1.

我们在 Hook 加载流程上补充上对比的过程。

Hook中存入的是上一次渲染后的快照值, 所以在执行时, 也会使用上一次渲染留下来的值。

到这里, 我们概括下函数组件的渲染流程

3. 总结

本文只是提到了useState, useEffect这两例典型的 React Hooks。

useState为例, 我们了解了React Hooks是如何解决函数组件内无法保留内部数据的问题。

useEffect为例, 我们呢了解了React Hooks如何处理在数据->视图之间一些时机的处理

以及很浅显的提到了Hook中的数据暂存(快照)的原因

React Hooks 还有很多种用法, 本章只是梳理 React Hooks 的一些基本概念。

PS: 下一章会着重于《如何在业务中梳理重组封装自己的 Hooks》

4. 参考 Reference

React Hook 解析 - 风吹老了好少年的文章 - 知乎