【译】ReactHooks-并非黑魔法,本质是数组

前言

本文翻译自 React hooks: not magic, just arrays,该文章很好的阐释了 react hooks 原理,我将保持原文的意思进行翻译,如有自己感觉不清楚的地方将会在括号中注明。以下为翻译内容。

我是 hooks api 的忠实粉丝。然而 hooks 在使用时却有一些奇怪的约束。在这里,我给尝试去理解这些规则由来的人提供一个了模型,用于思考如何去使用这些新 api。

hooks 的规则

hooks proposal documentation 的提纲中,React 核心团队规定了两条开发者在使用 hooks 时需要遵守的主要规则:

  • 不要在循环,条件或者是嵌套函数中使用 hooks
  • 只能在 React Functions 中才能使用 hooks

后者我认为是显而易见的。要将行为附加到函数式组件上,你需要能够以某种方式将该行为与组件关联起来。(原文为: To attach behaviour to a functional component you need to be able to associate that behaviour with the component somehow. 不太理解作者想表达的意思)

前者我认为会比较让人困惑,因为像这样使用 api 可能看起来不太自然。而这正是我今天想要探讨的内容。

hooks 的状态管理都是基于数组

为了更清晰地理解心智模型,让我们来看一下如何实现一个简单的 hooks api。

注意这只是 API 的一种可能的实现方式及猜想,用于让你理解,而非 API 真正的内部实现方式

我们如何实现 useState?

让我们打开一个实例来演示如何实现一个 state hook。

首先,我们定义一个组件:

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi");
  const [lastName, setLastName] = useState("Yardley");

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

这个 Hooks api 背后的思想是,你可以用 hook 函数返回的数组的第二个元素作为 setter 方法,这个 setter 方法可以控制被 hook 管理的状态。

那么 React 是如何做到这个的呢?

让我们看看这个在 React 内部是如何运作的。以下内容将在特定组件的上下文中运行。这就是说数据存储在要渲染的组件的上一层。这个 state 不与其他组件共享,但是维护在一个可以用于该组件后续渲染的作用域内。(原文:The following would work within the execution context for rendering a particular component. That means that the data stored here lives one level outside of the component being rendered. This state is not shared with other components but it is maintained in a scope that is accessible to subsequent rendering of the specific component)

  1. 初始化

    创建两个空数组:setterstate

    将指针设置为 0

    Image for post

  2. 首次渲染

    首次运行该组件。

    每次调用 useState ,如果是首次运行,则 push 一个 setter 方法(绑定到指针的位置)到 setters 数组中,并且 push 一些 state 到 state 数组中。

    Image for post

  3. 后续渲染

    每次后续渲染,指针将被重置,然后从各个数组中读取对应的值。

    Image for post

  4. 事件处理

    每一个 setter 都保存一个对应的指针位置的引用,因此触发任意一个 setter 都可以修改 state 数组中对应指针位置的 state。

    Image for post

简单的实现(And the naive implementation)

下面是简单的代码演示:

注意:这不代表 hooks 的实际实现,但是应该能给你一个理解 hooks 工作原理的思路。这就是我为什么使用块级变量的原因(原文: That is why we are using module level vars etc)

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}

// This is the pseudocode for the useState helper
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];
}

// Our component code that uses 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>
  );
}

// This is sort of simulating Reacts rendering cycle
function MyComponent() {
  cursor = 0; // resetting the cursor
  return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']

// click the 'Fred' button

console.log(state); // After-click: ['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,让我们看看造成了什么样的破坏:

坏组件的第一次渲染

Image for post

在这个时候,我们的实例变量 firstNamelastName 指向的正确的数据,但是我们看看第二次渲染发生了什么:

坏组件的第二次渲染

Image for post

现在我们的 state 存储出现异常, firstNamelastName 都指向 “Rudi”。上述明显错误的操作给了我们一个思路:为什么 hooks 要如此规定。

The React team are stipulating the usage rules because not following them will lead to inconsistent data

想想 hooks 操作一系列的数组,那么你就不会打破规则了(Think about hooks manipulating a set of arrays and you wont break the rules)

现在应该能很清晰理解为什么不能在条件或者循环语句中使用 use hook 了:因为我们使用了指针指向了数组,如果在 render 的时候改变了顺序,那么指针就不能对应正确的数据,所以调用 use 也不会指向正确的数据和 setter。

因此,诀窍就是将 hooks 想像成用恒定的指针来管理一系列数组。如果能做到这个,那么一切就能按预期进行。

结论

希望我已经就如何思考 hooks api 的工作原理指出了一个清晰的心智模型。(后面就偷个懒不翻译了,基本是上述观点的重复强调。)

Hopefully I have laid out a clearer mental model for how to think about what is going on under the hood with the new hooks API. Remember the true value here is being able to group concerns together so being careful about order and using the hooks API will have a high payoff.

Hooks is an effective plugin API for React Components. There is a reason why people are excited about this and if you think about this kind of model where state exists as a set of arrays then you should not find yourselves breaking the rules around their usage.