React 为什么不能在循环、条件或嵌套函数中调用 Hooks?

发布于 2024-01-01 11:46:22 字数 3787 浏览 17 评论 0

如果在条件语句中使用 hooks,React 会抛出 error。这与 React Hooks 的底层设计的数据结构相关,先抛出结论:react 用链表来严格保证 hooks 的顺序。一个典型的 useState 使用场景:

const [name,setName] = useState('leo');

......

setName('Lily');

那么 hooks 在这两条语句分别作了什么?

上图是 useState 首次渲染的路径,其中,跟我们问题相关的是 mountState 这个过程,简而言之,这个过程初始化了一个 hooks,并且将其追加到链表结尾。

// 进入 mounState 逻辑

function mountState(initialState) {

  // 将新的 hook 对象追加进链表尾部
  var hook = mountWorkInProgressHook();

  // initialState 可以是一个回调,若是回调,则取回调执行后的值

  if (typeof initialState === 'function') {

    // $FlowFixMe: Flow doesn't like mixed types

    initialState = initialState();
  }

  // 创建当前 hook 对象的更新队列,这一步主要是为了能够依序保留 dispatch

  const queue = hook.queue = {

    last: null,

    dispatch: null,

    lastRenderedReducer: basicStateReducer,

    lastRenderedState: (initialState: any),

  };

  // 将 initialState 作为一个“记忆值”存下来

  hook.memoizedState = hook.baseState = initialState;

  // dispatch 是由上下文中一个叫 dispatchAction 的方法创建的,这里不必纠结这个方法具体做了什么

  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);

  // 返回目标数组,dispatch 其实就是示例中常常见到的 setXXX 这个函数,想不到吧?哈哈

  return [hook.memoizedState, dispatch];
}

从这段源码中我们可以看出,mounState 的主要工作是初始化 Hooks。在整段源码中,最需要关注的是 mountWorkInProgressHook 方法,它为我们道出了 Hooks 背后的数据结构组织形式。以下是 mountWorkInProgressHook 方法的源码:

function mountWorkInProgressHook() {

  // 注意,单个 hook 是以对象的形式存在的
  var hook = {

    memoizedState: null,

    baseState: null,

    baseQueue: null,

    queue: null,

    next: null

  };

  if (workInProgressHook === null) {
    // 这行代码每个 React 版本不太一样,但做的都是同一件事:将 hook 作为链表的头节点处理
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 若链表不为空,则将 hook 追加到链表尾部
    workInProgressHook = workInProgressHook.next = hook;
  }
  // 返回当前的 hook
  return workInProgressHook;
}

到这里可以看出,hook 相关的所有信息收敛在一个 hook 对象里,而 hook 对象之间以单向链表的形式相互串联。

接着,我们来看更新过程

上图中,需要注意的是 updateState 的过程:按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染。我们把 mountState 和 updateState 做的事情放在一起来看:mountState(首次渲染)构建链表并渲染;updateState 依次遍历链表并渲染。

hooks 的渲染是通过“依次遍历”来定位每个 hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。

这个现象有点像我们构建了一个长度确定的数组,数组中的每个坑位都对应着一块确切的信息,后续每次从数组里取值的时候,只能够通过索引(也就是位置)来定位数据。也正因为如此,在许多文章里,都会直截了当地下这样的定义:Hooks 的本质就是数组。但读完这一课时的内容你就会知道,Hooks 的本质其实是链表。

我们举个例子:

    let mounted = false;

if(!mounted){
  // eslint-disable-next-line
  const [name,setName] = useState('leo');
  const [age,setAge] = useState(18);
  mounted = true;
}
const [career,setCareer] = useState('码农');
console.log('career',career);

......

<div onClick={()=>setName('Lily')}>
  点我点我点我
<div>

点击 div 后,我们期望的输出是 "码农",然而事实上(尽管会 error,但是打印还是执行)打印的为 Lily,原因是,三个 useState 在初始化的时候已经构建好了一个三个节点的链表结构,依次为: name('leo') --> age(18) --> career('码农')

每个节点都已经派发了一个与之对应的 update 操作,因此执行 setName 时候,三个节点就修改为了 name('Lily') --> age(18) --> career('码农')

然后执行 update 渲染操作,从链表依次取出值,此时,条件语句的不再执行,第一个取值操作会从链表的第一个,也就是 name 对应的 hooks 对象进行取值:此时取到的为 name:Lily

必须按照顺序调用从根本上来说是因为 useState 这个钩子在设计层面并没有“状态命名”这个动作,也就是说你每生成一个新的状态,React 并不知道这个状态名字叫啥,所以需要通过顺序来索引到对应的状态值

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

两仪

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

ni139999

文章 0 评论 0

Smile

文章 0 评论 0

木子李

文章 0 评论 0

仅此而已

文章 0 评论 0

qq_2gSKZM

文章 0 评论 0

内心激荡

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文