React 元素 VS 组件

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: React-Element/React-Componment/React-Instance之间的区别和联系。


并不是所有的问题都会有正确而完美的答案,对于那些没有明确答案的问题不必穷究到底,我们需要寻找的,是在当下条件允许的情况下,一个能够解决问题的足够好的答案。

但求够好,不求最好

大家好,我是柒八九

在前面的前端框架中,我们从Fiber的实现机制描绘了React在页面渲染和数据处理方面做了的努力。其中有很多源码级别的概念。例如,React-Element/React-Fiber/Current TreeworkInProgress Tree等。

我们其实在React_Fiber机制已经讲过React-ElementReact-Fiber之间的关系。但是,都是一带而过。

今天,我们来谈谈React-Element/React-Componment/React-Instance之间的区别和联系。

话不多说,开干。


React元素组件实例React中的不同术语,它们密切相关。

假设存在如下的代码:

const App = () => {
  return <p>Hello 789</p>;
};
复制代码

React-Componment就是一个组件的声明

在上面例子中,它是一个函数组件,但它也可以是任何其他类型的React组件(例如React类组件)

函数组件的情况下,它被声明为一个JavaScript函数,返回ReactJSX。更复杂的JSXHTMLJavaScript混合体,但这里我们处理的是一个简单的例子,它只返回一个带有内部内容的HTML元素。

(props) => JSX
复制代码

我们可以进行组件的嵌套处理。只要我们在另一个组件中把目标组件作为带角括号的React元素(例如:)即可。

const Greeting = ({ text }) => {
  return <p>{text}</p>;
};
const App = () => {
  return <Greeting text="Hello 789" />;
};
复制代码

可以将一个组件作为「React元素」多次渲染

每当一个组件被渲染成元素时,就会创建一个该组件的实例

const Greeting = ({ text }) => {
  return <p>{text}</p>;
};
const App = () => {
  return (
    <>
+      <Greeting text="Greeting-元素1" />
+      <Greeting text="Greeting-元素2" />
+      <Greeting text="Greeting-元素3" />
    </>
  );
};
复制代码
  1. React组件被声明一次
  2. 组件可以作为JSX中的React元素多次使用
  3. 元素被使用时,它就成为该组件的一个实例,挂载在React的组件树中


React-Element

继续从一个简单的例子入手:

const App = () => {
  return <p>Hello 789</p>;
};
复制代码

每当React组件调用(渲染)时React内部调用React.createElement()方法,该方法返回以下对象。

{
   $$typeof: Symbol(react.element)
   "type": "p",
   "key": null,
   "ref": null,
   "props": {
     "children": "Hello 789"
   },
   "_owner": null,
   "_store": {}
}
复制代码

挑几个重点的属性来解释下:

  • type:代表实际的HTML元素
  • props:传递给这个HTML元素的所有HTML属性(加上文本内容{Hello 789},读作:children

针对上面的

元素,没有属性被赋值。然而,Reactchildren 视为伪HTML属性,而children代表在HTML标签之间呈现的一切

当向

HTML元素添加属性时,props中的就会包含对应的信息。

const App = () => {
  return <p className="greet">Hello 789</p>;
};
console.log(App());
{
   $$typeof: Symbol(react.element)
   "type": "p",
   "key": null,
   "ref": null,
   "props": {
     "children": "Hello 789",
     "className": "greet"
   },
   "_owner": null,
   "_store": {}
}
复制代码

本质上,React 除了将所有HTML属性转换成React-props外,还将内部内容添加为children属性。

如前所述,ReactcreateElement()方法是内部调用的。因此,我们可以用它来替代返回的JSXReactcreateElement方法需要一个typepropschildren作为参数。

  • HTML标签p作为第一个参数
  • 第二个参数--props 用一个带有className的对象填充
  • children作为第三个参数
const App = () => {
  // return <p className="greet">Hello 789</p>;
  return React.createElement(
    'p',
    { className: 'greet' },
    'Hello 789'
  );
};
复制代码

该方法的调用并没有1:1地反映出返回的对象,其中children 元素是props对象的一部分。我们可以直接将children作为第二个参数props中的属性。

const App = () => {
  // return <p className="greet">Hello 789</p>;
  return React.createElement(
    'p',
    {
      className: 'greet',
      children: 'Hello 789'
    }
  );
};
复制代码

默认情况下,children是作为第三个参数使用的

下面的例子显示了一个React组件,它将HTML树渲染成JSX,并通过ReactcreateElement()方法转化为React元素

const App = () => {
  return (
    <div className="container">
      <p className="greet">Hello 789</p>
      <p className="info">你好!</p>
    </div>
  );
};
console.log(App());
{
   $$typeof: Symbol(react.element)
   "type": "div",
   "key": null,
   "ref": null,
   "props": {
     "className": "container",
     "children": [
       {
         $$typeof: Symbol(react.element)
         "type": "p",
         "key": null,
         "ref": null,
         "props": {
           "className": "greet",
           "children": "Hello 789"
         },
         "_owner": null,
         "_store": {}
       },
       {
         $$typeof: Symbol(react.element)
         "type": "p",
         "key": null,
         "ref": null,
         "props": {
           className: "info",
           children: "你好!"
         },
         "_owner": null,
         "_store": {}
       }
     ]
   },
   "_owner": null,
   "_store": {}
}
复制代码

在内部,所有的JSXReactcreateElement()方法转换

所以,我们在使用JSX的地方,都可以用createElement()进行同等效果替换。

const App = () => {
  // return (
  //   <div className="container">
  //     <p className="greet">Hello 789</p>
  //     <p className="info">你好!</p>
  //   </div>
  // );
  return React.createElement(
    'div',
    {
      className: 'container',
    },
    [
      React.createElement(
        'p',
        { className: 'greet' },
        'Hello 789'
      ),
      React.createElement(
        'p',
        { className: 'info' },
        '你好!'
      ),
    ]
  );
};
复制代码

同时,我们还可以对上述的代码进行组件的封装和抽离。将用于显示的信息,封装成Text组件。并且,在同样的位置,进行组件调用。

const Text = ({ className, children }) => {
  return <p className={className}>{children}</p>;
};
const App = () => {
  return (
    <div className="container">
      <Text className="greet">Hello 789</Text>
      <Text className="info">你好!</Text>
    </div>
  );
};
复制代码

然后,将用JSX的实现准换为用createElement实现,代码如下:

const Text = ({ className, children }) => {
  return <p className={className}>{children}</p>;
};
const App = () => {
  // return (
  //   <div className="container">
  //     <Text className="greet">Hello 789</Text>
  //     <Text className="info">你好!</Text>
  //   </div>
  // );
  return React.createElement(
    'div',
    {
      className: 'container',
    },
    [
      React.createElement(
        Text,
        { className: 'greet' },
        'Hello 789'
      ),
      React.createElement(
        Text,
        { className: 'info' },
        '你好!'
      ),
    ]
  );
};
复制代码

虽然,使用createElement()能达到与JSX同等的效果,但是在一般情况下,我们不推荐。

但是,但是,但是,在有些场景下,利用createElement(),可以达到很好的效果。

在前面的文章中,我们介绍了关键渲染路径,其中针对React的项目中,我们可以使用React.lazy()页面加载阶段,对代码进行分割处理。而在页面运行阶段,可以使用import()来做按需处理。

而在工程化之webpack打包过程中我们介绍到,一个动态导入(即import()函数)会产生一个新的子ChunkGroup,从而能够对业务逻辑进行分割处理。

这里我们举一个比较简单的例子。在React的项目开发中,我们进行弹窗的处理。

比较常规的处理方式

import React,{ FC, useState } from 'react';
import TestModal from './TestModal';
type TTest = React.PropsWithChildren<{ 
}>
const Test:FC<TTest> = ({}) => {
    const [visible,setVisible] = useState(false);
    return (
        <div >
            <button onClick={() => setVisible(true)}></button>
            {visible 
            && <TestModal handleCancelCB={() => setVisible(false) }/>}
        </div>
    )
}
export default Test;
复制代码

有关键的几步

  1. 页面同步引入资源(import)
  2. 在调用处,需要一个变量(visible)来控制TestModal显隐
  3. () => setVisible(false)传入到TestModal中,用于控制一堆操作后,将弹窗进行隐藏处理

利用import()处理

import React,{ FC, ReactNode, useState } from 'react';
type TTest = React.PropsWithChildren<{ 
}>
const Test:FC<TTest> = ({}) => {
    const [TestModal,setTestModal] = useState<ReactNode|null>(null);
    const triggerModalShow = async () => {
        const module = await import(/* webpackChunkName: "TestModal" */ './QRCodeModal')
        const TestModal = module.default;
        const instance = React.createElement(TestModal,{
            handleCancelCB:()=>setTestModal(null),
         })
         setTestModal(instance);
    }
    return (
        <div >
            <button onClick={() => triggerModalShow()}></button>
            {TestModal}
        </div>
    )
}
export default Test;
复制代码

这种处理方式,虽然看起来,代码量增多了,但是有几个好处

  1. 利用import()实现了按需加载,在代码运行阶段,减少了非关键的资源的加载
  2. 逻辑相对集中,相当于针对Modal的所有处理,都被限制在triggerModalShow中了
  3. 页面结构相对简介,在return不需要if/else或者三元进行代码逻辑的处理

由于例子比较简介,import()的代码看起来比常规方式多,但是,一个真正的逻辑复杂的弹窗需要更多的参数,到时候就会看到使用import()的好处了。 ---自我感觉,这种处理方式,还是值得一试。


调用函数组件会发生啥?

调用React函数组件与将其作为React元素的实际区别是什么?在前面的介绍中,我们调用函数组件,在React内部 调用createElement()方法返回函数组件。当把它作为React元素使用时,其输出有什么不同。

const App = () => {
  return <p>Hello 789</p>;
};
+ console.log(App());
 {
   $$typeof: Symbol(react.element),
+  "type": "p",
   "key": null,
   "ref": null,
   "props": {
     "children": "Hello 789"
   },
   "_owner": null,
   "_store": {}
}
+ console.log(<App />);
 {
   $$typeof: Symbol(react.element),
+  "type": () => {…},
   "key": null,
   "ref": null,
   "props": {},
   "_owner": null,
   "_store": {}
}
复制代码

App()/输出略有不同。

当使用React组件作为元素,type属性变成了一个函数,其中包含了所有函数组件的实现细节(例如,childrenhooks)。

props 是被传递给组件的所有属性。代码如下:

console.log(<App className="greet" />);
 {
   $$typeof: Symbol(react.element),
   "key": null,
   "ref": null,
   "props": {
       "className": "greet"
   },
   "type": () => {…},
   "_owner": null,
   "_store": {}
 }
复制代码

对于一个真正的React应用来说,type变成了一个函数,而不再是一个字符串,这意味着什么?让我们通过一个例子来看看这个问题,它展示了为什么我们不应该调用React函数组件

首先,我们通过使用<>,按原意使用组件。

const Counter = ({ initialCount }) => {
  const [count, setCount] = React.useState(initialCount);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <div>{count}</div>
    </div>
  );
};
const App = () => {
  return (
    <div>
      <Counter initialCount={42} />
    </div>
  );
};
复制代码

此时,针对Counter,使用函数调用(Counter())和将其作为元素()效果是一样的。

const App = () => {
  return (
    <div>
      {Counter({ initialCount: 42 })}
    </div>
  );
};
复制代码

让我们再看一个例子,就会发现,不应该调用React函数组件用于渲染页面内容。我们将对渲染的子组件使用条件渲染,可以通过点击按钮来切换。

const App = () => {
  const [isVisible, setVisible] = React.useState(true);
  return (
    <div>
      <button 
        onClick={() => setVisible(!isVisible)}
      >
        切换
      </button>
      {isVisible 
        ? Counter({ initialCount: 42 }) 
        : null
      }
    </div>
  );
};
复制代码

当我们将子组件切换为不可见时,我们得到一个错误提示Uncaught Error: Rendered fewer hooks than expected。这个错误,在使用hook的时候,一不小心就会出现。原因是,组件中的hook数量和上一次不一致了。

出错原因我们知道了,但是我们按照我们代码的意愿来分析。首先hook被分配在子组件中(Counter),这意味着如果子组件被卸载,hook应该被移除而不会有任何错误。只有当一个被挂载的组件改变了它的 hook 的数量(App),它才会崩溃。

但确实它崩溃了,因为一个被挂载的组件(App)改变了它的hook数量。因为我们是以函数的形式调用子组件(Counter),React并没有把它当作React组件的一个实例。相反,它只是将子组件的所有实现细节(如hook)直接放在其父组件中。

App中触发了条件渲染,部分代码变的不可见了。但是,在这部分代码中,存在hook的使用。进而触发了hook的减少。最终结果就是React应用由于hook减少而挂掉了。

将上面调用组件的方式用另外一种代码来实现。它们是等价的。

const App = () => {
  const [isVisible, setVisible] = React.useState(true);
  return (
    <div>
      <button 
        onClick={() => setVisible(!isVisible)}
      >
        切换
      </button>
      {isVisible
        ? (() => {
            const [count, setCount] = React.useState(42);
            return (
              <div>
                <button onClick={() => setCount(count + 1)}>
                  +
                </button>
                <button onClick={() => setCount(count - 1)}>
                  -
                </button>
                <div>{count}</div>
              </div>
            );
          })()
        : null
      }
    </div>
  );
};
复制代码

这违反了hook的规则Hook必须在组件的顶层作用域调用

我们可以通过告诉React这个React组件来解决这个错误,作为回报,React会被当作一个实际的组件实例。然后它就可以在这个组件的实例中分配实现细节了。当有条件的渲染开始时,该组件就会取消挂载,并随之取消其实现细节(如钩子)。

为了解决上面的问题,我们就需要换一种处理方式,用函数组件(Counter)的实例替换函数调用。我们上面讲过,经过JSX处理后组件,会生成对应组件的实例。

const App = () => {
  const [isVisible, setVisible] = React.useState(true);
  return (
    <div>
      <button onClick={() => setVisible(!isVisible)}>
        切换
      </button>
      {isVisible 
+        ? <Counter initialCount={42} /> 
        : null
      }
    </div>
  );
};
复制代码

每个组件实例都会将组件内部实现封存起来,而不会泄漏给其他组件。

因此在利用组件来处理各种封装和业务逻辑时,使用React元素而不是在JSX中调用一个函数组件


React-Element VS React-Component

让我们总结一下React-ElementReact-Component之间的关系。

React-Component是一个组件的一次性声明,但它可以作为JSX中的React-Element使用一次或多次。

也就是说 React-ComponentReact-Element1对多的关系

在JSX中,它可以使用<>,然而,在React底层实现中,React调用createElement方法,为每个HTML元素创建React-Element

const Text = ({ children }) => {
  console.log('Text作为实例被调用');
  return <p>{children}</p>;
};
console.log('此时Text为组件',Text );
const App = () => {
  console.log('App作为实例被调用');
  const paragraphOne = <p>Hello 789!</p>;
  const paragraphTwo = <Text>React</Text>;
  console.log('此时是React-Element:', paragraphOne);
  console.log('此时是React-Element:', paragraphTwo);
  return (
    <div>
      <p>Hello React</p>
      {paragraphOne}
      {paragraphTwo}
    </div>
  );
};
console.log('此时是React-Component', App);
console.log('此时是React-Element', <App />);
console.log('此时是React-Element', <p>too</p>);
复制代码

后记

分享是一种态度

参考资料:

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
11天前
|
前端开发 JavaScript 测试技术
React 分页组件 Pagination
本文介绍了如何在 React 中从零构建分页组件,涵盖基础概念、常见问题及解决方案。通过示例代码详细讲解了分页按钮的创建、分页按钮过多、初始加载慢、状态管理混乱等常见问题的解决方法,以及如何避免边界条件、性能优化和用户反馈等方面的易错点。旨在帮助开发者更好地理解和掌握 React 分页组件的开发技巧,提升应用的性能和用户体验。
40 0
|
1月前
|
前端开发 开发者
React 函数组件与类组件对比
【10月更文挑战第4天】本文详细比较了React中的函数组件与类组件。函数组件是一种简单的组件形式,以纯函数的形式返回JSX,易于理解与维护,适用于简单的UI逻辑。类组件则是基于ES6类实现的,需要重写`render`方法并能利用更多生命周期方法进行状态管理。文章通过示例代码展示了两者在状态管理与生命周期管理上的差异,并讨论了常见的问题如状态更新异步性与生命周期管理的复杂性,最后给出了相应的解决方法。通过学习,开发者可以根据具体需求选择合适的组件类型。
56 8
|
2月前
|
前端开发 JavaScript 网络架构
react对antd中Select组件二次封装
本文介绍了如何在React中对Ant Design(antd)的Select组件进行二次封装,包括创建MSelect组件、定义默认属性、渲染Select组件,并展示了如何使用Less进行样式定义和如何在项目中使用封装后的Select组件。
100 2
react对antd中Select组件二次封装
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
169 2
|
1月前
|
前端开发 JavaScript 容器
React 元素渲染
10月更文挑战第7天
26 1
|
16天前
|
移动开发 前端开发 API
React 拖拽组件 Drag & Drop
本文介绍了在 React 中实现拖拽功能的方法,包括使用原生 HTML5 Drag and Drop API 和第三方库 `react-dnd`。通过代码示例详细讲解了基本的拖拽实现、常见问题及易错点,帮助开发者更好地理解和应用拖拽功能。
47 9
|
10天前
|
前端开发 UED 开发者
React 分页组件 Pagination
本文介绍了如何在 React 中实现分页组件,从基础概念到常见问题及解决方案。分页组件用于将大量数据分成多个页面,提升用户体验。文章详细讲解了分页组件的基本结构、快速入门步骤、以及如何处理页面跳转不平滑、页码过多导致布局混乱、边界条件处理和数据加载延迟等问题。通过本文,读者可以全面了解并掌握 React 分页组件的开发技巧。
16 2
|
14天前
|
设计模式 前端开发 编译器
与普通组件相比,React 泛型组件有哪些优势?
与普通组件相比,React 泛型组件有哪些优势?
30 6
|
22天前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
36 2
|
29天前
|
前端开发 JavaScript 测试技术
React 高阶组件 (HOC) 应用
【10月更文挑战第16天】高阶组件(HOC)是 React 中一种复用组件逻辑的方式,通过接受一个组件并返回新组件来实现。本文介绍了 HOC 的基础概念、核心功能和常见问题,包括静态方法丢失、ref 丢失、多个 HOC 组合和 props 冲突的解决方案,并提供了具体的 React 代码示例。通过本文,读者可以更好地理解和应用 HOC,提高代码的复用性和可维护性。
59 8