[前端]字节面试官: React的<组件 /> 和 组件?() 啥啥分不清楚?

急诊TL;DR

To be a component ≠ Return JSX
<Component /> ≠ Component()

Babel REPL跑一跑

const A1 = () => <div />
const B1 = () => <div><A1 /></div>

const A2 = <div />
const B2 = <div>{A2}</div>

编译成babel:

import { jsx as _jsx } from "react/jsx-runtime";
const A1 = () => /*#__PURE__*/_jsx("div", {});
const B1 = () => /*#__PURE__*/_jsx("div", {
  children: /*#__PURE__*/_jsx(A1, {})
});
const A2 = /*#__PURE__*/_jsx("div", {});
const B2 = /*#__PURE__*/_jsx("div", {
  children: A2
});

门诊

注意:本文试图解释一个相对高级的概念。

在网页开发中,我最喜欢的事情之一是几乎任何问题都可能引发一次难忘的深入研究,从而揭示一个非常熟悉的事物中的全新内容。

这刚刚发生在我身上,所以现在我对React了解得更多一点,想与你分享。

一切都始于一个错误,我们将逐步重现这个错误。以下是起点:

这个应用只包含两个组件,App 和 Counter。

让我们检查一下 App 的代码:

const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal(currentTotal => currentTotal + 1);

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};

目前为止还没什么有趣的,对吧?它只是渲染了3个计数器,并追踪并显示所有计数器的总和。

现在让我们为我们的应用添加一个简短的描述:

const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal((currentTotal) => currentTotal + 1);
+ const Description = () => (
+   <p>
+     I like coding counters!
+     Sum of all counters is now {total}
+   </p>
+ );

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
+       <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};

仍然像之前一样完美工作,但现在它有了一个全新的描述,很酷!

你可能注意到我声明了一个名为 Description 的组件,而不是直接在 App 的 return 语句中编写 JSX。可能有很多原因,让我们简单说我想让 App 的 return 内部的 JSX 保持清晰易读,所以我将所有混乱的 JSX 移到了 Description 组件中。

你还可能注意到我在 App 内声明了 Description。这不是一种标准的方式,但是 Description 需要知道当前的状态以显示总点击数。我可以重构它并将总数传递为属性,但我不打算重新使用 Description,因为我只需要为整个应用程序使用一个!

现在,如果我们还想在中央计数器上方显示一些额外的文本怎么办?让我们尝试添加它:

const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal((currentTotal) => currentTotal + 1);
  const Description = () => (
    <p>
      I like coding counters!
      Sum of all counters is now {total}
    </p>
  );
+
+ const CounterWithWeekday = (props) => {
+   let today;
+   switch (new Date().getDay()) {
+     case 0:
+     case 6:
+       today = "a weekend!";
+       break;
+     case 1:
+       today = "Monday";
+       break;
+     case 2:
+       today = "Tuesday";
+       break;
+     default:
+       today = "some day close to a weekend!";
+       break;
+   }
+
+   return (
+     <div>
+       <Counter {...props} />
+       <br />
+       <span>Today is {today}</span>
+     </div>
+   );
+ };

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
        <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
-       <Counter onClick={incrementTotal} />
+       <CounterWithWeekday onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};

太棒了!现在我们确实有一个错误!看一下:

注意一下,当你点击中央计数器时,总数是增加的,但计数器本身始终保持在0。

现在,让我感到惊讶的不是这个错误本身,而是我意外发现以下内容可以无缝运行:

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
        <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
-       <CounterWithWeekday onClick={incrementTotal} />
+       { CounterWithWeekday({ onClick: incrementTotal }) }
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );

也感到惊讶吗?我们一起来深入探讨吧!

从这个bug谈起

这个错误发生是因为我们在每次 App 更新时都创建了全新的 CounterWithWeekday。 这是因为 CounterWithWeekday 被声明在 App 内部,这可能被视为一种反模式

在这种特定情况下,解决起来很容易。只需将 CounterWithWeekday 的声明移到 App 外部,错误就会消失。

你可能会想为什么我们在 Description 上没有相同的问题,即使它也是在 App 内部声明的。 实际上我们是有的!只是不太明显,因为 React 重新挂载组件得非常快,我们几乎察觉不到,而且由于该组件没有内部状态,所以不像 CounterWithWeekday 那样会丢失。

但为什么直接调用 CounterWithWeekday 也能解决 bug 呢?是不是有文档说明你可以将一个函数组件直接作为普通函数调用?两种选项之间有什么区别?一个函数不管以什么方式调用,应该返回完全相同的结果,对吗? 🤔

直接调用

根据 React 文档,组件只是一个普通的 JavaScript 类或函数,最终返回 JSX(大多数情况下是这样)。

然而,如果函数组件只是函数,为什么我们不直接调用它们呢?为什么要使用 <Component /> 语法呢?

事实证明,直接调用在 React 的早期版本中是一个热门讨论的话题。实际上,帖子的作者分享了一个 Babel 插件的链接,该插件(而不是创建 React 元素)有助于直接调用您的组件。

在 React 文档中,我并没有找到有关直接调用函数组件的任何提及,然而,有一种技术演示了这种可能性——渲染属性(render props)

经过一些实验,我得出了一个相当有趣的结论。

所以组件到底是啥?

返回 JSX、接受属性或将某些内容渲染到屏幕上与是否是组件无关。

同一个函数可以同时充当组件和普通函数。

成为组件更多地涉及拥有自己的生命周期和状态。

让我们看一下前面示例中 <CounterWithWeekday onClick={incrementTotal} /> 在 React 开发者工具中的样子:

那么,它是一个渲染另一个组件(Counter)的组件。

现在让我们将其更改为 { CounterWithWeekday({ onClick: incrementTotal }) } 并再次查看 React 开发者工具:

确切!没有 CounterWithWeekday 组件。它根本不存在。

Counter 组件和 CounterWithWeekday 返回的文本现在是 App 的直接子组件。

此外,这个 bug 现在已经消失了,因为 CounterWithWeekday 组件不存在,中央计数器不再依赖它的生命周期,因此它的行为与它的兄弟计数器完全相同。

以下是我一直在探讨的一些问题的快速答案。希望对某些人有所帮助。

为什么 CounterWithWeekday 组件在 React 开发者工具中不再显示? 原因是它不再是一个组件,而只是一个函数调用。

当你这样做时:

const HelloWorld = () => {
  const text = () => 'Hello, World';

  return (
    <h2>{text()}</h2>
  );
}

很明显,变量 text 不是一个组件。 如果它返回 JSX,它也不会是一个组件。 如果它接受一个名为 props 的单一参数,它也不会是一个组件。

一个可以作为组件使用的函数并不一定会被当作组件来使用。所以,要成为一个组件,它需要被使用为 <Text />

CounterWithWeekday 也是同样的情况。

顺便说一下,组件可以返回普通字符串。

为什么 Counter 现在不再失去状态呢? 为了回答这个问题,让我们先回答为什么 Counter 的状态会被重置。

以下是逐步发生的事情:

  1. CounterWithWeekday 在 App 内部声明并用作组件。
  2. 它首次被渲染。
  3. 每次 App 更新时,都会创建一个新的 CounterWithWeekday。
  4. CounterWithWeekday 每次 App 更新都是全新的函数,因此 React 无法确定它是相同的组件。
  5. React 清除 CounterWithWeekday 之前的输出(包括其子组件),并在每次 App 更新时挂载新的 CounterWithWeekday 输出。因此,与其他组件不同,CounterWithWeekday 从不更新,而总是从头开始挂载。
  6. 由于每次 App 更新都会重新创建 Counter,所以每次父组件更新后,Counter 的状态将始终为 0。

因此,当我们将 CounterWithWeekday 作为一个函数调用时,它也在每次 App 更新时被重新声明,但这不再重要。让我们再次查看 hello world 示例,看看为什么:

const HelloWorld = () => {
  const text = () => 'Hello, World';

  return (
    <h2>{text()}</h2>
  );
}

在这种情况下,React 不会期望在更新 HelloWorld 时 text 引用是相同的,对吧?

实际上,React 甚至无法检查 text 引用是什么。它根本不知道 text 的存在。如果我们像这样将 text 内联,React 实际上不会注意到任何差异:

const HelloWorld = () => {
- const text = () => 'Hello, World';
-
  return (
-   <h2>{text()}</h2>
+   <h2>Hello, World</h2>
  );
}

因此,通过使用 <Component />,我们使组件对 React 可见。然而,在我们的示例中,由于 text 只是直接调用,React 永远不会知道它的存在。 在这种情况下,React 只是比较 JSX(或在这种情况下是文本)。只要 text 返回的内容相同,就不会重新渲染任何内容。

这正是发生在 CounterWithWeekday 上的情况。如果我们不像 <CounterWithWeekday /> 那样使用它,它就永远不会暴露给 React。

这样,React 只会比较函数的输出,而不是函数本身(在我们将其用作组件时会比较函数本身)。 由于 CounterWithWeekday 的输出是正确的,因此不会重新挂载任何内容。

总结

一个返回 JSX 的函数可能不是一个组件,这取决于它是如何被使用的。

要成为一个组件,返回 JSX 的函数应该像 <Component /> 这样使用,而不是像 Component() 这样直接调用。

当一个函数组件被用作 <Component /> 时,它将具有生命周期并且可以有状态。

当一个函数被直接调用为 Component() 时,它只会运行并(可能)返回一些东西。没有生命周期,没有 hooks,没有 React 的任何魔法。它非常类似于将一些 JSX 赋值给一个变量,但具有更大的灵活性(你可以使用 if 语句、switch、throw 等)。

在非组件中使用状态是危险的。

在将来,未被声明为组件的返回 JSX 的函数可能会被官方视为反模式。有一些特殊情况(比如渲染属性),但一般来说,你几乎总是想要重构这些函数以成为组件,因为这是推荐的方式。

如果必须在函数组件内声明一个返回 JSX 的函数(例如,由于紧密耦合的逻辑),直接调用它如 {component()} 可能比将其用作 <Component /> 更好。

将简单的 <Component /> 转换为 {Component()} 可能在调试时非常方便。

重要内容

请点赞并关注!Please like and follow! ¡Por favor, dale me gusta y sígueme! कृपया लाइक और फॉलो करें! الرجاء الإعجاب والمتابعة! Por favor, curta e siga! Пожалуйста, поставьте лайк и подпишитесь! いいねとフォローお願いします!Bitte liken und folgen! Veuillez aimer et suivre ! Per favore, metti mi piace e seguimi! 좋아요와 팔로우 부탁드립니다! Lütfen beğenip takip edin! Tolong like dan follow! Like en volg alsjeblieft! Silakan suka dan ikuti! தயவு செய்து விரும்பி பிடிக்கவும் மற்றும் பின்னூட்டவும்! Molimo vas da lajkujete i pratite! לאָב און פאָלגן! ਕਿਰਪਾ ਕਰਕੇ ਪਸੰਦ ਕਰੋ ਅਤੇ ਫਾਲੋ ਕਰੋ! 请讚和關注!Palun meeldi ja jälgi! Silakan suka lan ikuti! ದಯವಿಟ್ಟು ಲೈಕ್ ಮಾಡಿ ಮತ್ತು ಅನುಸರಿಸಿ! زنده‌باد و دنبال کنید! お願いいたします!សូមស្រស់លុយនិងតាមដាន! অনুগ্রহ করে লাইক এবং অনুসরণ করুন! ကျေးဇူးပါပြီ နှင့် လွှဲရန်! ਕਿਰਪਾ ਕਰਕੇ ਲਾਇਕ ਅਤੇ ਫਾਲੋ ਕਰੋ! ביטע לייק און פאלגן! Naalika ಮತ್ತು ಅನುಸರಿಸಿ! Дараа нь нэгтэйчүүлж, дагаадагч хайрцагтай болно уу! ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਲਾਇਕ ਅਤੇ ਫਾਲੋ ਕਰੋ! இந்தியாவின் சொந்தக் கோவில் பகுதியில் பங்குபற்றவும் பின்னர் பிரிக்கவும்! لطفاً لایک کنید و دنبال کنید! مہربانی کرکے لائیک اور فالو کریں! Зүгээр лайк болон дагаж мэдэхийг хүсье! Дараа нь лайк болон дагаж мэдэхийг хүсье! কিন্তু আমি ভালো কাজ করে আসতে চাই।

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值