likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 35

原文: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 的实现方式。

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

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

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

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

2) 首次渲染

首次运行组件函数。

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

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

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

3) 后续渲染

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

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

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

4) 事件处理

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

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

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

简单(naive)的实现

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

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

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 团队说你不应该做的事情:

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。让我们看看这会给程序带来多大的破坏。

不良组件的首次渲染

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

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

不良组件的第二次渲染

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

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

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

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

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

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

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

结论

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

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

转载自:https://juejin.cn/post/7263115061182824485
评论
请登录