image.png

在 React 中创建自定义钩子(hooks)是一个非常有价值的练习,甚至可以说是许多开发者的“啊哈”时刻。

剧透警告:第一次尝试是在 Revolut 的一次高压面试中,结果并不如意。

在这篇文章中,带你一起回顾我重现著名的 useState() 钩子的历程,称它为 useMyState()。同时,我也会给出一些建议,帮助你下次面试时避免失败。

让我们一起来探讨哪里出了问题,我是如何重新拾起信心的,以及你如何避免犯同样的错误。准备好迎接一些高级 React 的乐趣吧!🎉

梦寐以求的工作和艰难的面试 🎯

拿到 Revolut 的面试机会感觉就像中了彩票。我满怀激动和信心,因为我花了数周时间打磨我的 React 和 TypeScript 技能。

我以为我已经掌握了一切——但面试让我意识到,我的信心过于自满。这个面试,我既兴奋又紧张,然而很快,这次经历变成了一次让我永生难忘的挫败。

在顺利通过一些初步面试轮次后,我遇到了一个我没有完全预料到的挑战:“你能从头开始重现 useState() 钩子吗?”

这个问题看似简单,但其背后的意义深远。我知道 useState() 在高层次上的工作原理,但在面试现场,面对时钟滴答作响,要从头构建它却是另一回事。

任务:在压力下重现 useState() 💥

面试官解释说,我需要创建一个自定义钩子,模仿 React 的 useState() 的行为。

image.png

这个钩子应该存储状态,允许状态更新,并在状态变化时触发重新渲染。

听起来很熟悉,对吧?我曾经读过这类内容,甚至考虑过如何实现。但现在,在面试压力下,一切似乎变得更加复杂。

关键问题是:我从未实际在现场实现过类似的东西。

概念在我脑海中很清晰,但在面对一个可能决定我职业生涯的人时,执行起来却完全不同。

最终,我没有完成这个任务……但后来我决定在没有时间压力的情况下再试一次,结果是这样的。

为什么要重现 useState()? 🤔

你可能会问,“为什么要重现一个已经完美运作的东西?”嗯,这正是我为什么要做的原因——因为它确实运作得很好。

理解 useState() 的底层工作原理有助于你更好地掌握 React 的“魔法”。

此外,构建你自己的工具——即使它们已经存在——也是一种非常有成就感的体验。

入门:useMyState() 背后的概念 🔨

在开始编码之前,让我们明确我们想要实现的目标。React 中的 useState() 钩子:

  • 返回一个状态变量和一个用于更新该状态的函数。
  • 每当状态更新时,重新渲染组件。

我们的 useMyState() 钩子应该实现相同的功能。本质上,我们需要一种方式来存储状态,更新它,并在发生变化时触发重新渲染。听起来很简单,对吧?嗯,这既简单又复杂。

构建模块:useState() 的底层工作原理 🧱

首先,让我们讨论一下 React 如何在内部管理状态。当你调用 useState() 时,React 在内部通过一个称为“钩子状态队列”的机制来跟踪你的状态。

每当你调用更新函数(我们称之为 setState)时,React 并不会立即更新状态。相反,它会调度一次重新渲染。

在这次重新渲染期间,React 根据队列计算新的状态。

要复制这种行为,我们需要:

  • 将状态存储在一个可以跨渲染持续存在的地方。
  • 提供一个函数来更新这个状态并触发重新渲染。

创建 useMyState():编写一些代码! 💻

我们可以这样开始。首先,我们需要创建一个组件级别的状态存储。

由于 React 并不提供一种直接的方式来在渲染之间保持变量的持久性(除了使用类似 useState() 或 useRef() 的钩子),我们需要使用 useRef() 来实现这一点。

function useMyState(initialValue) {
  const stateRef = React.useRef(initialValue);
  const [, forceRender] = React.useReducer(x => x + 1, 0);

  const setState = (newValue) => {
    stateRef.current = newValue;
    forceRender();
  };

  return [stateRef.current, setState];
}

细分解释 🕵️‍♂️

  • useRef(initialValue): 这是我们的状态所在。useRef() 可以在渲染之间保持值,而不会导致重新渲染。
  • useReducer(x => x + 1, 0): 这是一个巧妙的技巧,用来强制组件重新渲染。我们不关心 x 的值,只是调用 forceRender() 会使 React 重新渲染我们的组件。
  • setState(newValue): 我们更新 stateRef 的 current 属性,然后触发一次重新渲染。

就这样!通过这三部分,我们就有了自己的 useState() 版本。但我们怎么知道它是否有效呢?让我们测试一下。

测试 useMyState(): 让它工作 🔥

让我们看看这个钩子的实际效果。这里有一个简单的计数器组件,使用我们的 useMyState():

function Counter() {
  const [count, setCount] = useMyState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

当你点击“Increment”按钮时,计数器应该增加,组件会重新渲染以反映新的计数。这就像使用 useState() 一样。很棒吧?

处理多个状态:扩展 useMyState() ⚖️

现在,如果我们需要在同一个组件中管理多个状态会怎么样?React 的 useState() 可以优雅地处理这个问题,我们的 useMyState() 也可以。

function MultipleStatesComponent() {
  const [count, setCount] = useMyState(0);
  const [text, setText] = useMyState('Hello');

  return (
    <div>
      <p>{count}</p>
      <p>{text}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setText('World')}>Change Text</button>
    </div>
  );
}

这个组件管理两个独立的状态变量,useMyState() 像 useState() 一样处理它们。

总结 🎯

React 的 useState() 钩子经过高度优化、经过实战检验,能够处理我们简单的 useMyState() 可能忽略的边缘情况。

尽管我未能在面试中重现它,但我能够在一个平静的环境中自己重新实现它,下次面试时,我会准备得更好。

重现这样的工具并不是为了取代它们,而是为了学习它们的工作原理,从而更好地使用它们。

谁知道呢?你也许会在一个副项目中找到 useMyState() 的用武之地,或者只是把它当作一个有趣的技巧留在你的工具箱里。

祝你好运!

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68k 声望104.9k 粉丝