重学React(一):描述UI

发布于:2025-05-01 ⋅ 阅读:(14) ⋅ 点赞:(0)

背景:React现在已经更新到19了,文档地址也做了全面的更新,上一次系统性的学习还是在16-17的大版本更新。所以,现在就开始重新学习吧~

学习内容:

  1. React官网教程:https://zh-hans.react.dev/learn/describing-the-ui
  2. 其他辅助资料(看到再补充)
    补充说明:这次学习更多的是以学习笔记的形式记录,看到哪记到哪

基础知识

React 应用是由被称为 组件 的独立 UI 片段构建而成。React 组件本质上是可以任意添加标签的 JavaScript 函数
React 允许你将标签、CSS 和 JavaScript 组合成自定义“组件”,即 应用程序中可复用的 UI 元素
React 组件是一段可以使用标签进行扩展 的 JavaScript 函数,组件的名称必须以大写字母开头(React的语法规定,这样它才能分清是React组件还是正常的html标签)

// export default 导出声明
// function Profile 定义函数,function名必须首字母大写

export default function Profile() {
// return 如果换行,就必须用()将内容包裹
// 没有括号包裹的话,任何在 return 下一行的代码都将被忽略!
  return (
    <img
      src="https://i.imgur.com/MK3eW3Am.jpg"
      alt="Katherine Johnson"
    />
  )
  // 或者 标签只有一行的时候括号可以省略
   return <img src="https://i.imgur.com/MK3eW3Am.jpg" alt="Katherine Johnson" />
  )
}

// 你可以只定义组件一次,然后按需多处和多次使用
export default function Gallery() {
  return (
    <section>
      <h1>了不起的科学家</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

// 组件不建议嵌套组件定义,不然会很慢并且可能会有bug产生
export default function Gallery() {
  // 🔴 永远不要在组件中定义组件
  function Profile() {
    // ...
  }
  // ...
}

// 使用时可以import './Gallery.js' 或者 './Gallery',在 React 里都能正常使用,只是前者更符合 原生 ES 模块
import Gallery from './Gallery';
import Gallery from './Gallery.js';

导出方式

默认导出 vs 具名导出
一个文件里有且仅有一个 默认 导出,但是可以有任意多个 具名 导出
当使用默认导入时,可以在 import 语句后面进行任意命名。比如 import Banana from ‘./Button.js’。相反,对于具名导入,导入和导出的名字必须一致。
同一文件中,有且仅有一个默认导出,但可以有多个具名导出

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

JSX

  1. 只能返回一个根元素
    在一个组件中包含多个元素,需要用一个父标签把它们包裹起来,如果不想添加新的dom元素,可以使用<>...</>(Fragment),React Fragment 允许将子元素分组,而不会在 HTML 结构中添加额外节点
    原因:JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来
  2. 标签必须闭合
    这是强制规定,要么自闭合(<img />),要么添加闭合标签(<li>...</li>)
  3. 使用驼峰式命名法给大部分属性命名
    JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。组件经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制,所以需要避开这些限制
    1. 变量名称不能包含 - 符号,所以属性大部分用驼峰
    2. 变量不能用保留字如class,所以在jsx中用className代替
    3. 由于历史原因,aria-* 和 data-* 属性是以带 - 符号的 HTML 格式书写的

需要将一个字符串属性传递给 JSX 时,把它放到单引号或双引号

// "" 引号中的内容按照字符串的形式处理,单引号双引号都可以,但使用双引号会多点
// {} 大括号中的内容会被动态引用,直接在标签中使用 JavaScript,可以在其他地方声明,在使用的时候直接读取js对应的值
// 大括号内的任何 JavaScript 表达式都能正常运行
export default function Avatar() {
const alt = "Gregorio Y. Zara"
//
  return (
    <img
      className="avatar"
      src="https://i.imgur.com/7vQD0fPs.jpg"
      alt={alt}
    />
  );
}

大括号使用场景:

  1. 用作 JSX 标签内的文本:<h1>{name}'s To Do List</h1> 是有效的,但是 <{tag}>Gregorio Y. Zara's To Do List</{tag}> 无效。
  2. 用作紧跟在 = 符号后的 属性:src={avatar} 会读取 avatar 变量,但是 src="{avatar}" 只会传一个字符串 {avatar}

在JSX中还可以传递对象,对象也是用大括号表示,所以要引用对象的时候就需要使用两个括号
JSX 是一种模板语言的最小实现,因为它允许你通过 JavaScript 来组织数据和逻辑

Props

React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它,包括对象、数组和函数等

import { getImageUrl } from './utils.js';
// props是组件的唯一参数
// function里使用大括号获取props是解构
// 也可以写成这样
// function Avatar(props) {
//	const person = props.person
//	const size = props.size
// }
// 如果你想在没有指定值的情况下给 prop 一个默认值,可以通过在参数后面写 = 和默认值来进行解构
// 默认值仅在缺少 size prop 或 size={undefined} 时生效,等于null都不行
function Avatar({ person, size=100 }) {
// person 和 size 是可访问的
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

export default function Profile() {
// 这样使用不同的参数,就能展示出两个类似但是又独立的组件,这就是组件复用一个很重要的意义
  return (
    <div>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi', 
          imageId: 'YfeOqp2'
        }}
      />
      <Avatar
        size={80}
        person={{
          name: 'Aklilu Lemma', 
          imageId: 'OKS67lh'
        }}
      />
    </div>
  );
}

// 还可以使用 JSX 展开语法传递 props 
// 像这个场景,props里面所有的内容都是需要传递到Avatar组件中时,就可以直接用展开语法传递
function Profile({ person, size, isSepia, thickBorder }) {
  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}
// 可以写成这样
function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}
// 假设Profile 中isSepia不需要传递,其他都需要,还可以写成这样
function Profile({isSepia, ...rest}) {
  return (
    <div className="card">
      <Avatar {...rest} />
    </div>
  );
}

当你将内容嵌套在 JSX 标签中时,父组件将在名为 children 的 prop 中接收到该内容
通俗的来说就是某个组件标签内容,在接收的时候会自动处理成children的props
<Aaa><div>里面是一系列的内容</div> </Aaa>,在声明Aaa这个组件时,有一个隐藏props,children,表示的就是div及其包裹的内容

可以将带有 children prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”
在这里插入图片描述

import Avatar from './Avatar.js';

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>
  );
}

props 是 不可变的。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存
Props 是只读的时间快照:每次渲染都会收到新版本的 props
你不能改变 props。当你需要交互性时,你可以设置 state。

渲染

在 React 中,可以通过使用 JavaScript 的 if 语句、&& 和 ? : 运算符来选择性地渲染 JSX

  1. if /else
// 这段代码的意思是如果这个组件引用的时候返回了isPacked,那展示的时候就带勾,否则就不带
// 这种代码很适合使用在非黑即白的场景下,如果是,就展示A,其他情况都展示B
// 注意两种情况下都需要写return
function Item({ name, isPacked }) {
  if (isPacked) {
  return <li className="item">{name}</li>;
}
return <li className="item">{name}</li>;
}

// 还可以写成这样
// 这样虽然冗长,但最灵活
function Item({ name, isPacked }) {
  let itemContent = name;
  if (isPacked) {
    itemContent = name + " ✅";
  }
  return (
    <li className="item">
      {itemContent}
    </li>
  );
}

在一些情况下,你不想有任何东西进行渲染。比如,你不想显示已经打包好的物品。但一个组件必须返回一些东西。这种情况下,你可以直接返回 null
一般来说,尽量不要在子组件里返回null,而是由父组件控制是否渲染
2. 三目运算符(? :)
有时候可能需要更加紧凑的形式来进行条件渲染,这个时候三目运算符就可以使用了

// 这种写法和上面if/else的写法是完全一样的,因为JSX 元素不是“实例”,它们没有内部状态也不是真实的 DOM 节点
function Item({ name, isPacked }) {
return (
  <li className="item">
    {isPacked ? name + ' ✅' : name}
  </li>
);
}
  1. 与运算符(&&)
    在 React 组件里,通常用在当条件成立时,想渲染一些 JSX,或者不做任何渲染,这时候使用&&
    &&运算符左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但如果左侧是 false,则整个表达式会变成 false

逻辑与(&&)运算符从左到右对操作数求值,遇到第一个假值操作数时立即返回;如果所有的操作数都是真值,则返回最后一个操作数的值

在 JSX 里,React 会将 false 视为一个“空值”,不会被渲染
⚠️⚠️⚠️ 如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染(这个记得和常规的JS进行区分)

// 上面的例子也可以表现成,isPacked,才展示勾,就可以写成这样
return (
  <li className="item">
    {name} {isPacked && '✅'}
  </li>
);

渲染列表

我们也可以像在js那样使用数组的方法在jsx里渲染列表,目前示例用的filter和map

const people = [{
  id: 0,
  name: '凯瑟琳·约翰逊',
  profession: '数学家',
}, {
  id: 1,
  name: '马里奥·莫利纳',
  profession: '化学家',
}, {
  id: 2,
  name: '穆罕默德·阿卜杜勒·萨拉姆',
  profession: '物理学家',
}, {
  id: 3,
  name: '珀西·莱温·朱利亚',
  profession: '化学家',
}, {
  id: 4,
  name: '苏布拉马尼扬·钱德拉塞卡',
  profession: '天体物理学家',
}];
export default function List() {
// 这里根据具体的业务需求,用fill,reduce,concat等等都可以,并不局限于filter
  const chemists = people.filter(person =>
    person.profession === '化学家'
  );
  // 箭头函数会隐式地返回位于 => 之后的表达式,所以这里可以省略 return 语句
  // 但如果箭头函数后面带了大括号,就必须使用return
  // const listItems = chemists.map(person => {
  // return <li>...</li>;
  // });
  // 箭头函数 => { 后面的部分被称为 “块函数体”,块函数体支持多行代码的写法,但要用 return 语句才能指定返回值。假如忘了写 return,函数什么都不会返回
  const listItems = chemists.map(person =>
  // 直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值
  // key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要
    <li key={person.id}>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}{person.accomplishment}而闻名世界
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}
// 如果需要渲染多个元素,可以用div包裹,也可以用Fragment,但是不能用<></>这种简写形式,因为它不支持添加key
// Fragment不会出现在DOM上
const listItems = people.map(person =>
  <Fragment key={person.id}>
    <h1>{person.name}</h1>
    <p>{person.bio}</p>
  </Fragment>
);

直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值
key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要

  1. key 值在兄弟节点之间必须是唯一的。 不要求全局唯一,在不同的数组中可以使用相同的 key。
  2. key 值不能改变,否则就失去了使用 key 的意义!⚠️所以千万不要在渲染时动态地生成 key。

为什么React需要Key

  1. key可以作为数组中的唯一标识,即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它
  2. 最好不要使用index作为key,数组项在插入、删除或者重新排序等操作中发生改变时,可能会引起一些奇奇怪怪的bug(这个时候index会产生一些变化)
  3. 不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建
  4. 组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用

纯函数

概念:仅执行计算操作,不做其他任何操作
主要特征:

  1. 只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
  2. 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。

好处:

  1. 组件可以在不同的环境下运行 — 例如,在服务器上!由于它们针对相同的输入,总是返回相同的结果,因此一个组件可以满足多个用户请求。
  2. 为那些输入未更改的组件来 跳过渲染,以提高性能。这是安全的做法,因为纯函数总是返回相同的结果,所以可以安全地缓存它们(比如memo)。
  3. 如果在渲染深层组件树的过程中,某些数据发生了变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全地停止计算

React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX
也就是说,如果在一个组件里输入同样的内容,那渲染出来的东西在React的假设中应该是一样的

let guest = 0;

function Cup() {
  // 因为guest是全局变量,所以每一次即使没有props出现,展示的结果都会不一样
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

// 为什么会执行两次:在 React 18+ 的开发环境中运行代码时,React 会对每个组件函数执行两次,这是为了帮助发现副作用(side effect)的问题
// <React.StrictMode>,可以选择关闭StrictMode,但这是React不建议的行为
export default function TeaSet() {
  return (
    <>
      <Cup /> // guest 2
      <Cup /> // guest 4
      <Cup /> // guest 6
    </>
  );
}

当你想根据用户输入更改某些内容时,你应该设置状态(state),而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。
mutation(突变):组件改变了预先存在的变量的值,比如上面的例子,因为在组件里修改了全局的变量,导致最终的输出不符合预期,就属于一种突变
局部 mutation:在函数内部修改的变量值,它的所有处理都在函数内部完成,并不会影响函数外面的其他代码,这种突变属于可控突变

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

// 如果cups 这个数组是在TeaGathering函数外面生成的,可能会出现一些不可预料的bug,比如上面那种情况
// 但在内部生成,并且内部处理,外部没有感知,Cup组件此时也能按照预期渲染
export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

函数式编程在很大程度上依赖于纯函数,但 某些事物 在特定情况下不得不发生改变。这是编程的要义!这些变动包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 “额外” 发生的事情,与渲染过程无关
副作用通常属于 事件处理程序。事件处理程序是 React 执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在组件内部定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数
如果用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的 useEffect 方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它

这段话看起来有点难理解,我们结合上面的例子尝试用通俗的语言来看看
首先,React想要我们尽可能的使用纯函数这个初衷是不会变的,所以在可能的情况下我们需要尽可能的把组件往纯函数的方向去写
但是,有些时候会有一些跟渲染过程无关的事情发生,比如从后端获取数据渲染表格,本质上我们做的是表格的渲染,但由于后端的数据是可变的,即使对应的函数入参保持一致,渲染过程不变,但如果后端返回的数据不一样,展示结果也会不一样,这种就是副作用的表现形式之一
按照这个例子,如果是有个按钮,点击一下就请求后端接口,那这就属于一个事件处理程序,因为这需要用户的一个执行操作才能触发,跟渲染没有关系,但是执行的时候触发了数据改变,展示出来的东西也是会有变化的。
如果需要实现一打开这个页面就自动请求,没有人为的操作,这个时候就可以用useEffect,在第一次渲染结束时执行这个数据接口

React组件结构

React 以及许多其他 UI 库,将 UI 建模为树。将应用程序视为树对于理解组件之间的关系以及调试性能和状态管理等未来将会遇到的一些概念非常有用
在这里插入图片描述
React 构建的UI树是由每一个渲染过的组件生成的,被称为渲染树
在这里插入图片描述
因为React是跨平台的UI框架,React 应用程序同样可以渲染到移动设备或桌面平台,所以它以组件作为树的组成部分,在底层根据不同的平台渲染出不同的原语UI

因为有条件渲染的存在,所以每一次的渲染结果所生成的渲染树都可能会不一样,但通常这些树有助于识别 React 应用程序中的顶级和叶子组件。顶级组件是离根组件最近的组件,它们影响其下所有组件的渲染性能,通常包含最多复杂性。叶子组件位于树的底部,没有子组件,通常会频繁重新渲染

模块依赖树:将应用程序的模块依赖关系映射成树形结构
模块依赖树中的每个节点都是一个模块,每个分支代表该模块中的 import 语句
依赖树对于确定运行 React 应用程序所需的模块非常有用。在为生产环境构建 React 应用程序时,通常会有一个构建步骤,该步骤将捆绑所有必要的 JavaScript 以供客户端使用。负责此操作的工具称为 bundler(捆绑器),并且 bundler 将使用依赖树来确定应包含哪些模块

~~~~~~~~~~~~~~~~~~~~~~~~~~~
第一阶段梳理完毕,撒花🎉🎉🎉
本来以为这些基础的知识不需要花费太多时间,没想到前前后后花了快一周的空余时间,仔细看下来还是有很多意外收获,对自己现阶段的代码也有一些帮助,比如之前一直喜欢if(XXX) return null的形式,现在才知道官方就很直白的说了这样不好,再比如之前一直知道有时候用&&会渲染出来0,但是说不出所以然。还有很多细节的部分,包括对副作用的理解,对纯函数的使用,很大的感受就是有时间可以开始考虑修一下代码了(虽然老代码那个架构摇摇欲坠也不敢动太多)
接下来就继续下一个阶段吧~希望下一个阶段可以提高效率的同时,理解能更加深刻一些,又能有更多不一样的收获~~~


网站公告

今日签到

点亮在社区的每一天
去签到