征得译者统一转载, 原翻译在知乎 https://zhuanlan.zhihu.com/p/48293710
原文地址 https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e
我是新的 API 的忠实粉丝。然而,它添加了一些奇怪的约束关于你需要如何去用它。这里我演示了一个模型,给那些想使用新的API 但是不能理解加的这些规则的原理的人。
注意: Hooks 还在实验中
这个文章是关于目前还是实验版的 Hooks API。 你可以在这里找到稳定的 React API 文档.
解析 Hooks 是如何工作的
我听到一些人对新的 Hooks API 草案在苦苦思考里面的“魔法”,所以我打算至少从表面层面开始分析语法是如何工作的。
有两个主要的使用规则是 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']
Gist https://gist.github.com/ryardley/e83bad1985740ab33b18fc578ec1957a/
为什么顺序很重要
如何我们改变了 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>
);
}
Gist https://gist.github.com/ryardley/3093d3c440ea6bdcdfcbc058e03ce5ab
我们在逻辑判断里面调用了 useState
,让我们看它在系统上造成的破坏
糟糕的组件第一次渲染
我们的实例变量 firstName
和 lastName
包含了正确的数据,让我们看看第二次渲染发生的事情
糟糕组件的第二次渲染
firstName
和 lastName
都被设置成了 “Rudi” 在我的状态存储中,变得不一致,这个很明显不正确,也不工作,但是给了我们一个想法为什么 hook 的规则是按照它要求的被制定。
React 团队执行了这个使用规则,是因为不准守就会导致不一致的数据
思考 hooks 维护了一系列数组,所以你不能打破规则
所以现在很清楚了为什么你不能在条件判断和循环里面调用 hooks,因为我们处理了游标指向一系列数组,如果你改变了内部的调用顺序,游标就不会匹配到正确的数据, 你的调用就不会指向正确的数据句柄。
所以这个技巧就是思考 hooks 的管理,需要一个一致的游标去管理一系列的数组。如果做到了任何写法都会能工作。
总结
有希望的是我们制定了一个清晰的思维模型,去思考在 hook 这个新的 API 底层做了什么事情。 思考真实的值能够把关注点聚到一起去小心这个顺序,这样在使用 API 的时候会有更好的效果。
Hooks 是一个高效的 API 插件对 React 组件来说。这就是为什么大家都很兴奋,如果你知道这个模型里面状态是一系列可以被设置的数组,那么你就会发现自己在使用的时候不会打破规则。
之后我打算看一下 useEffects 方法,尝试把它跟 React 组件的生命周期方法去比较。
这个文章是一个在线文档,如果你想贡献或者返回任何错误,请联系我 Twitter @rudiyardley 或者 github @ryardley .