在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,
};
将相应的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的眼镜继续开发下去。