在React中,三元运算符和逻辑运算符对待state的方式有何不同?

首先

React的文档不仅作为入门书,对于已经有一定了解的人来说,也是一个很好的网站来加深对React的理解。因此,我们公司内部以志愿者的形式每周举行读书会来交流和讨论。

由于对阅读了“状态的保持和重置”页面的会议上引发的疑问感到很有兴趣,本文成为了关于追求该问题的文章。

 

怀疑

为什么当使用三元运算符来分叉Counter并使用相同的输出DOM时,它会保持状态,但当使用逻辑运算符来表示时,它却不保持状态?

三个运算符的例子

// isFancyを切り替えてもCounterの値が保持される
<div>
  {isFancy ? (
    <Counter isFancy={true} /> 
  ) : (
    <Counter isFancy={false} />
  )}
</div>

 

論理運算符的例子

// isFancyを切り替えるとCounterの値がリセットされる
<div>
  {isFancy && (
    <Counter isFancy={true} /> 
  )}
  {!isFancy && (
    <Counter isFancy={false} />
  )}
</div>

 

回顾文件

首先,让我们回顾一下文档的内容。
首先是关于将React组件传递给浏览器DOM的方法。

React 在管理和建模用户创建的 UI 方面使用树结构。React 从 JSX 中创建 UI 树。然后,React DOM 根据这个 UI 树更新浏览器的 DOM 元素。(对于 React Native,它将 UI 树转换为移动平台特定的元素。)

下面是关于更新state的方法。

当组件在 UI 树中持续渲染时,React 会保持该组件的状态。

因此,在这里获得的知识仅仅是无法理解UI树是什么以及如何构建它,所以自然而然地应该考虑到JSX和基于它构建的DOM树,并研究如何维护状态。
在这个前提下考虑,这个疑问是合理的。在这种情况下,我解释说:”与DOM树不同,UI树决定状态的维护。由于UI树还包含一些不会出现在DOM树中的信息,比如false或null,因此前者会保留状态,而后者则不会保留。”。然而,我没有解释UI树是什么,因此回答并不完整。

因此,本文将阐述什么是UI树,以及它是如何构建的,以消除这种模糊不清。

关于UI树的内容

UI树是在传递JSX时,React将其转换为React DOM可以轻松处理的形式的结构(React DOM的部分也可以替换为React Native)。
为了确定UI树指的是什么,我们关注React DOM作为React组件处理方法之一的createRoot。常见的写法如下所示。

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

render的部分是传递JSX的地方。由于直接传递JSX,所以React没有处理将JSX转换为UI树的过程。

因此我们来考虑一下JSX。JSX是JavaScript的扩展语法,在运行时使用SWC或ESBuild等工具将其转译为浏览器可处理的JavaScript格式。

根据ESBuild的文档进行代码转换,

import React from 'react';
let button = <button>Click me</button>
render(button)

以下的代碼使用了React.createElement,轉換成下面的代碼如下。

import React from 'react';
let button = React.createElement('button', null, "Click me");
render(button);

换句话说,JSX被隐藏在转译过程中,通过使用React的createElement方法将其转换为便于React DOM处理的形式。
ESBuild的解释很容易理解,我只是用它作为参考。但是在React17之后,不再主流使用createElement的方法。
根据当时的文档,这种情况将被转译为以下形式。

import {jsx as _jsx} from 'react/jsx-runtime';
import React from 'react';
let button = _jsx('button', { children: 'Click me' });
render(button);

不再使用react的createElement方法,而是使用react/jsx-runtime中的_jsx。在先前的文档中有写到这样做的好处。
既然这样,我们就继续使用这种新的转换格式。
_jsx是如何转换的呢?react/jsx-runtime是从这段代码开始,并且大部分的处理逻辑都写在了ReactJSXElement.js中。
大致上来看,JSX似乎会被转换成以下的对象形式。

{
  $$typeof: REACT_ELEMENT_TYPE,
  type: type,
  key: key,
  ref: ref,
  props: props,
  _owner: owner,
};

在中国大陆说,_jsx函数的第一个参数是type,第二个参数是ref和props,第三个参数是key。如果props的children是JSX,那么_jsx会继续下去,因此会形成一个嵌套的树结构。

{
  $$typeof: REACT_ELEMENT_TYPE,
  type: type1,
  key: key1,
  ref: ref1,
  props: {
    children: [
      {
        $$typeof: REACT_ELEMENT_TYPE,
        type: type2,
        key: key2,
        ref: ref2,
        props: props2,
        _owner: owner,
      },
    ],
  },
  _owner: owner,
};

这可能被认为是文档中的UI树(由于文档中没有明确说明,所以只是推测)。
刚才的例子会被转化为以下的形式。

{
  $$typeof: Symbol(react.element),
  type: 'button',
  key: null,
  ref: null,
  props: {
    children: 'Click me'
  },
  _owner: null,
};
实际上,当在浏览器上显示时,无需追踪React代码,因为已经完成了转译。您可以在您的环境中通过将console.log()输出到控制台来查看UI树。

将相应的JSX转换为UI树

首先,让我们了解了什么是UI树后,将转换和确认JSX的议题。
首先是通过三元操作符进行分支的JSX。

<div>
  {isFancy ? (
    <Counter isFancy={true} /> 
  ) : (
    <Counter isFancy={false} />
  )}
</div>

将其转译成中文后,以下是一个选项:

进行转译后,如下所示。

const Counter = ({ isFancy }) => (_jsx("button", { className: isFancy ? 'fancy' : undefined, children: "0" }));
_jsx("div", { children: isFancy ? (_jsx(Counter, { isFancy: true })) : (_jsx(Counter, { isFancy: false })) });

当isFancy为true时,UI树的结构如下(省略了一些信息)。

{
  $$typeof: Symbol.for("react.element"),
  type: 'div',
  props: {
    children: [
      {
        $$typeof: Symbol.for("react.element"),
        type: ({ isFancy })=> {},
        props: { isFancy: true }
      }
    ]
  }
}

如果(isFancy),则`{…}`是计算组件的部分。接下来,我们将介绍如何利用逻辑运算符的JSX。

<div>
  {isFancy && (
    <Counter isFancy={true} /> 
  )}
  {!isFancy && (
    <Counter isFancy={false} />
  )}
</div>

当进行转译后,结果如下所示。

const Counter = ({ isFancy }) => (_jsx("button", { className: isFancy ? 'fancy' : undefined, children: "0" }));
_jsxs("div", { children: [isFancy && (_jsx(Counter, { isFancy: true })), !isFancy && (_jsx(Counter, { isFancy: false }))] })

如果出现_jsxs,将其视为类似于jsx的东西(参考,实际上在生产环境中会以同样的方式工作)。
如果isFancy为true,则如下所示

{
  $$typeof: Symbol.for("react.element"),
  type: 'div',
  props: {
    children: [
      {
        $$typeof: Symbol.for("react.element"),
        type: ({ isFancy })=> {},
        props: { isFancy: true }
      },
      false
    ]
  }
}

如果是错误的情况,将如下所示。

{
  $$typeof: Symbol.for("react.element"),
  type: 'div',
  props: {
    children: [
      false,
      {
        $$typeof: Symbol.for("react.element"),
        type: ({ isFancy })=> {},
        props: { isFancy: false }
      }
    ]
  }
}

這樣看來,確實在後者的寫法中,根據isFancy的切換,Counter所在的UI樹位置會從true轉變為false。
追蹤到這裡,可以理解為組件在UI樹上建立在不同的位置,也就解釋了狀態無法被持續保留的事實。

最後に

根据阅读React文档中提出的问题,我解决了关于UI树的实现。在React中,如果不了解其背景和理念,即使是自然的行为也可能看起来很不自然。因此,我希望能够更深入地理解其背景和思想,以便能够戴上React的眼镜继续开发下去。

广告
将在 10 秒后关闭
bannerAds