【译】React hooks:不是黑魔法,仅仅是数组

原文:React hooks: not magic, just arrays

by:Rudi Yardley

我是 hooks API 的超级粉丝。不过,它在使用上有一些奇怪的限制。本文我将为那些难以理解这些限制的人提供一个使用 hooks API 的思考模型。

解读 hooks 的工作原理

我听说有些人对 hooks API 提案中的"黑魔法"感到困惑,所以,我想至少从表面上解读一下该提案是如何工作的。

hooks 的规则

React 核心团队在hooks 提案文档中概述了使用 hooks 需要遵循的两条主要规则。

  • 不要在循环语句、条件语句或嵌套函数内调用 hooks
  • 只在 React 函数中调用 hooks

我认为后者是不言自明的。要在函数组件上附加行为,就得以某种方式将行为与组件联系起来。

不过,我认为前者可能会让人感到困惑,因为得按照这种规则使用 API 似乎不太合乎常理,这正是我今天要探讨的主题。

hooks 中的状态管理与数组有关

为了获得更清晰的思维模型,让我们来看看一个简单得 hooks API 实现。

请注意,这只是一种推测,只是展示你的思考方式的一种可能的实现方式。这并不一定是 API 的内部工作方式。

如何实现 useState()

接下来我们将举例说明状态 hook 的实现方式。

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

js 复制代码
function RenderFunctionComponent() { 
    const [firstName, setFirstName] = useState("Rudi");
    const [lastName, setLastName] = useState("Yardley");      
    
    return (
        <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    );                                                        
}

hooks API 背后的理念是:hook 函数返回一个数组,数组的第二项是一个 setter 函数,你可以使用该函数管理状态。

那么,React 是如何做到的呢?

让我们看一下 React 内部是如何工作的。以下代码运行在特定组件的渲染上下文中。这意味着,这里存储的数据(状态)位于正在渲染的组件外。该状态不会与其他组件共享,但可以在该组件的后续渲染中访问。

1) 初始化

创建两个空数组:settersstate

设置索引为 0

初始:两个空数组,索引为 0

2) 首次渲染

首次运行组件函数。

首次运行时,每次调用 useState() 都会将一个 setter 函数压入 setters(压入索引位置),然后将一些状态压入 state 数组。

首次渲染:状态和 setter 函数被压入数组,同时索引自增

3) 后续渲染

后续的每一次渲染,索引值都将重置,然后从数组中分别读取状态和 setter 函数。

后续渲染:读取状态和 setter 函数,同时索引自增

4) 事件处理

每个 setter 函数都有指向其索引的引用,因此只要调用任意 setter,就会改变 state 数组中对应索引位置状态的值。

setter 会"记住"它的索引,并根据索引设置状态

简单(naive)的实现

下面是一个简单的代码示例。

注:以下并不代表 hooks 的工作方式,但它应该能让你对 hooks 如何在单个组件中工作有一个很好的概念。这也是我们使用模块级变量的原因。

js 复制代码
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];
}

// 使用 hooks 的组件
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>
  );
}

// 这是在模拟 React 渲染周期
function MyComponent() {
  cursor = 0; // 重置索引
  return <RenderFunctionComponent />; // 渲染
}

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

// 点击 "Fred" 按钮

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

为什么顺序这么重要

现在,如果我们根据一些外部因素甚至组件的状态改变渲染周期内 hooks 的顺序,会发生什么?

让我们来做 React 团队说你不应该做的事情:

js 复制代码
let firstRender = true;

function RenderFunctionComponent() {
  let initName;
  
  if(firstRender){
    [initName] = useState("Rudi");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("Yardley");

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

我们在条件语句中调用了 useState。让我们看看这会给程序带来多大的破坏。

不良组件的首次渲染

渲染一个"不良" hook,该 hook 在下次渲染就会消失

不良组件的第二次渲染

如果在渲染之间移除 hook,就会出现错误

现在,由于存储的状态变得不一致,firstNamelastName 都被设置为 "Rudi",这显然是错误的,并且无法工作,但它让我们明白了为什么 hook 要那样规定。

React 团队之所以要设置使用规则,是因为不遵守这些规则会导致数据不一致。

想想 hooks 如何操作数组,你就不会违反规则了

所以,现在你应该清楚为什么不能在条件或循环中调用 hooks 了。因为我们处理的是指向数组的索引,如果你在渲染过程中改变调用的顺序,索引将无法匹配数据,你的 hooks 调用将无法指向正确的状态或 setter。

所以诀窍是把 hooks 看作是需要一致索引的数组。如果你这样了,一切都将正常工作。

结论

希望我已经为 hooks API 的底层工作机制提供了更清晰的思维模式。请记住,这里的真正价值是将关注点组合在一起,因此要小心顺序,使用 hook API 将带来很高的回报。

Hooks 是 React 组件的一个有效的插件 API。人们对它感到兴奋是有原因的,如果你考虑这种状态以数组形式存在的模型,那么你应该不会违反它们的使用规则。

相关推荐
浮华似水几秒前
Yargs里的Levenshtein距离算法
前端
_.Switch34 分钟前
Python Web 架构设计与性能优化
开发语言·前端·数据库·后端·python·架构·log4j
libai36 分钟前
STM32 USB HOST CDC 驱动CH340
java·前端·stm32
Java搬砖组长1 小时前
html外部链接css怎么引用
前端
GoppViper1 小时前
uniapp js修改数组某个下标以外的所有值
开发语言·前端·javascript·前端框架·uni-app·前端开发
丶白泽1 小时前
重修设计模式-结构型-适配器模式
前端·设计模式·适配器模式
程序员小羊!1 小时前
UI自动化测试(python)Web端4.0
前端·python·ui
破z晓1 小时前
OpenLayers 开源的Web GIS引擎 - 地图初始化
前端·开源
维生素C++2 小时前
【可变模板参数】
linux·服务器·c语言·前端·数据结构·c++·算法
vah1012 小时前
python队列操作
开发语言·前端·python