[React 基础系列] React 中的 元素 vs 组件

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[React 基础系列] React 中的 元素 vs 组件相关的知识,希望对你有一定的参考价值。

在这次复习官方教程之前,我还没意识到 元素(element) 和 组件(component) 在 React 中的定义还是有些不太一样的。通俗理解就是,组件是由元素所构成的。

第一个复习的系列是 JSX 的内容:什么是 JSX,以及如何使用 JSX

元素

官网上是这么描述元素的:

Elements are the smallest building blocks of React apps.

元素是构成 React 应用的最小砖块。

这个翻译我真的觉得像是机翻

直白的说法就是,元素是 React 应用中可被创建的最基本的组成部分,没有比元素更小的组成部分了。依旧是用 Hello World 为例,如果想要在页面上渲染 Hello World,最简单的写法如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const element = <h1>Hello World</h1>;

ReactDOM.render(element, document.getElementById('root'));

效果如图下:

这么看来,元素的写法其实还颇有一种,所思即所见 的感觉,也就是说,你想在页面上呈现的内容,就是写出来的内容。

在这个案例中,如果想要渲染一个 h1 的标签,其内容就是 Hello World,那么需要定义一个元素,标签为 <h1>,定义的元素内容为 Hello World,最后将其挂载到页面上即可。

元素的渲染

在上一篇 什么是 JSX 部分中曾经讲过,其实 JSX 最终会被编译成一个 javascript 对象,如:

const element = <h1 className="greeting">Hello, world!</h1>;

// 实际上经过编译之后等同于
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!',
  },
};

单纯的 JavaScript 对象是没有办法直接被渲染到页面上的,因此它需要借助另一个函数—— ReactDOM.render() ——将 JavaScript 对象渲染成实际的 DOM 元素。ReactDOM.render() 的使用语法为:

ReactDOM.render(element, document.getElementById('root'));

其中,element 是创建的 JavaScript 对象;

roothtml 页面上的真实存在的元素。

完整的页面结构为:

|- public
|  |- index.html => 作为index.js的入口文件
|- src
|  |- index.js => 作为整个项目的入口文件

index.html 中一般只会存在一个 root 元素作为整个 DOM 树的根节点,ReactDOM.render() 会获取这个根节点,并负责将创建的元素由 JavaScript 对象转化为页面上展示的 DOM 节点。

元素的更新

React 的很多核心设计都是基于不可变性,React 中的元素也是如此。这也就意味着一旦一个 React 的元素被创建成功之后,元素的属性、子元素便无法被更新,而唯一更新 UI 的方式就是创建一个新的元素,并且将新的元素传入到 ReactDOM.render() 之中。

如果 React 只是在每一次有 UI 变动时,就更新一次完整的 DOM 树,那么这就和传统非常耗时的 DOM 操作没什么区别。

不过,React 内部有自己的更新机制,能够很好的对操作 DOM 进行优化,减少无意义的重复渲染造成的性能损耗。

Virtual DOM

Virtual DOM 就是 React 实现的 DOM 更新优化机制,它将 DOM 树进行虚拟化的操作后存入内存之中。虚拟的 DOM 树与真实的 DOM 树会通过如 ReactDOM 之类的库进行连接。

使用虚拟 DOM 树的优点非常的明显,在对 DOM 进行操作时,直接更新原有的 DOM 树是非常耗时的操作。但是 ReactDOM 会对两棵树——虚拟 DOM 树和真实 DOM 树进行对比,只重新渲染更新过的 DOM 节点,这样就能够省去大量无意义的重新渲染,从而提升性能。

对于 React 团队来说,他们的理念是专注于当下的页面看起来应当是什么模样,而非如何在未来更新 UI。

组件

组件拥有以下几个特性:

  • 是一部分 UI
  • 这一部分 UI 是 独立(independent)可复用 的片段
  • 这一部分 UI 可 孤立(isolate) 于其他的片段单独存在
  • 可以通过 props 去获得从外部传来的数据

和 元素 对比一下:

// 组件
// 函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
// 类组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// 这里的 Welcome 返回的是自定义元素
// 调用的方法都是一样的
const elementOfComponent = <Welcome />;

能够很明显的发现,元素的概念是一个 JavaScript 的对象;而组件的概念类似于一个 JavaScript 的函数。

注:

自定义的组件必须以大写字母开头

小写字母开头的开头的组件将会被试做是原生 DOM 的标签

组件的渲染

以案例中的 Welcome 为例,这个时候的返回值是空白的:

原因是因为外部没有传进 name 这个属性给 Welcome组件,在获取不到数据的情况下,props.name 的值就是 undefined,因此页面上只会显示 Hello,

现在,就将 name 传给 Welcome组件,以此展示一下 props 的调用。

// 组件的实现方式不变
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// 仅修改调用方式
const element = <Welcome name="John" />;

// 具体页面的渲染还是需要依靠 ReactDOM 去实现
ReactDOM.render(element, document.getElementById('root'));

这时候,页面显示的就是更有意义的内容:

组合组件

传统意义上来说,组合组件有这么会有 父子兄弟 这两种关系。

父子组件

父子组件也就是在组件之中继续嵌套一个组件,例如说一个主页面中可以嵌套一个导航栏和正文内容:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const Nav = () => {
  return (
    <nav>
      <ol>
        <li className="nav-list">
          <span>homepage</span>
        </li>
        <li className="nav-list">
          <span>articles</span>
        </li>
        <li className="nav-list">some article</li>
      </ol>
    </nav>
  );
};

const MainContent = () => {
  return (
    <div className="main-content">
      <h1>Header</h1>
      <p>some content</p>
    </div>
  );
};

const MainPage = (props) => {
  return (
    <>
      <Nav />
      <MainContent />
    </>
  );
};

const component = <MainPage name="John" />;

ReactDOM.render(component, document.getElementById('root'));

效果图如下:

这个页面的结构如下:

|- MainPage
|  |- Nav
|  |- MainContent

这其中,MainPageNav/MainContent 就是父子关系。又因为 React 的数据是单向传输的,所以要传数据的话,数据只能从 MainPage 传向 Nav/MainContent

兄弟组件

同样是上面的案例,NavMainContent 是并列存在的,因此他们之间的关系就是兄弟关系。

在 React 之中,数据是单向传输的,只能通过 props 从 父 传到 子,兄弟之间也无法交换信息。所以 NavMainContent 之间是没有办法进行数据的交换的。

另外,要渲染兄弟组件有以下几种方式:

  1. 将他们包裹在一个根元素之中,根元素作为他们的父元素

    这种写法适合兄弟之间有一定的关联的情况下,这种常见的案例有导航栏下的 li,它们可以隶属于 nav 下作为子组件。

  2. 如果兄弟组件之间没有特别明显的层级关系,又不想额外嵌套一个 <div> </div> 在页面上去增加复杂度,可以使用 <React.Fragment> <Component /> </React.Fragment>

    React.Fragment 是 React 提供的语法糖,这时候 React 会生成一个空的父节点,语法上是正确的,但是不会增加额外的节点,也就不会增加额外的复杂度。

  3. 上文使用的 <><Component /></>React.Fragment 的语法糖,本质上来说和 React.Fragment 的效果是一样的,只是更加省事一些。

    这个写法应该是 v17 之后推出的新写法,v16 无法兼容。

组件的提取

如何合理地对组件进行提取以达到更好的复用性,一直都是一个比较复杂的问题。根据 React 的官方建议来说,当组件满足以下两个特性,就代表这个部分可以被提取成单独的组件:

  • 被多次重复使用的 UI 部分

    以 苹果 为例,页面上的一些 UI 部分都有一定程度的相似性,如:

    这几个产品介绍的设计结构其实都是一样的:

    |- 组件(有不同的背景色)
    |  |- header,商品名
    |  |- 商品卖点
    |  |- 让客户选择查看更多,和选择购买的超链接
    |  |- 商品图片
    

    这种情况下,就可以选择将这几个部分组成一个组件,随后只要通过 props 将对应的属性传进去,就可以有效的降低开发时间,提升开发效率。

  • 当组件有一定程度的复杂性

    以淘宝为例:

    如果直接进行开发的话,这个页面实现起来绝对会有相当程度的复杂性。全都写在一个组件之中对于页面的更新(如出现不同的活动)、后期的维护也会形成一个极大的挑战。

    但是如果将页面根据功能进行简单的拆分,那么按组件实现的难度就会降低不少:

    根据划分的区域可以大致分为:

    |- page,整个页面
    |  |- 导航栏,导航栏组件在其他页面也可以复用,之后直接引用即可
    |  |- 搜索,搜索组件在其他页面也可以复用,之后直接引用即可
    |  |- 首页的主内容
    |  |  |- 这中间还可以继续拆分,这里不再细讲
    |  |- 侧边栏,侧边栏在其他页面也可以复用,之后直接引用即可
    

    可以看到,这里粗粗划分了四个组件,三个组件是可以在其他页面直接复用。也就是说,在实现其他页面时,就不需要再去重复实现已经实现过的功能。

总结

本章学习内容主要学习了一下 元素 和 组件,知识点包括:

  • 元素

    • 元素是 React 应用的最小组成部分

    • 元素被编译完成后是一个 JavaScript 对象

    • 元素的渲染是通过 ReactDOM.render() 实现的

    • 元素具有不可变性,需要更新元素只能重新创建一个元素,并传入 ReactDOM.render() 中重新渲染

    • React 实现了 Virtual DOM,会对比更新前后的虚拟 DOM 树,并且只重新渲染更新过的 DOM 节点

  • 组件

    • 组件是可独立复用的 UI 代码段

    • 组件类似于 JavaScript 中的函数

    • 组件可以组合使用

    • 为了方便实现以及维护,可以提取组件,做更细颗粒的实现

下一章的学习将会注重与组件的状态管理以及生命周期。

以上是关于[React 基础系列] React 中的 元素 vs 组件的主要内容,如果未能解决你的问题,请参考以下文章

react源码系列 — 创建元素组件

react源码系列 — 创建元素组件

React技术栈系列—基础01

React 系列导航

深入解析React中的元素组件实例和节点

深入解析React中的元素组件实例和节点