React 基础:剖析 UI 描述之道

时间:2024-12-18 07:00:03

目录

      • 介绍
      • 编写你的第一个组件
        • 定义组件
        • 使用组件
        • 组件的导入与导出
      • 使用JSX代替HTML
        • JSX规则
        • JSX中通过大括号使用JS
      • 将 Props 传递给组件
        • 向组件传递props
        • 使用 JSX 展开语法传递 props
        • 将JSX作为子组件传递
      • 条件渲染
        • 条件返回JSX
        • 三目运算符
        • 与运算符
      • 渲染列表
        • 对数组进行过滤并渲染数据
        • 用key保持列表项的顺序
      • 保持组件纯粹
        • 纯函数:组件作为公式
        • 哪些地方可能引发副作用
      • 参考

介绍

本次React文章一共有四篇,我将从描述UI,添加交互,状态管理,脱围机制去进行讲解。这部分内容参考了 React 的官方文档,如果大家对某些概念或用法有更深入的探究需求,不妨到文章末尾进入React官网,以获取最原汁原味的信息。

编写你的第一个组件

在React中,组件是核心概念之类。有点类似Java中一个一个的类,然后通过各个类的互相import去实现一个页面的组装。

定义组件

React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数。

export default function Profile() {
  return (
    <img
      src="https://img2.baidu.com/it/u=1337068678,3064275007&fm=253"
      alt="图片测试"
    />
  )
}
  1. 导出组件,export default 前缀是一种 JavaScript 标准语法。它允许你标签一个文件中的主要函数以便你以后可以从其他文件引入它。
  2. 定义函数,使用function 组件名()去定义函数。其实和写一段js代码一样,只不过组件名要大写。(为了区分组件和js函数)
  3. 添加标签,在js函数的return部分添加你想展示的标签。如果有多行,return后加小括号。
使用组件

根据以上的步骤成功定义了一个React组件。如何使用呢?

function Image() {
  return (
    <img
      src="https://img2.baidu.com/it/u=1337068678,3064275007&fm=253"
      alt="图片测试"
    />
  );
}

export default function App() {
  return (
    <section>
      <h1>一朵花</h1>
      <Image />
    </section>
  );
}

只要用放入对应位置即可。

image-20241216161232898

怎么样?是不是很神奇呢?接下来我将一步一步讲述React的核心用法与细节。

组件的导入与导出

相信编写了一个组件后你对组件有了一定的理解,它是可重复使用的,并且能在任意地方复用。

在第一个组件的例子中,因为我们把App组件和Image组件编写在了同一个js文件中,所以不需要导入导出即可直接在App组件中使用Image。

当我们的组件越来越多,而且需要在多个文件导入时,我们就需要把他们拆分开。如下。

export default function Image() {
    return (
        <img
            src="https://img2.baidu.com/it/u=1337068678,3064275007&fm=253"
            alt="图片测试"
        />
    );
}
import Image from "@/app/pages/mypage";


export default function App() {
    return (
        <section>
            <h1>一朵花</h1>
            <Image />
            <Image />
            <Image />
        </section>
    );
}

通过export default的方式导出该组件,这样在别的组件中使用import导入即可使用。

默认导出vs具名导出

语法 导出语句 导入语句
默认 export default function Button() {} import Button from './Button.js';
具名 export function Button() {} import { Button } from './Button.js';

当使用默认导入时,你可以在 import 语句后面进行任意命名。比如 import Banana from './Button.js',如此你能获得与默认导出一致的内容。相反,对于具名导入,导入和导出的名字必须一致。这也是为什么称其为 具名 导入的原因!

一个文件中只能包含一个默认导出,但可以包含多个具名导出。

使用JSX代替HTML

JSX是JavaScript语法扩展,可以让你在avaScript 文件中书写类似 HTML 的标签。该方法十分简洁,一起来看看吧!

JSX规则

只能返回一个根元素

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当你尝试return两个根元素将会报错。

解决办法:

//第一种,在元素外包一个div   
return (
    <div>
       <section>
       </section>
       <section>
       </section>
    </div>
);
//第二种,在元素外包一个空标签
return (
    <>
       <section>
       </section>
       <section>
       </section>
    </>
);

这个空标签被称作 Fragment。React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。

JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。

标签必须闭合

JSX 要求标签必须正确闭合。像 <img> 这样的自闭合标签必须书写成 <img />,而像 <li>oranges 这样只有开始标签的元素必须带有闭合标签,需要改为 <li>oranges</li>

驼峰命名

需要用 strokeWidth 代替 stroke-width。由于 class 是一个保留字,所以在 React 中需要用 className 来代替。

最后建议你用JSX转换器

可以将现有的HTML代码转为JSX,这样可以省去繁琐的步骤。

JSX中通过大括号使用JS

使用引号传递字符串

当你想把一个字符串属性传递给 JSX 时,把它放到单引号或双引号中:

export default function Image() {
    const avatar = 'https://img2.baidu.com/it/u=1337068678,3064275007&fm=253';
    const description = '图片';
    return (
        <img
            className="avatar"
            src={avatar}
            alt={description}
        />
    );
}

通过{}可以进行属性赋值。

可以在哪使用大括号

两种情况,一个是标签的属性值,一个是标签的内容值。

<img
   className="avatar"
   src={avatar}
   alt={description}
/>
     
<h1>{}abc</h1>       

JSX中的CSS和对象

上述说的{}可以传字符串,传js函数,传数字。

但其实还可以传递对象和CSS

export default function TodoList() {
  return (
    <ul style={{
      backgroundColor: 'black',
      color: 'pink'
    }}>
      <li>Improve the videophone</li>
      <li>Prepare aeronautics lectures</li>
      <li>Work on the alcohol-fuelled engine</li>
    </ul>
  );
}

CSS和对象一样,本身都用{}包起来,所以为了在JSX中使用它,你必须再包一层{}。

所以当你在JSX中看到{{}},其实就是一个对象罢了。

当然你可以按对象的使用方法去使用person.

const person = {
  name: 'Gregorio Y. Zara',
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

export default function TodoList() {
  return (
    <div style={person.theme}>
      <h1>{person.name}'s Todos</h1>
      <img
        className="avatar"
        src="https://i.imgur.com/7vQD0fPs.jpg"
        alt="Gregorio Y. Zara"
      />
      <ul>
        <li>Improve the videophone</li>
        <li>Prepare aeronautics lectures</li>
        <li>Work on the alcohol-fuelled engine</li>
      </ul>
    </div>
  );
}

将 Props 传递给组件

React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。

向组件传递props

步骤1 将props传递给子组件

export default function Profile() {
  return (
    <Avatar
      person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
      size={100}
    />
  );
}

步骤2 在子组件中读取props

function Avatar({ person, size }) {
  //通过在function Avatar后直接列出props的名字,以直接在组件中使用他们的值。
    return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

通常你不需要整个 props 对象,所以可以将它解构为单独的 props。

//可以通过props接收参数进行使用。
function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}

优点:props可以使父子组件分离,在定义子组件时无需在意属性值只需要在意结构,在父组件中使用的时候给定他们的属性值。

给组件一个默认值

function Avatar({ person, size = 100 }) {
  // ...
}

默认值仅在缺少 size prop 或 size={undefined} 时生效。 但是如果你传递了 size={null}size={0},默认值将 被使用。

使用 JSX 展开语法传递 props

先来看下面这个例子

function Profile({ person, size, isSepia, thickBorder }) {
  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}

父组件Profile接收到props后都传递给了Avatar组件。对于这种接收到的组件全传给了子组件,并且自己没有使用,那么我们就可以用展开语法

function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}
将JSX作为子组件传递

Card将收到一个被设为 <Avatar />children prop 并将其包裹在 div 中渲染。

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

条件渲染

条件返回JSX
function Item({ name, isPacked }) {
  if (isPacked) {
    return <li className="item">{name}</li>;
  }
  return <li className="item">{name}</li>;
}

export default function PackingList() {
  return (
    <section>
      <h1>Sally Ride 的行李清单</h1>
      <ul>
        <Item 
          isPacked={true} 
          name="宇航服" 
        />
      </ul>
    </section>
  );
}

通过if/else语句,动态的决定该Item如何显示

三目运算符
return (
  <li className="item">
    {isPacked ? name + ' ✔' : name}
  </li>
);
与运算符

在 React 组件里,通常用在当条件成立时,你想渲染一些 JSX,或者不做任何渲染。使用 &&,你也可以实现仅当 isPackedtrue 时,渲染勾选符号。

return (
  <li className="item">
    {name} {isPacked && '✔'}
  </li>
);

isPacked 为真值时,则(&&)渲染勾选符号,否则,不渲染。

当js中&&左侧为true时则会返回其右侧的值。当左侧为false那么整个表达式就是false,JSX中把false看作null或undifiend,这样React就不会进行渲染。

渲染列表

对数组进行过滤并渲染数据

这里我们要用到两个方法

  • filter() 过滤数据
  • map() 渲染数据

我们直接来看一个例子

首先我们给定一个数组

const people = [
  {
    id: 0,
    name: '凯瑟琳·约翰逊',
    profession: '数学家',
  },
  {
    id: 1,
    name: '马里奥·莫利纳',
    profession: '化学家',
  },
  {
    id: 2,
    name: '*·阿卜杜勒·萨拉姆',
    profession: '物理学家',
  },
  {
    name: '珀西·莱温·朱利亚',
    profession: '化学家',
  },
  {
    name: '苏布拉马尼扬·钱德拉塞卡',
    profession: '天体物理学家',
  },
];

如果我们只想获取其中是化学家的人,那我们要进行过滤

const chemists = people.filter(person =>
    person.profession === '化学家'
);

通过这样的方式,现在chemists中包含所有profession属性为化学家的数组元素,此时如果我们想渲染。

const listItems = chemists.map(person =>
    <li>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}{person.accomplishment}而闻名世界
      </p>
    </li>
  );
return <ul>{listItems}</ul>;

通过map函数,将每一个数组元素按自己想要的方式进行描述渲染。最后通过return

  • {listItems}
;返回

用key保持列表项的顺序

如果把之前的案例在标签页打开,那么会显示一个错误.

Warning: Each child in a list should have a unique “key” prop.

它的意思是数组的孩子节点没有独一无二的key值。因此你要给每个节点一个key可以唯一标识它。

<li key={person.id}>...</li>

如何设定key值

如果你获取的数据是从后端传输过来,那么它来自数据库,key值直接使用数据库表的主键id即可。

如果你的数据是本地产生的,那么你可以使用uuid来生成key。

key需要满足的条件

key值在兄弟节点间必须唯一,你可以在不同数组间使用相同的id。

key值不能改变,所以不要在渲染的时候动态生成key,要提前准备好。

保持组件纯粹

纯函数:组件作为公式
function double(number) {
  return 2 * number;
}

对于上面这个函数就是纯函数,你给他传入的值是确定的,那么它的输出就是确定的,不会因为多次调用而改变。

React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX。

反例是什么样的

这意味着,当父组件给子组件通过props传递参数后,子组件只能对props进行使用而不可以更改,不然多次调用子组件就会造成不同的结果,比如下面这个例子

let guest = 0;

function Cup() {
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
    </>
  );
}

在这个例子中Cup组件将从全局获取到的guest变量进行了更改,那么每次调用Cup组件guest又不一样的结果,不是纯函数,违背原则。

什么情况可以更改

当变量是组件中定义的,那么可以更改。

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

之前的反例中的问题是组件更改了预先存在的变量的值,这种情况我们把它叫做突变

但对于现在这个例子,你完全可以改变刚刚创建的变量和对象

哪些地方可能引发副作用

虽然React函数式编程很大程度依赖纯函数,但某些地方不得不变,比如更新屏幕,更改数据,他们被称为副作用.他们是额外发生的事,和渲染无关。

在React中,副作用通常属于事件处理程序。这种程序无需纯函数,因为它是用户进行某些操作比如点击后才会触发的,所以它即使在你的组件内部定义,那么在渲染的时候也不会运行。

如果你用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的 useEffect 方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它。然而,这种方法应该是你最后的手段

参考

React中文文档