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

1,075 阅读6分钟

图解hooks的规则

我是新的hooks API的超级粉丝。然而,在使用hooks的时候有一些奇怪的限制。在这里,我将为那些对于理解这些规则有困难的人,呈现一个模型来解释如何去思考新的API。

Photo by rawpixel.com from Pexels

揭秘hooks是如何工作的

我听闻有些人对于新的hooks API的“魔力”感到非常困惑,因此我想我会至少在使用级别揭秘这个语法是怎么工作的。

hooks的原则

对于hooks的使用,React核心团队规定了两条使用规则,这两条规则列在了文档Hooks规则中。

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

后一条规则,我想是显而易见的。为了将这些功能添加到(React)函数组件中,就必须要将这些功能与(React)组件联系起来。 然而,前一条规则,我想可能是比较令人疑惑的。因为对于使用一个像这样的API来说,这条规则似乎不太常见。这也正是,我今天想要探索内容。

在hooks中的状态的管理全是关于数组的

为了获取到一个更加形象的模型,让我们来看下hooks API的一个简单的使用是什么样的。

请注意这只是猜测,也只是使用API的一种方式,用来展现关于hooks,你可能想知道的。这并不一定是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背后的思想是:你可以使用一个setter函数作为hook函数返回值的数组的第二个元素。这个setter函数会控制被hook管理的state状态值。

所以React将会对这些做什么呢?

让我们来解释下这些在React的内部可能是怎么工作的。接下来的执行过程是在渲染一个特定组件的执行上下文中运行。这意味着这里存储的数据在渲染组件的外级存放。这个state不与其他组件共享,只维护在一个作用域中,在这个特定组件二次渲染的时候可以获取得到。

1)初始化

创建两个空的数组:settersstate

设置光标为0:

初始化:两个空的数组,光标是0

2)首次渲染

第一次运行这个函数组件。 每个useState调用,首次运行的时候,都会将一个setter函数(绑定着一个光标位置)推进setters数组中,然后将一些state状态值推进state数组。

首次渲染:随着光标增加,每一个元素被写入数组

3)二次渲染

每个二次渲染,光标都会被重制,那些值正是从每一个数组中去读取。

二次渲染:随着光标增加,值从数组中读取

4)事件处理

每一个setter跟它的光标位置之间都有一个对应关系。因此任一一个setter被触发调用,在state数组中,相应位置的state的值就会被改变。

Setters会“记住”他们的位置索引值,并据此设置内存值

简单的模拟

这里是一段代码示例来论证执行过程。

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

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

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];
}
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>
    )
}

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在条件语句中被调用。让我们来看下这将会对这个系统造成哪些破坏。

有问题组件的首次渲染

渲染一个二次render时不执行的额外的有问题的hook

此时我们的例子中的变量firstNamelastName包含着正确的数据。但是让我们来看下第二次渲染的时候发生了什么:

有问题组件的第二次渲染

在渲染之间移除了一个hook,我们得到了一个错误

当下firstNamelastName都被设置为了Rudi,因为我们的state的存储变得不一致了。这显然是错误的、并且不能正常工作的。但是这也给了我们一个解释,为什么hooks的原则要被设置成那样。

React团队规定这些规则,因为不遵循这些规则将会导致数据的不一致性

设想hooks是正在操作一系列的数组,你将不会违反这些规则

所以现在应该很明确了,为什么你不能在条件或者是循环中调用hooks。因为我们正在处理一个指向一系列数组的光标。如果你在render之间改变调用的顺序,光标将无法与数据相匹配,同时你的use调用将不会指向正确的数据或者是处理器。

所以这里的技巧是设想hooks是把它的功能逻辑当作一系列数组来处理,同时还需要一个能够保持一致性的光标。如果你能做到这一点,所有的hooks都将能正常工作。

总结

希望我已经呈现了一个比较清晰的思维模型,能够帮助大家思考在新的Hooks API下,一切都是怎么运行的。记得这里(hooks)真正的价值是能够将一组相关的逻辑组合在一起,因此对于执行顺序要小心。使用hooks API将会有较高的回报。

Hooks对于React组件是一个有用的API插件。这也是人们对于hooks感到比较激动的一个原因。如果你将这种模型想象成一系列的存放state状态值的数组,那么你应该不会在使用他们的时候违反这些规则。