【React】React学习:从初级到高级(二)

React学习【二】

  • [2 添加交互](#2 添加交互)
    • [2.1 响应事件](#2.1 响应事件)
      • [2.1.1 添加事件处理函数](#2.1.1 添加事件处理函数)
      • [2.1.2 在事件处理函数中读取`props`](#2.1.2 在事件处理函数中读取props)
      • [2.1.3 将事件处理函数作为`props`传递](#2.1.3 将事件处理函数作为props传递)
      • [2.1.4 命名事件处理函数prop](#2.1.4 命名事件处理函数prop)
      • [2.1.5 事件传播](#2.1.5 事件传播)
      • [2.1.6 阻止传播](#2.1.6 阻止传播)
      • [2.1.7 传递处理函数作为事件传播的替代方案](#2.1.7 传递处理函数作为事件传播的替代方案)
      • [2.1.8 阻止默认行为](#2.1.8 阻止默认行为)
    • [2.2 State: 组件的记忆](#2.2 State: 组件的记忆)
      • [2.2.1 添加一个state变量](#2.2.1 添加一个state变量)
      • [2.2.2 Hook函数](#2.2.2 Hook函数)
      • [2.2.3 剖析useState](#2.2.3 剖析useState)
      • [2.2.3 赋予一个组件多个state变量](#2.2.3 赋予一个组件多个state变量)
      • [2.2.4 State是隔离且私有的](#2.2.4 State是隔离且私有的)
    • [2.3 渲染和提交](#2.3 渲染和提交)
    • [2.4 state在渲染时不会发生更改](#2.4 state在渲染时不会发生更改)
    • [2.5 把一系列state更新加入队列](#2.5 把一系列state更新加入队列)
      • [2.5.1 React会对state更新进行批处理](#2.5.1 React会对state更新进行批处理)
      • [2.5.2 在下次渲染前多次更新同一个state](#2.5.2 在下次渲染前多次更新同一个state)
      • [2.5.3 state更新函数的命名惯例](#2.5.3 state更新函数的命名惯例)
    • [2.6 更新state中的对象](#2.6 更新state中的对象)
      • [2.6.1 使用展开语法复制对象](#2.6.1 使用展开语法复制对象)
      • [2.6.2 使用一个事件处理函数来更新多个字段](#2.6.2 使用一个事件处理函数来更新多个字段)
      • [2.6.3 更新一个嵌套的对象](#2.6.3 更新一个嵌套的对象)
      • [2.6.4 使用Immer编写更简洁的更新逻辑](#2.6.4 使用Immer编写更简洁的更新逻辑)
    • [2.7 更新State中的数组](#2.7 更新State中的数组)
      • [2.7.1 更新数组内部的对象](#2.7.1 更新数组内部的对象)

2 添加交互

在 React 中,随时间变化的数据被称为状态(state)。

2.1 响应事件

事件处理程序是开发者自己写的的函数,它将在用户交互时被触发,如点击、悬停、焦点在表单输入框上等等。

<button> 等内置组件只支持内置浏览器事件,如 onClick。但是,开发者也可以创建自己的组件,并给它们的事件处理程序 props 指定名称。

2.1.1 添加事件处理函数

如果需要添加一个事件处理函数,需要先定义一个函数,然后将其作为prop传入合适的JSX标签.

事件处理函数有如下特点:

  1. 通常在组件内部定义
  2. 名称以handle开头,后跟事件名称

当函数体较短时,内联事件处理函数会很方便。比如:

js 复制代码
<button onClick={function handleClick() {
  alert('你点击了我!');
}}>

// 箭头函数 
<button onClick={() => {
  alert('你点击了我!');
}}>

传递给事件处理函数的函数应直接传递<button onClick={handleClick}>,而非调用<button onClick={handleClick()}>。加上()后函数会立即执行,而不是点击按钮时才执行。传递内联函数时,应该将内联事件处理函数包装在匿名函数中。

2.1.2 在事件处理函数中读取props

由于事件处理函数声明于组件内部,因此它们可以直接访问组件的 props。

2.1.3 将事件处理函数作为props传递

将组件从父组件接收的 prop 作为事件处理函数传递。

js 复制代码
function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`正在播放 ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      播放 "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('正在上传!')}>
      上传图片
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="魔女宅急便" />
      <UploadButton />
    </div>
  );
}

2.1.4 命名事件处理函数prop

  • 按照惯例,事件处理函数 props 应该以 on 开头,后跟一个大写字母。
  • 确保为事件处理程序使用适当的 HTML 标签。

2.1.5 事件传播

如果子组件定义了一个函数,那么在子组件函数被触发后,会向上冒泡到父级组件层级。

在 React 中所有事件都会传播,除了 onScroll,它仅适用于你附加到的 JSX 标签。

2.1.6 阻止传播

事件处理函数接收一个 事件对象 作为唯一的参数。按照惯例,它通常被称为 e ,代表 "event"(事件)。这个事件对象还允许阻止传播。如果想阻止一个事件到达父组件,需要调用 e.stopPropagation()

js 复制代码
function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('你点击了 toolbar !');
    }}>
      <Button onClick={() => alert('正在播放!')}>
        播放电影
      </Button>
      <Button onClick={() => alert('正在上传!')}>
        上传图片
      </Button>
    </div>
  );
}

当点击按钮时:

  1. React 调用了传递给 <button>onClick 处理函数。
  2. 定义在Button中的处理函数执行了如下操作:
    • 调用 e.stopPropagation(),阻止事件进一步冒泡。
    • 调用 onClick 函数,它是从 Toolbar 组件传递过来的 prop。
  3. Toolbar 组件中定义的函数,显示按钮对应的 alert。
  4. 由于传播被阻止,父级 <div>onClick 处理函数不会执行。

若想对每次点击进行埋点记录,可以通过在事件名称末尾添加 Capture 来实现。

js 复制代码
<div onClickCapture={() => { /* 这会首先执行 */ }}>
  <button onClick={e => e.stopPropagation()} />
  <button onClick={e => e.stopPropagation()} />
</div>

每个事件分三个阶段传播:

  1. 它向下传播,调用所有的 onClickCapture 处理函数。
  2. 它执行被点击元素的 onClick 处理函数。
  3. 它向上传播,调用所有的 onClick 处理函数。

捕获事件对于路由或数据分析之类的代码很有用。

2.1.7 传递处理函数作为事件传播的替代方案

此处的点击事件处理函数先执行了一行代码,然后调用了父组件传递的 onClick prop:

js 复制代码
function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

也可以在调用父元素 onClick 函数之前,向这个处理函数添加更多代码。

2.1.8 阻止默认行为

某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form> 表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面:

js 复制代码
export default function Signup() {
  return (
    <form onSubmit={() => alert('提交表单!')}>
      <input />
      <button>发送</button>
    </form>
  );
}

可以调用事件对象中的 e.preventDefault() 来阻止这种情况。

js 复制代码
export default function SignUp() {
    return (
        <form onSubmit={e => {
                e.preventDefault();
                alert('提交表单!');
            }}>
            <input />
            <button>发送</button>
        </form>
    );
}

2.2 State: 组件的记忆

要使用新数据更新组件,需要做两件事:

  1. 保留 渲染之间的数据。
  2. 触发 React 使用新数据渲染组件(重新渲染)。

useState Hook 提供了这两个功能:

  1. State 变量 用于保存渲染间的数据。
  2. State setter 函数 更新变量并触发 React 再次渲染组件。

2.2.1 添加一个state变量

js 复制代码
import { useState } from 'react'

const [index, setIndex] = useState[0];

function handleClick() {
    setIndex(index + 1);
}

2.2.2 Hook函数

在 React 中,useState 以及任何其他以"use"开头的函数都被称为 Hook

Hooks ------以 use 开头的函数------只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 "use" React 特性,类似于在文件顶部"导入"模块。

2.2.3 剖析useState

注意:惯例是将这对返回值命名为 const [thing, setThing]

useState 的唯一参数是 state 变量的初始值

每次你的组件渲染时,useState 都会给你一个包含两个值的数组:

  1. state 变量 (index) 会保存上次渲染的值。
  2. state setter 函数 (setIndex) 可以更新 state 变量并触发 React 重新渲染组件。

2.2.3 赋予一个组件多个state变量

可以在一个组件中拥有任意多种类型的 state 变量。

useState的实现依靠的是数组:

在 React 内部,为每个组件保存了一个数组,其中每一项都是一个 state 对。它维护当前 state 对的索引值,在渲染之前将其设置为 "0"。每次调用 useState 时,React 都会为你提供一个 state 对并增加索引值。

2.2.4 State是隔离且私有的

如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。

props 不同,state 完全私有于声明它的组件

State 变量仅用于在组件重渲染时保存信息。在单个事件处理函数中,普通变量就足够了。当普通变量运行良好时,不要引入 state 变量。比如:

js 复制代码
export default function FeedbackForm() {
  function handleClick() {
    const name = prompt('What is your name?');
    alert(`Hello, ${name}!`);
  }

  return (
    <button onClick={handleClick}>
      Greet
    </button>
  );
}

2.3 渲染和提交

React请求和提供UI的过程总共包括三个步骤:

  1. 触发渲染
    • 组件的 初次渲染。
    • 组件(或者其祖先之一)的 状态发生了改变。
  2. 渲染组件
    • 在进行初次渲染时, React 会调用根组件root
    • 对于后续的渲染, React 会调用那些使内部状态更新从而触发渲染的函数组件。
  3. 提交到DOM
    • 对于初次渲染, React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
    • 对于重复渲染, React 将只执行必要渲染操作,以使得 DOM节点 与最新的渲染输出结果匹配一致。

2.4 state在渲染时不会发生更改

一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。

2.5 把一系列state更新加入队列

2.5.1 React会对state更新进行批处理

React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。

比如

js 复制代码
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

组件的重新渲染只会发生在这三次setNumber()调用之后。

2.5.2 在下次渲染前多次更新同一个state

若多次更新同一个stateReact会将每一次state的更新状态存入队列,并把最后的结果更新到state中。这称为批处理。

以下是可以考虑传递给 setNumber state 设置函数的内容:

  • 一个更新函数 (例如:n => n + 1)会被添加到队列中。
  • 任何其他的值 (例如:数字 5)会导致"替换为 5"被添加到队列中,已经在队列中的内容会被忽略。

2.5.3 state更新函数的命名惯例

通常可以通过相应 state 变量的第一个字母来命名更新函数的参数,也可以用更明晰的命名:

js 复制代码
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

2.6 更新state中的对象

应该 把所有存放在 state 中的 JavaScript 对象都视为只读的。在改变state时,不能改变state中现有的对象,要重新创建一个对象把原来的对象替换掉。比如下面两种写法是正确且等价的:

js 复制代码
// 第一种
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

// 第二种
setPosition({
  x: e.clientX,
  y: e.clientY
});

2.6.1 使用展开语法复制对象

通常,你会希望把 现有 数据作为你所创建的新对象的一部分。例如,你可能只想要更新表单中的一个字段,其他的字段仍然使用之前的值。那么此时就可以用展开语法...

js 复制代码
import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

请注意 ... 展开语法本质是是"浅拷贝"------它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。

2.6.2 使用一个事件处理函数来更新多个字段

js 复制代码
import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value  // 重点是这里,使用 DOM 元素的 name属性
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          name="email"
          value={person.email}
          onChange={handleChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

在这里,e.target.name 引用了 <input> 这个 DOM 元素的 name 属性。

2.6.3 更新一个嵌套的对象

如果对象拥有多层嵌套,那么可以创建新的对象:

js 复制代码
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

或者写成一个函数调用:

js 复制代码
setPerson({
  ...person, // 复制其它字段的数据 
  artwork: { // 替换 artwork 字段 
    ...person.artwork, // 复制之前 person.artwork 中的数据
    city: e.target.value // 但是将 city 的值替换为 New Delhi!
  }
});

对象并非真正嵌套,只是属性"指向"彼此而已。

2.6.4 使用Immer编写更简洁的更新逻辑

Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。

使用Immer:

  1. 运行 npm install use-immer 添加 Immer 依赖
  2. import { useImmer } from 'use-immer' 替换掉 import { useState } from 'react'
js 复制代码
import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

为什么在 React 中不推荐直接修改 state?

  1. 调试时使用console.log()可以很容易发现前后两次渲染发生了什么变化.
  2. React常见的优化策略依赖于如果之前的 props 或者 state 的值和下一次相同就跳过渲染。
  3. 如果用户需求变更,可以很容易恢复到以前的版本。

2.7 更新State中的数组

同对象一样,当想要更新存储于 state 中的数组时,需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state

当操作 React state 中的数组时,需要避免使用左列能改变原数组的方法,而首选右列能返回一个新数组的方法:

避免使用(会改变原始数组) 推荐使用(返回一个新数组)
添加元素 push/unshift concat/[...arr]展开语法
删除元素 pop/shift/splice filter/slice
替换元素 splice/arr[i]=...赋值 map
排序 reverse/sort 先将数组复制一份、toSorted

或者使用Immer

2.7.1 更新数组内部的对象

即使拷贝了数组,还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝------新的数组中依然保留了与原始数组相同的元素。

比如这样就不行:

js 复制代码
const nextList = [...list];
nextList[0].seen = true; // 问题:直接修改了 list[0] 的值
setList(nextList);

正确的做法是再次拷贝一份,然后进行修改,我们可以使用map函数:

js 复制代码
setMyList(myList.map(artwork => {
  if (artwork.id === artworkId) {
    // 创建包含变更的*新*对象
    return { ...artwork, seen: nextSeen };
  } else {
    // 没有变更
    return artwork;
  }
}));

或者使用更简洁的immer

js 复制代码
updateMyTodos(draft => {
  const artwork = draft.find(a => a.id === artworkId);
  artwork.seen = nextSeen;
});

最简单的一种就是使用 ... 数组展开 语法:

js 复制代码
setArtists( // 替换 state
  [ // 是通过传入一个新数组实现的
    ...artists, // 新数组包含原数组的所有元素
    { id: nextId++, name: name } // 并在末尾添加了一个新的元素
  ]
);

数组展开运算符还允许你把新添加的元素放在原始的 ...artists 之前:

js 复制代码
setArtists([
  { id: nextId++, name: name },
  ...artists // 将原数组中的元素放在末尾
]);

这样一来,展开操作就可以完成 push()unshift() 的工作,将新元素添加到数组的末尾和开头.

从数组中删除一个元素最简单的方法就是将它过滤出去 。可以通过 filter 方法实现:

js 复制代码
// 使用filter方法删除元素
setArtists(
    artists.filter(a =>a.id !== artist.id)
);

如果想改变数组中的某些或全部元素,可以先用 map() 创建一个数组。再使用新的数组进行重新渲染。

js 复制代码
import { useState } from 'react';

let initialShapes = [
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
];

export default function ShapeEditor() {
  const [shapes, setShapes] = useState(
    initialShapes
  );

  function handleClick() {
    const nextShapes = shapes.map(shape => {
      if (shape.type === 'square') {
        // 不作改变
        return shape;
      } else {
        // 返回一个新的圆形,位置在下方 50px 处
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // 使用新的数组进行重渲染
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        所有圆形向下移动!
      </button>
      {shapes.map(shape => (
        <div
          key={shape.id}
          style={{
          background: 'purple',
          position: 'absolute',
          left: shape.x,
          top: shape.y,
          borderRadius:
            shape.type === 'circle'
              ? '50%' : '',
          width: 20,
          height: 20,
        }} />
      ))}
    </>
  );
}

插入元素

向数组特定位置插入一个元素。可以将数组展开运算符 ...slice() 方法一起使用。

js 复制代码
  function handleClick() {
    const insertAt = 1; // 可能是任何索引
    const nextArtists = [
      // 插入点之前的元素:
      ...artists.slice(0, insertAt),
      // 新的元素:
      { id: nextId++, name: name },
      // 插入点之后的元素:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

其他改变数组中元素的情况,可以先拷贝这个数组,再改变这个拷贝后的值。

相关推荐
恋猫de小郭28 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端