[React Hooks]没有魔法,只是数组

346 阅读5分钟

原文链接:medium.com/@ryardley/r…

我非常喜欢新的Hooks API。但是,它使用起来有一些奇怪的约束。在这里,我提供了一个模型,帮助那些正在考虑如何使用这个新API的人理解这些规则背后的原因。

解密Hooks如何工作的

我听到有些人对Hooks里面的‘魔法’实现有一些争议。所以我想尝试粗浅的向大家解密一下这个新语法是如何工作的。

Hooks的规则

React核心团队在文档中有提到使用Hooks的两个重要规则:

  • 不要在循环、条件语句或者嵌套函数中使用Hooks
  • 只可以在函数式组件中调用Hooks

我认为后者是不言而喻的。为了在函数式组件中关联行为,你需要通过某种方式将行为和组件进行关联。

但是,我认为前者可能会造成混淆,因为使用这样的API进行编程似乎不自然,这就是我今天要探讨的内容。

状态管理在Hooks中都与数组有关

为了有一个更清晰的理论模型,让我们看下Hooks的一个简单实现方式是什么样的。

请注意,这只是推测,也是实现API的一种可能方式,以展示你可能想如何实现。 API内部不一定是这样的工作方式。

如何实现useState

让我们来看一个示例,演示一下Hooks可能通过怎样的实现方式工作的。

首先我们写一个组件:

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

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

Hooks API背后的想法是,你可以使用setter函数作为hook函数的第二个数组项返回,并且setter将控制由hook管理的状态。

那么React打算如何做呢?

让我们注释一下React内部可能如何工作的。下面的例子会在执行上下文中渲染一个特定的组件。这意味着此处存储的数据位于要渲染的组件之外的一层。此状态不与其他组件共享,而是保持在当前作用域中,可以在下一次渲染组件时候访问。

  1. 初始化 创建两个空数组:settersstate

设置游标为0

  1. 第一次render 第一次执行这个函数式组件

第一次执行的时候调用每个useState(),将setter函数(和当前下标关联)push到setters数组然后push state到state数组。

  1. 下一次渲染 每当下一次渲染的时候游标都会被重置然后这些values就是从数组中获取。

  2. 事件处理 每一个setter指向数组中下标所在位置的引用,所以每一次调用任何setter都会改变state数组中这个位置的value。

一个简单的实现

以下是一个简单的代码示例,以演示该实现。

注意:这不代表hooks内部的真实实现,而是展示一个例子帮助说明hooks在一个组件中如何工作的。这也是为什么我们使用模块级别的变量的原因。

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']

为什么顺序非常重要

现在,如果我们根据某些外部因素甚至组件状态更改渲染周期的挂钩顺序,会发生什么情况?

让我们试一下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变量是正确的,但是我们来看看接下来第二次渲染会发生什么:

错误组件第二次渲染

现在firstNamelastName都被设为了'Rudi',我们的state存储的数据变得不一致了。这显然是错误的并且无法正常工作,但是它让我们知道为什么hooks有这个限制规则。

React团队规定hooks的使用规则就是为了保证不会出现这种数据不一致的问题。

考虑到hooks是在操作一个数组那么你就知道为什么不要打破这个规则

所以现在你应该清楚为什么不可以在条件语句或者循环中调用hooks。因为我们在处理数组的下标,如果你改变了hooks在render时候的调用顺序,这个下标将不会匹配到准确,所以你不会访问到正确的数据或者handles。

因此,诀窍是考虑将hooks作为一组需要一致游标的数组来管理其业务。如果执行此操作,则一切正常。

结论

希望我已经为如何思考新的hooks API在幕后发生的事情奠定了更清晰的思维模型。请记住,这里的真正价值在于能够将关注点归类在一起,因此在使用hooks API时如果能够小心顺序的问题则会带来很高的回报。