发布于 2026-01-06 7 阅读
0

React Hooks 揭秘 我的第一本书

React Hooks 揭秘

我的第一本书

封面图片由 Miguel Discart 提供,来自 Flickr

在 ReactConf 大会上,React 团队提出了一种使用 React 实现交互式组件的新方法,称为hooks

他们发布了一份RFC,以便 React 开发者可以讨论这是否是一个好主意。

本文将探讨如何实现这种功能。

什么

Hooks 是可以在函数式组件内部调用的函数,用于获取通常只有组件类才能提供的功能。

为什么

hooks 的基本思想是简化 React 开发,但我不会详细介绍,你可以从 React 核心开发者 Dan Abramov 的文章中了解更多信息(链接在此)

免责声明

请先阅读文档

这是 React 的一个ALPHA功能,不应该在生产代码中使用。

在这篇文章中,我们不会使用 React,而是用几行代码来说明 hooks 的工作原理。

如何

很多人认为 Hooks 很神奇,违背了 React 的设计理念,我也能理解他们的想法。如果我们看一下示例,它并没有清楚地说明发生了什么。

import React, {useState} from "react";

function CounterButtton(props) {
  let [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

它使用一个简单的函数调用useState,设法获取当前状态,并允许我们更改它,并使用新值重新渲染组件。

JavaScript 高手一眼就能看出罪魁祸首:全局变量!

如果该useState函数不修改调用堆栈来访问我们的调用组件函数,则必须将数据存储在全局范围内。

如果你读了丹的文章,你可能会看到这条推文:

  1. JavaScript 是单线程的,如果有人在调用我们的钩子函数之前清除了全局变量,我们将写入一个新的全局变量,只要我们只进行同步调用,在我们的函数运行时,其他人就无法执行任何操作。
  2. React 会调用我们的函数式组件,因此它可以控制调用前后发生的事情。

钩子示例

下面我尝试写一个简单的例子来说明如何实现 Hooks 的“魔力”。这与 React 官方的实现无关,而是为了展示这个概念的工作原理。

首先,我们来定义一下组件:

function NumberButton() {
  let [valueA, setValueA] = useState(0);
  let [valueB, setValueB] = useState(100);

  return {
    type: "button",
    props: {
      children: `A:${valueA} B:${valueB}`,
      onClick() {
        setValueA(valueA + 1);
        setValueB(valueB - 1);
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

NumberButton函数调用另一个useState函数,该函数与 React 函数具有相同的接口useState

它返回一个对象,该对象定义了一个<button>包含一些文本和处理程序的元素。

将所有内容渲染到 DOM 中的函数如下所示:

function run(components, target) {
  let savedHooks = new Map();
  render();

  function render() {
    target.innerHTML = "";
    components.forEach(function(component) {
      globalHooks = savedHooks.get(component);

      if (!globalHooks) globalHooks = new Map();

      renderToDom(component, target);

      for (let [id, hookData] of globalHooks.entries()) {
        hookData.calls = 0;
        hookData.render = render;
      }

      savedHooks.set(component, globalHooks);

      globalHooks = null;
    });
  }
}

function renderToDom(component, target) {
  let { props, type } = component();

  let element = document.createElement(type);
  element.innerHTML = props.children;
  element.onclick = props.onClick;
  target.appendChild(element);
}
Enter fullscreen mode Exit fullscreen mode

它接受一个组件数组和一个 DOM 元素作为渲染目标。

为了保持简洁,它只能渲染扁平化的组件列表,不支持嵌套。它也不进行任何 DOM 差异比较。

  1. 它会创建一个局部变量savedHooks来存储所有钩子的状态。
  2. 它调用本地render函数进行实际渲染。
  3. render函数清除targetDOM 元素并遍历数组components
  4. 神奇之处在于:在组件函数被使用之前,变量globalHooks会被覆盖,覆盖的可以是上次运行中已存储的数据,也可以是新创建的Map对象。
  5. 组件会执行其功能,例如调用useState函数。
  6. hookData我们的组件存储的调用会获取useState对本地函数的引用render,以便它可以启动重新渲染,并且其calls属性会被重置。
  7. 数据globalHooks将保存到本地,以供下次运行。
  8. 如果globalHooks设置了null,则如果存在下一个组件,它将无法globalHooks再通过访问我们的数据。

实际的钩子函数如下所示:

let globalHooks;
function useState(defaultValue) {
  let hookData = globalHooks.get(useState);

  if (!hookData) hookData = { calls: 0, store: [] };

  if (hookData.store[hookData.calls] === undefined)
    hookData.store[hookData.calls] = defaultValue;

  let value = hookData.store[hookData.calls];

  let calls = hookData.calls;
  let setValue = function(newValue) {
    hookData.store[calls] = newValue;
    hookData.render();
  };

  hookData.calls += 1;
  globalHooks.set(useState, hookData);

  return [value, setValue];
}
Enter fullscreen mode Exit fullscreen mode

让我们一步一步来:

  1. 它获取一个defaultValue应该在第一次调用时返回的值。
  2. 它尝试从变量中获取上次运行的状态globalHooks。这是一个Map由我们的run函数在调用组件函数之前设置的对象。它要么包含上次运行的数据,要么我们需要创建自己的数据hookData
  3. hookData.store数组用于存储上次调用时的值,该hookData.calls值用于跟踪我们的组件调用此函数的次数。
  4. 我们hookData.store[hookData.calls]可以获取此调用存储的最后一个值;如果该值不存在,则必须使用defaultValue.
  5. 回调函数setValue用于更新我们的值,例如在点击按钮时。它会获取一个引用,calls以便知道它属于哪个函数调用setState。然后,它使用函数hookData.render提供的回调函数render来启动所有组件的重新渲染。
  6. 计数器hookData.calls加一。
  7. hookData值存储在变量中,因此组件函数返回后,globalHooks该变量可以被后续函数使用。render

我们可以像这样运行示例:

let target = document.getElementById("app");
run([NumberButton], target);
Enter fullscreen mode Exit fullscreen mode

您可以在 GitHub 上找到包含示例组件的可运行实现。

结论

虽然我采用的实现方法与实际的 React 实现相去甚远,但我认为这表明 hooks 不是疯狂的开发魔法,而是一种巧妙地使用 JavaScript 约束的方法,你可以自己实现它。

我的第一本书

最近几个月我写博客的频率不如以前了。那是因为我写了一本关于学习 React 基础知识的书:

从零开始的 React 书籍横幅

如果你喜欢通过拆解来理解 React 的工作原理,那么你可能会喜欢我的书《React From Zero》。在书中,我通过分析组件的工作原理、元素的渲染方式以及如何创建自己的虚拟 DOM 来剖析 React 的运作机制。

您可以点击此处免费下载第一章

文章来源:https://dev.to/kayis/react-hooks-demystified-2af6