我非常喜欢新的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内部可能如何工作的。下面的例子会在执行上下文中渲染一个特定的组件。这意味着此处存储的数据位于要渲染的组件之外的一层。此状态不与其他组件共享,而是保持在当前作用域中,可以在下一次渲染组件时候访问。
- 初始化
创建两个空数组:
setters
和state
。
设置游标为0
- 第一次render 第一次执行这个函数式组件
第一次执行的时候调用每个useState()
,将setter函数(和当前下标关联)push到setters
数组然后push state到state
数组。
-
下一次渲染 每当下一次渲染的时候游标都会被重置然后这些values就是从数组中获取。
-
事件处理 每一个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
在条件语句中调用。让我们看看这样对系统造成的破坏。
错误组件第一次渲染
这个时候firstName
和lastName
变量是正确的,但是我们来看看接下来第二次渲染会发生什么:
错误组件第二次渲染
现在firstName
和lastName
都被设为了'Rudi',我们的state存储的数据变得不一致了。这显然是错误的并且无法正常工作,但是它让我们知道为什么hooks有这个限制规则。
React团队规定hooks的使用规则就是为了保证不会出现这种数据不一致的问题。
考虑到hooks是在操作一个数组那么你就知道为什么不要打破这个规则
所以现在你应该清楚为什么不可以在条件语句或者循环中调用hooks。因为我们在处理数组的下标,如果你改变了hooks在render时候的调用顺序,这个下标将不会匹配到准确,所以你不会访问到正确的数据或者handles。
因此,诀窍是考虑将hooks作为一组需要一致游标的数组来管理其业务。如果执行此操作,则一切正常。
结论
希望我已经为如何思考新的hooks API在幕后发生的事情奠定了更清晰的思维模型。请记住,这里的真正价值在于能够将关注点归类在一起,因此在使用hooks API时如果能够小心顺序的问题则会带来很高的回报。