译 React hooks: 不是魔法,只是数组

使用图来揭开这个提议的规则

引言

原文地址 medium.com/@ryardley/re

我是新的 API 的忠实粉丝。然而,它添加了一些奇怪的约束关于你需要如何去用它。

这里我演示了一个模型,给那些想使用新的API 但是不能理解加的这些规则的原理的人。

注意: Hooks 还在实验中

这个文章是关于目前还是实验版的 Hooks API。 你可以在这里找到稳定的 React API 文档

解析 Hooks 是如何工作的

我听到一些人对新的 Hooks API 草案在苦苦思考里面的“魔法”,所以我打算至少从表面层面开始分析语法是如何工作的。

Hooks 的规则

有两个主要的使用规则是 React 核心团队规定你在使用 hooks 的时候需要去遵守的,具体在概述里面

  • 不要在循环,条件判断,嵌套函数里面调用 Hooks
  • 只在 React 的函数里面调用 Hooks

后者我认为是显而易见的。把这些行为添加给了函数组件,所以你需要能够按照函数方式去把这些行为跟组件关联。

前者我认为可能是让人困惑的,使用这样的 API 似乎是对程序来说不自然的,这就是我今天需要探索的。

状态管理都是关于数组

为了得到一个更清晰的思维模型,让我看看简单的 hooks API 接口应该是什么样子的

请注意这只是个推测,只有一种可能的方式去实现 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 里面返回使用第二个素组元素作为设置函数作为,这设置函数会控制由hook 管理的状态。

所以 React 将要用这个去做什么呢?

让我们解释下在 React 内部,这个是如何工作的。在执行 context 去渲染一个特殊组件的时候,下面的步骤会工作。这意味着在组件正在被渲染的时候,数据是被存储在组件外面的。状态不会在其他组件之间被分享,但是可以在一个的特殊作用域内被维护,这个作用域可以被起特殊渲染作用的子组间读取。

1)初始化 initialisation

创建两个空素组 setters 和 state

设置游标(cursor)为 0

2)第一次渲染

第一运行组件函数

每个 useState() 调用。在第一次运行的时候,会把一个设置函数(跟游标位置绑定的)放到 setters 数组,接着把一些状态放到 state 数组里面。

3)后续渲染

每个后面被渲染的时候,游标都会被重置,这些值只是从每个数组中读取

4)事件处理

每个 setter 有个引用指向右边的位置,通过触发调用任何 setter,它都会改变在状态数组里面对应的状态值。

底层的实现

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,让我们看它在系统上造成的破坏

糟糕的组件第一次渲染

我们的实例变量 firstName 和 lastName 包含了正确的数据,让我们看看第二次渲染发生的事情

糟糕组件的第二次渲染

first name 和 lastName 都被设置成了 “Rudi” 在我的状态存储中,变得不一致,这个很明显不正确,也不工作,但是给了我们一个想法为什么 hook 的规则是按照它要求的被制定。

React 团队执行了这个使用规则,是因为不准守就会导致不一致的数据


思考 hooks 维护了一系列数组,所以你不能打破规则

所以现在很清楚了为什么你不能在条件判断和循环里面调用 hooks,因为我们处理了游标指向一系列数组,如果你改变了内部的调用顺序,游标就不会匹配到正确的数据, 你的调用就不会指向正确的数据句柄。

所以这个技巧就是思考 hooks 的管理,需要一个一致的游标去管理一系列的数组。如果做到了任何写法都会能工作。

总结


有希望的是我们制定了一个清晰的思维模型,去思考在 hook 这个新的 API 底层做了什么事情。 思考真实的值能够把关注点聚到一起去小心这个顺序,这样在使用 API 的时候会有更好的效果。

Hooks 是一个高效的 API 插件对 React 组件来说。这就是为什么大家都很兴奋,如果你知道这个模型里面状态是一系列可以被设置的数组,那么你就会发现自己在使用的时候不会打破规则。

之后我打算看一下 useEffects 方法,尝试把它跟 React 组件的生命周期方法去比较。


这个文章是一个在线文档,如果你想贡献或者返回任何错误,请联系我 Twitter @@rudiyardley 或者 github a href="github.com/ryardley">@ryardley

编辑于 2018-11-02

文章被以下专栏收录