理解 React Hooks 心智模型:必须按顺序、不能在条件语句中调用的规则

前言

自从 React 推出 hooks 的 API 后,相信大家对新 API 都很喜欢,但是它对你如何使用它会有一些奇怪的限制。比如,React 官网介绍了 Hooks 的这样一个限制:

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

useState Hook 的工作原理

这个限制并不是 React 团队凭空造出来的,的确是由于 React Hooks 的实现设计而不得已为之。

为了让大家有一个更清晰的思维模型,我将用数组来模拟useState的简单实现。 首先让我们通过一个例子来看看 hook 是如何工作的。

我们首先从一个组件开始:

js 复制代码
function RenderFunctionComponent() {
    const [firstName, setFirstName] = useState("Rudi");
    const [lastName, setLastName] = useState("Yardley");
 
    return (
        <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    );
}

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

具体实现

1)初始化

创建两个空数组:settersstate

将 cursor 设置为 0

2)第一次渲染

首次运行组件函数。

每次useState()调用,在第一次运行时,都会将一个 setter 函数推送到 setters 数组上,然后将一些状态推送到 state 数组上。

3)后续渲染

每次后续渲染都会重置 cursor,并且仅从每个数组中读取这些值。

4)事件处理

每个 setter 都有对其 cursor 的引用,因此通过触发对 setter 的调用,setter 它将更改状态数组中该位置的状态值。

简单代码实现

下面通过一段简单的代码示例来演示该实现。

注意:这并不是 React 的底层实现,但对于我们理解 react hook 的心智模型非常有帮助。

js 复制代码
const state = [];
const setters = [];
let cursor = 0;
 
function createSetter(cursor) {
    return function setterWithCursor(newVal) {
        state[cursor] = newVal;
    };
}
 
export function useState(initVal) {
    if (state[cursor] === undefined && setters[cursor] === undefined) {
        state.push(initVal);
        setters.push(createSetter(cursor));
    }
 
    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={() => setLastName('Fred')}>Fred</button>
        </div>
    );
}
 
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 'Richard' button
 
console.log(state); // After-click: ['Richard', 'Yardley']

为什么顺序很重要

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

让我们做 React 团队说你不应该做的事情:

js 复制代码
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的一个条件调用。让我们看看这对系统造成的破坏。

Bad Component 第一次渲染

我们的实例变量firstNamelastName包含正确的数据,但让我们看看第二次渲染会发生什么:

Bad Component 第二次渲染

现在,firstNamelastName发生了错位,我们的状态存储变得不一致了。这就是为什么保持正确顺序的重要性。

总结:

通过对 useState 的简单实现来理解 react hooks 的幕后实现逻辑。考虑将状态作为一组数组存在的模型,那么我们不该违反其对应的使用规则。

相关推荐
耶啵奶膘24 分钟前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^2 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic3 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿3 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具4 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161774 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test5 小时前
js下载excel示例demo
前端·javascript·excel
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事5 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro