前言: 每个人都有属于本身的一片森林,也许咱们未曾去过,可是它一直在那里,总会在那里。迷失的人迷失了,相逢的人会再次相逢。 - 《挪威的森林》html
简单来讲Hooks规则就是咱们在使用Hooks编写程序的时候须要遵循的规范。react
不要在循环,条件或者嵌套函数中调用Hook.git
不要在普通的 JavaScript 函数中调用 Hook.github
咱们接下来将会举一个错误的例子,而且将会展开分析为何不能这么写, 这么写会致使什么错误发生。数组
⚠️ 错误示例(非完整版):架构
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
return arr.map(() => {
return Child();
});
};
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
复制代码
咱们都知道在组件中使用state hooks和effect hooks,靠的是Hook的调用顺序,这样React才能知道哪一个state对应那个useState。那么咱们先来捋一下上述示例代码Hooks的调用顺序。dom
// ------------
// 首次渲染
// ------------
useState([0, 1, 2]) // 使用[0, 1, 2]数组初始化arr
useState('Hello World!') // 使用'Hello World!'初始化title
useState('Hello World!') // 使用'Hello World!'初始化title
useState('Hello World!') // 使用'Hello World!'初始化title
// ------------
// 第二次渲染
// ------------
useState([0, 1, 2]) // 读取变量名为arr的state
useState('Hello World!') // 读取变量名为title的state - (A hook)
useState('Hello World!') // 读取变量名为title的state - (B hook)
useState('Hello World!') // 读取变量名为title的state - (C hook)
复制代码
以上就是Hooks的调用顺序了,上述这段代码确实没有什么问题,也能够正常执行。接下来咱们稍微修改一下代码。ide
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
return arr.map(() => {
return Child();
});
};
+ useEffect(() => {
+ setTimeout(() => {
+ setArr([0, 1])
+ }, 500);
+ }, [])
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
复制代码
咱们抛开effect的钩子不谈,就看state的钩子。咱们能够很容易地得出第三次Hooks调用的顺序是:函数
// ------------
// 第三次渲染
// ------------
useState([0, 1]) // 读取变量名为arr的state
useState('Hello World!') // 读取变量名为title的state
useState('Hello World!') // 读取变量名为title的state
复制代码
咱们发现程序抛出异常了,缘由是: 从新渲染后的钩子比预期的钩子要少。spa
Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
咱们再🤔思考一下, 若是咱们第三次渲染的时候, 渲染的钩子数量大于等于上一次的时候会不会抛出异常呢?咱们来试验一下。
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
return arr.map(() => {
return Child();
});
};
+ useEffect(() => {
+ setTimeout(() => {
+ setArr([0, 1, 2, 3])
+ }, 500);
+ }, [])
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
复制代码
咱们惊讶地发现,程序居然能够正常运行。那么这时候,咱们仔细推敲一下Hook的第一个规则: 不要在循环,条件或者嵌套函数中调用Hooks
, 其实这个规则的深层意思就是, 要让上一次的Hook的知道它应该返回什么。
什么意思呢? 就是说咱们在第三次渲染的时候, 应该让 A, B, C hook知道我应该返回什么值。当arr变化为[0, 1]的时候, C hook是不知道应该返回什么东西的, 所以程序就会报错。可是, 当arr变化为[0, 1, 2, 3]的时候, A, B, C hook都知道本身应该返回什么值, 所以程序能够正常运行。既然本文讲的是深度理解Hook规则,那么咱们接下来将会进行源码架构的分析。
为了保证你们都能看懂,下面的内容不会过多地涉及Hooks源码解析。
首先咱们得明白, Hook的更新流程是经过链表完成的。若是你们对于为何用链表感兴趣的能够去看这篇文章: 无心识设计-复盘React Hook的创造过程。 那么链表的结构应该是怎么样的呢?
咱们来模拟一下上述例子首次渲染的过程:
初始化的时候(组件还未渲染): firstWorkInProgressHook = workInProgressHook = null
组件初次渲染的时候
firstWorkInProgressHook = workInProgressHook = hook1
workInProgressHook = workInProgressHook.next = hook2
workInProgressHook = workInProgressHook.next = hook3
workInProgressHook = workInProgressHook.next = hook4
这个过程,其实就是一个用链表存储的过程, 那么每个hook至少应该可以保存当前它本身的信息和下一个节点(hook)的信息而且拥有可以更新这个链表的功能。
type Hook = {
memoizedState: any, // 上次更新完的最终状态
queue: UpdateQueue<any, any> | null, // 更新队列
next: Hook | null, // 下一个hook
};
复制代码
那么咱们能够很容易的摸出, 整个链表应该长什么样子:
const fiber = {
//...
memoizedState: {
memoizedState: [0, 1, 2],
queue: {
// ...
},
next: {
memoizedState: 'Hello World!',
queue: {
// ...
},
next: 'Hello World'
}
},
// ...
memoizedState: {
memoizedState: 'Hello World',
queue: {
// ...
},
next: {
memoizedState: 'Hello World!',
queue: {
// ...
},
next: 'Hello World'
}
},
//...
}
复制代码
整个链表是在mount时构造的,所以当咱们执行update操做的时候必定要保证执行顺序, 否则的话整个链表就乱了。这时候, 咱们联想到Hook的第一条规则: 不要在循环,条件 或者嵌套函数中调用Hook, 你们应该可以大体理解为何要遵照这个规则了吧。接下来, 咱们复盘一下, 上面的错误例子,加深一下咱们的印象。
当咱们执行更新arr操做的时候, setArr([0, 1])
, 第三个hook的next会找不到下一个节点.所以会在finishHooks的时候会抛出异常。咱们能够在react-dom.development.js
看到 当咱们更新到第三个hooks、的时候, 会出现找不到下一个hook的状况, 所以didRenderTooFewHooks
为 false
。因此抛出了上面例子中的异常。
// 源码部分
function finishHooks() {
// ...
var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
// ...
!!didRenderTooFewHooks ? invariant(false, 'Rendered fewer hooks than expected. This may be caused by an accidental early return statement.') : void 0;
// ...
}
复制代码
import React, { Fragment, useState, useEffect } from 'react';
const Child = () => {
const [title, setTitle] = useState('Hello World!');
return <h1>{title}</h1>;
};
const Layout = () => {
const [arr, setArr] = useState([0, 1, 2]);
const renderItem = () => {
+ return arr.map((_, index) => {
- return arr.map(() => {
- return Child();
+ return <Child key={index} />
});
};
useEffect(() => {
setTimeout(() => {
setArr([0, 1])
}, 500);
}, [])
return <Fragment>{renderItem()}</Fragment>;
};
export default Layout;
复制代码