React hooks:并不神奇,就是数组

1,100 阅读5分钟

原文地址: medium.com/@ryardley/r…
译文地址:github.com/xiao-T/note…
本文版权归原作者所有,翻译仅用于学习。

使用图解的方式揭开规范中的使用规则

我是新 API Hooks 的粉丝。然而,使用它有一些奇怪的约束。在这,我为那些想使用新 API,但是,又不理解规则的人,展示一个模型以便于更好的理解规则。

img

了解 hooks 的工作模式

我听到有些人对于那些 hooks API 的规范难以理解,因此,在表面上,我可以尝试揭示规范中的新语法是如何工作的。

Hooks 的规则

有两条使用规则是 React 核心团队明确规定的,在使用 hooks 时,你应该按照文档的约束来做。

  • 不要在循环内部、条件语句和嵌套方法中调用 Hooks
  • 只在函数组件中调用 Hooks

第二条不言而喻。为了函数组件有更强大的功能,你需要一些方法来实现。

然而,对于第一条,有点混乱,这有点不符合常规。这也是我想要解释的地方。

Hooks 通过数组管理 State

为了得到一个清晰明确的模型,我们来实现一个简单版本的 Hooks API。

请注意:这只是一种猜测,为了演示我是如何理解的 Hooks,唯一一种实现方式。这并不是 Hooks 内部真正的实现方式。

如何实现 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 方法,然后,这个 setter 可以控制 hook 返回的 state。

React 如何处理的呢?

让我们来一一解释 React 内部是如何工作的。以下的内容将会在特定组件的上下文环境中执行。也就是说,在组件被渲染后数据存储在组件外面一层。这些数据并不会和其他组件共享,但是,在后续的渲染中可以被特定的组件的访问。

1)初始化

创建两个空数组:settersstate

设置指针为 0

2)第一次渲染

执行函数实现组件的第一次渲染。

第一次渲染时,每次执行 useState(),添加一个 setter 方法(绑定指针的位置)到 setters 数组中,然后,添加相应的 state 到 setate 数组中。

3)后续渲染

后续的每次渲染指针都会被重置,然后读取相应的数据。

4)事件处理

每个 setter 对应指针的位置都会有一个引用,因此,任何 setter 的调用都会触发 state 数组中相应位置数据的变化。

最后简易版本的实现

接下来的代码只是演示如何实现的。

注意:这并不代表所有的 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

为什么顺序非常重要

现在,如果我们根据一些外部的数据或者组件 state 改变组件中 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 在条件语句中执行。我们来看看这会给系统带来什么问题。

不良组件的第一次渲染

这个时候,我们的变量 firstNamelastName 都有正确的数据,但是,让我们看看在第二次渲染时会发生什么。

不良组件的二次渲染

现在,firstName and lastName 都被设置成了 “Rudi”,这就让 state 变的不一致了。这明显是个错误,也不能工作。但是,正好可以让我们理解为什么 React Hooks 会存在那些规则了。

React 团队之所以明确使用规则,这是因为如果不遵守规则会导致数据的不一致

想象一下 hooks 是通过数组维护的,你就不会打破规则了

现在,应该清楚的知道了为什么不能在条件语句或者循环中调用 hooks 了。因为,我们通过数组指针来维护 setterstate 的对应关系,如果,在渲染时改变了调用顺序,指针就不能正确的匹配 setterstate

因此,我们需要关心的是:将管理业务的 Hooks 视为一组指针无变化的固定数组。如果,你这么做了,一切将会正常。

总结

希望我已经整理出了一个更加清晰明确的模型,可以更好的理解新 Hooks API 底层是如何工作的。记住真正的值是关联在一起的数组,因此要多注意它们之间的顺序,使用 hooks API 将会得到更高的好处。

Hooks 是 React 组件有效的 API。人们喜欢它是有原因的,如果,你打算用这类模型存储 state,那么你就不要违反使用规则。