React交互

266 阅读15分钟

摘抄自React官方文档

在React中,随着时间变化的数据被称为状态(state)。你可以向任何组件中添加状态,并按需进行更新。

响应事件

事件处理函数的使用

  • 你需要先定义一个函数,然后 将其作为 prop 传入 合适的 JSX 标签。

  • 按照惯例,通常将事件处理程序命名为 handle,后接事件名。你会经常看到 onClick={handleClick}onMouseEnter={handleMouseEnter} 等。

  • 你可以作为props传递事件处理函数

  • 事件处理函数 props 应该以 on 开头,后跟一个大写字母。例如onClick

export default function Button() {
  function handleClick() {
    alert('你点击了我!');
  }

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

------------------------------------------------
<button onClick={() => {  
  alert('你点击了我!');  
}}>

事件传播

事件处理函数还将捕获任何来自子组件的事件。通常,我们会说事件会沿着树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播。

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

如果你点击任一按钮,它自身的 onClick 将首先执行,然后父级 <div>onClick 会接着执行。因此会出现两条消息。如果你点击 toolbar 本身,将只有父级 <div>onClick 会执行。

阻止事件传播

事件处理函数接收一个 事件对象 作为唯一的参数。按照惯例,它通常被称为 e ,代表 “event”(事件)。你可以使用此对象来读取有关事件的信息。

这个事件对象还允许你阻止传播。如果你想阻止一个事件到达父组件,你需要像下面 Button 组件那样调用 e.stopPropagation()

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 处理函数不会执行。

由于调用了 e.stopPropagation(),点击按钮现在将只显示一个 alert(来自 <button>),而并非两个(分别来自 <button> 和父级 toolbar <div>)。点击按钮与点击周围的 toolbar 不同,因此阻止传播对这个 UI 是有意义的。

阻止默认行为

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

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

你可以调用事件对象中的 e.preventDefault() 来阻止重新加载整个页面的情况发生

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

不要混淆 e.stopPropagation()e.preventDefault()。它们都很有用,但二者并不相关:

事件处理函数和副作用

事件处理函数是执行副作用的最佳位置。

  • 访问组件状态和属性:事件处理函数可以直接访问组件的状态(state)和属性(props),并根据需要进行操作。这使得事件处理函数能够响应用户的操作,并更新组件的状态或触发其他逻辑。

  • 生命周期钩子函数:事件处理函数可以在组件的生命周期钩子函数中被调用,这使得我们可以更好地控制副作用的发生时机。例如,在 componentDidMount 钩子函数中添加事件监听器,可以确保在组件挂载到 DOM 后才开始监听事件。

React-State

state的由来及其基本使用

组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。组件需要“记住”某些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆被称为 state

当你调用 useState 时,你是在告诉 React 你想让这个组件记住一些东西,写在useState 的唯一参数是 state 变量的初始值

useState Hook 提供了这两个功能:

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

这里的 [] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项。

import { useState } from 'react';
const [index, setIndex] = useState(0);

Hook

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

Hook 是特殊的函数,只在 React 渲染时有效(我们将在下一节详细介绍)。它们能让你 “hook” 到不同的 React 特性中去。

image.png image.png

State 是隔离且私有的

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

渲染和提交

组件显示到屏幕之前,其必须被 React 渲染。

步骤一:触发渲染

有两种原因会导致组件的渲染:

  1. 组件的 初次渲染。
  2. 组件(或者其祖先之一)的 状态发生了改变。

初次渲染

当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它是通过调用目标 DOM 节点的 createRoot,然后用你的组件调用 render 函数完成的

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);
// 试着注释掉 `root.render()`,然后您将会看到组件消失。

状态更新时重新渲染

一旦组件被初次渲染,您就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。(您可以想象这种情况成餐厅客人在第一次下单之后又点了茶、点心和各种东西,具体取决于他们的胃口。)

步骤二:React渲染你的组件

在您触发渲染后,React 会调用您的组件来确定要在屏幕上显示的内容。 “渲染过程” 就是React在调用你的组件“正在渲染” 就意味着 React 正在调用你的组件——一个函数。

  • 在进行初次渲染时,  React 会调用根组件。
  • 对于后续的渲染,  React 会调用内部状态更新触发了渲染的函数组件,即存在state的组件。

这是一个递归的过程: 如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。

export default function Gallery() {
  return (
    <section>
      <h1>鼓舞人心的雕塑</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}

function Image() {
  return (
    <img
      src="https://i.imgur.com/ZF6s192.jpg"
      alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
    />
  );
}
  • 在初次渲染中,  React 将会为<section><h1> 和三个 <img> 标签 创建 DOM 节点
  • 在一次重渲染过程中,  React 将计算它们的哪些属性(如果有的话)自上次渲染以来已更改。在下一步(提交阶段)之前,它不会对这些信息执行任何操作。

步骤 3: React 把更改提交到 DOM 上

渲染(调用)您的组件之后,React 将会修改 DOM。

  • 对于初次渲染,  React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
  • 对于重新渲染,React 仅在渲染之间存在差异时才会更改 DOM 节点。使得 DOM 与最新的渲染输出相互匹配。

步骤4:浏览器绘制

在渲染完成并且 React 更新 DOM 之后,浏览器就会重新绘制屏幕。

state渲染快照

以下是一个state渲染的一个例子,点击一次该组件的按钮,number只会递增一次而不是+3。

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

连续调用了三次 setNumber(number + 1),由于这三次调用是在同一次渲染周期内执行的,它们都是基于同一个初始值 number=0 来计算新的状态值,并将这些更新操作加入更新队列。然后,React 在组件渲染完成后才会去批量更新状态,将所有更新操作应用到最新的状态值上。

所以,当你连续点击按钮时,setNumber(number + 1) 在同一次渲染周期内执行了三次,但它们都是基于初始值 number=0 计算得到的,因此最终的结果是 number=1,而不是期望的 number=3。如果你想实现每次点击按钮都加3的效果,可以使用函数式更新来解决:

<button onClick={() => {
  setNumber(prevNumber => prevNumber + 1);
  setNumber(prevNumber => prevNumber + 1);
  setNumber(prevNumber => prevNumber + 1);
}}>+3</button>

通过函数式更新,每个 setNumber 都会接收到前一个状态值作为参数,从而保证每次更新都是基于前一个状态值进行计算,最终实现每次点击按钮都加3的效果。

state

state渲染快照

一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。在 那次渲染的 onClick 内部,number 的值即使在调用 setNumber(number + 5) 之后也还是 0。它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        alert(number);
      }}>+5</button>
    </>
  )
}

把一系列 state 更新加入队列

设置组件 state 会把一次重新渲染加入队列。但有时你可能会希望在下次渲染加入队列之前对 state 的值执行多次操作。为此,了解 React 如何批量更新 state 会很有帮助。

每一次渲染的 state 值都是固定的,因此无论你调用多少次 setNumber(1),在第一次渲染的事件处理函数内部的 number 值总是 0

React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。 这就是为什么重新渲染只会发生在所有这些 setNumber() 调用 之后 的原因。

这让你可以更新多个 state 变量——甚至来自多个组件的 state 变量——而不会触发太多的重新渲染。但这也意味着只有在你的事件处理函数及其中任何代码执行完成 之后,UI 才会更新。

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

更新state中的对象

在 React 中,你需要将 state 视为不可变的!你应该把所有存放在 state 中的 JavaScript 对象都视为只读的。不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。

注意:在事件处理函数中需要调用setThing才能触发React对组件的重新渲染,若使用obj.key = value直接修改存放在useState()对象,由于没有调用setThing()所以虽然改变你了state但是没有触发渲染。

创建一个新的对象

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

// 为了真正地 触发一次重新渲染,你需要创建一个新对象并把它传递给 state 的设置函数
onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

用展开语法复制原对象

你可以使用 ... 对象展开 语法,这样你就不需要单独复制每个属性。

setPerson({  
  ...person, // 复制上一个 person 中的所有字段  
  firstName: e.target.value // 但是覆盖 firstName 字段  
});

更新一个嵌套对象

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

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

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

如果你的 state 有多层的嵌套,你或许应该考虑 将其扁平化。但是,如果你不想改变 state 的数据结构,你可能更喜欢用一种更便捷的方式来实现嵌套展开的效果。Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。

updatePerson(draft => {  
  draft.artwork.city = 'Lagos';  
});

更新state中的数组

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

image.png

image.png

向数组中添加元素

你应该创建一个 数组,其包含了原始数组的所有元素 以及 一个在末尾的新元素。这可以通过很多种方法实现,最简单的一种就是使用 ... 数组展开 语法:

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

// 数组展开运算符还允许你把新添加的元素放在原始的 `...artists` 之前:
// 将元素添加在数组开头
setArtists([  
    { id: nextId++, name: name },  
    ...artists // 将原数组中的元素放在末尾  
]);

从数组中删除元素

从数组中删除一个元素最简单的方法就是将它过滤出去。换句话说,你需要生成一个不包含该元素的新数组。这可以通过 filter 方法实现,例如:

import { useState } from 'react';

let initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [artists, setArtists] = useState(
    initialArtists
  );

  return (
    <>
      <h1>振奋人心的雕塑家们:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

转换数组

如果你想改变数组中的某些或全部元素,你可以用 map() 创建一个数组。你传入 map 的函数决定了要根据每个元素的值或索引(或二者都要)对元素做何处理。

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

替换数组中的元素

要替换一个元素,请使用 map 创建一个新数组。在你的 map 回调里,第二个参数是元素的索引。使用索引来判断最终是返回原始的元素(即回调的第一个参数)还是替换成其他值

import { useState } from 'react';

let initialCounters = [
  0, 0, 0
];

export default function CounterList() {
  const [counters, setCounters] = useState(
    initialCounters
  );

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // 递增被点击的计数器数值
        return c + 1;
      } else {
        // 其余部分不发生变化
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

向数组中插入元素

有时,你也许想向数组特定位置插入一个元素,这个位置既不在数组开头,也不在末尾。为此,你可以将数组展开运算符 ...slice() 方法一起使用。slice() 方法让你从数组中切出“一片”。为了将元素插入数组,你需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。

下面的例子中,插入按钮总是会将元素插入到数组中索引为 1 的位置。

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

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

  return (
    <>
      <h1>振奋人心的雕塑家们:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        插入
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

其他改变数组的情况

是你仅仅依靠展开运算符和 map() 或者 filter() 等不会直接修改原值的方法所无法做到的。例如,你可能想翻转数组,或是对数组排序。而 JavaScript 中的 reverse()sort() 方法会改变原数组,所以你无法直接使用它们。

然而,你可以先拷贝这个数组,再改变这个拷贝后的值。

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        翻转
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

然而,即使你拷贝了数组,你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。因此,如果你修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。举个例子,像下面的代码就会带来问题。

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

nextListlist 是两个不同的数组,nextList[0]list[0] 却指向了同一个对象。因此,通过改变 nextList[0].seenlist[0].seen 的值也被改变了。看以下解决方案

更新数组内部的对象

对象并不是 真的 位于数组“内部”。可能他们在代码中看起来像是在数组“内部”,但其实数组中的每个对象都是这个数组“指向”的一个存储于其它位置的值。

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

此处的 ... 是一个对象展开语法,被用来创建一个对象的拷贝.