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


    2 添加交互

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

    2.1 响应事件

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

  • 定义在Button中的处理函数执行了如下操作:
    • 调用 e.stopPropagation(),阻止事件进一步冒泡。
    • 调用 onClick 函数,它是从 Toolbar 组件传递过来的 prop。
  • Toolbar 组件中定义的函数,显示按钮对应的 alert。
  • 由于传播被阻止,父级
    onClick 处理函数不会执行。
  • 若想对每次点击进行埋点记录,可以通过在事件名称末尾添加 Capture 来实现。

    <div onClickCapture={() => { /* 这会首先执行 */ }}>
      <button onClick={e => e.stopPropagation()} />
      <button onClick={e => e.stopPropagation()} />
    </div>
    
    • 1
    • 2
    • 3
    • 4

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

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

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

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

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

    function Button({ onClick, children }) {
      return (
        <button onClick={e => {
          e.stopPropagation();
          onClick();
        }}>
          {children}
        </button>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    2.1.8 阻止默认行为

    某些浏览器事件具有与事件相关联的默认行为。例如,点击

    表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面:

    export default function Signup() {
      return (
        <form onSubmit={() => alert('提交表单!')}>
          <input />
          <button>发送</button>
        </form>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    export default function SignUp() {
        return (
            <form onSubmit={e => {
                    e.preventDefault();
                    alert('提交表单!');
                }}>
                <input />
                <button>发送</button>
            </form>
        );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.2 State: 组件的记忆

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

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

    useState Hook 提供了这两个功能:

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

    2.2.1 添加一个state变量

    import { useState } from 'react'
    
    const [index, setIndex] = useState[0];
    
    function handleClick() {
        setIndex(index + 1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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 变量。比如:

    export default function FeedbackForm() {
      function handleClick() {
        const name = prompt('What is your name?');
        alert(`Hello, ${name}!`);
      }
    
      return (
        <button onClick={handleClick}>
          Greet
        </button>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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 更新。

    比如

    setNumber(0 + 1);
    setNumber(0 + 1);
    setNumber(0 + 1);
    
    • 1
    • 2
    • 3

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

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

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

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

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

    2.5.3 state更新函数的命名惯例

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

    setEnabled(e => !e);
    setLastName(ln => ln.reverse());
    setFriendCount(fc => fc * 2);
    
    • 1
    • 2
    • 3

    2.6 更新state中的对象

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

    // 第一种
    const nextPosition = {};
    nextPosition.x = e.clientX;
    nextPosition.y = e.clientY;
    setPosition(nextPosition);
    
    // 第二种
    setPosition({
      x: e.clientX,
      y: e.clientY
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.6.1 使用展开语法复制对象

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

    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>
        </>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

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

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

    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>
        </>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

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

    2.6.3 更新一个嵌套的对象

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

    const nextArtwork = { ...person.artwork, city: 'New Delhi' };
    const nextPerson = { ...person, artwork: nextArtwork };
    setPerson(nextPerson);
    
    • 1
    • 2
    • 3

    或者写成一个函数调用:

    setPerson({
      ...person, // 复制其它字段的数据 
      artwork: { // 替换 artwork 字段 
        ...person.artwork, // 复制之前 person.artwork 中的数据
        city: e.target.value // 但是将 city 的值替换为 New Delhi!
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    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'
    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}
          />
        </>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

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

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

    2.7 更新State中的数组

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

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

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

    或者使用Immer

    2.7.1 更新数组内部的对象

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

    比如这样就不行:

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

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

    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // 创建包含变更的*新*对象
        return { ...artwork, seen: nextSeen };
      } else {
        // 没有变更
        return artwork;
      }
    }));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    或者使用更简洁的immer

    updateMyTodos(draft => {
      const artwork = draft.find(a => a.id === artworkId);
      artwork.seen = nextSeen;
    });
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    setArtists([
      { id: nextId++, name: name },
      ...artists // 将原数组中的元素放在末尾
    ]);
    
    • 1
    • 2
    • 3
    • 4

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

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

    // 使用filter方法删除元素
    setArtists(
        artists.filter(a =>a.id !== artist.id)
    );
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果想改变数组中的某些或全部元素,可以先用 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,
            }} />
          ))}
        </>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    插入元素

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

      function handleClick() {
        const insertAt = 1; // 可能是任何索引
        const nextArtists = [
          // 插入点之前的元素:
          ...artists.slice(0, insertAt),
          // 新的元素:
          { id: nextId++, name: name },
          // 插入点之后的元素:
          ...artists.slice(insertAt)
        ];
        setArtists(nextArtists);
        setName('');
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

  • 相关阅读:
    孔乙己脱不下的长衫:人工智能对学历的看法
    UltraISO下载与安装
    场景应用:设计一个论坛的评论回复功能
    Pandas里的Series学习
    盘点一下我用kafka两年以来踩过的一些非比寻常的坑
    Java 中数据结构LinkedList的用法
    网络、HTTP、HTTPS、Session、Cookie、UDP、TCP
    币圈是什么意思?币圈开发
    双向链表(带头双向循环链表)的实现
    TMS320C6678 DSP + Xilinx Kintex-7 FPGA核心板硬件参数资源说明分享
  • 原文地址:https://blog.csdn.net/qq_41488033/article/details/132590576