【2024官方文档版】React-添加交互

系列文章目录

一、 React快速入门

二、React描述IU

三、React添加交互


文章目录

  • 系列文章目录
  • 前言
  • 三、添加交互
    • 1.响应事件
    • [2. State](#2. State)
      • [2.1 添加state变量](#2.1 添加state变量)
      • [2.2 Hook](#2.2 Hook)
      • [2.3 useState详解](#2.3 useState详解)
      • [2.4 使用多个state变量](#2.4 使用多个state变量)
      • [2.5 小结](#2.5 小结)
    • [3. 渲染和提交](#3. 渲染和提交)
      • [3.1 步骤一:触发一次渲染](#3.1 步骤一:触发一次渲染)
      • [3.2 步骤二:React渲染组件](#3.2 步骤二:React渲染组件)
      • [3.3 步骤三:React把更改提交到DOM](#3.3 步骤三:React把更改提交到DOM)
      • 3.4小结
    • [4. state如同一张快照](#4. state如同一张快照)
      • 4.1设置state会触发渲染
      • [4.2 渲染会及时生成一张快照](#4.2 渲染会及时生成一张快照)
      • [4.3 随时间变化的state](#4.3 随时间变化的state)
      • [4.4 小结](#4.4 小结)
      • [4.5 作业](#4.5 作业)
    • [5. 把一系列的state更新加入队列](#5. 把一系列的state更新加入队列)
      • [5.1 React对state更新进行批处理](#5.1 React对state更新进行批处理)
      • [5.2 在下次渲染前多次更新同一个 state](#5.2 在下次渲染前多次更新同一个 state)
      • [5.3 替换 state 后更新 state](#5.3 替换 state 后更新 state)
      • [5.4 关于更新函数](#5.4 关于更新函数)
      • 5.5小结
    • 6.更新state中的对象
      • [6.1 mutation](#6.1 mutation)
      • [6.2 将state视为只读](#6.2 将state视为只读)
      • [6.3 展开语法复制对象](#6.3 展开语法复制对象)
      • 6.4更新一个嵌套对象
      • [6.5 Immer简洁逻辑](#6.5 Immer简洁逻辑)
    • [7. 更新state中的数组](#7. 更新state中的数组)
      • [7.1 使用数组方法更新数组](#7.1 使用数组方法更新数组)
        • [7.1.1 像数组中添加元素concat/[...arr]](#7.1.1 像数组中添加元素concat/[...arr])
        • 7.1.2从数组中删除元素filter/slice
        • [7.1.3 替换(修改)数组中的元素](#7.1.3 替换(修改)数组中的元素)
        • [7.1.4 数组中插入元素](#7.1.4 数组中插入元素)
        • [7.1.5 其他改变数组的情况](#7.1.5 其他改变数组的情况)
      • [7.2 更新数组内部的对象](#7.2 更新数组内部的对象)
      • [7.3 Immer简洁逻辑](#7.3 Immer简洁逻辑)
      • [7.4 小结](#7.4 小结)
    • 总结

前言

界面上的一些元素(比如文本框、按钮、下拉菜单等)会根据用户在这些元素上输入或者选择的内容而发生变化,例如,点击按钮切换轮播图的展示。

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

在本章节中,你将学习如何编写处理交互 的组件,更新它们的状态,并根据时间变化显示不同的效果。

继续往下看吧 ^皿^

三、添加交互

1.响应事件

React可以在JSX添加事件处理函数
事件处理函数:自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发

1.1添加事件处理函数

步骤一:定义一个Button组件,内部声明一个handleClick函数

javascript 复制代码
function Button() {
    return (
        <button>
            点击弹出提示
        </button>
    )
}

步骤二:实现内部逻辑(栗子中的逻辑是alert显示消息)

javascript 复制代码
function handleClick() {
        alert('点我了!')
    }

步骤三:添加onClick={handleClick}button标签上
<button onClick={handleClick}>

全部代码:

javascript 复制代码
export default function Button() {
    function handleClick() {
        alert('点我了!');
    }

    return (
        <button onClick={handleClick}>
            点击弹出提示
        </button>
    );
}

这样我们就完成了

a.事件处理函数的特点:通常在组件内部 定义; 名称以handle开头,后跟事件的名称

b.事件处理函数简短的话我们也可以使用内联 的方式:
<button onClick={function handleClick() { alert('你点击了我!'); }}> or <button onClick={() => { alert('你点击了我!'); }}>

c.>

1.2事件处理函数读取props

事件处理函数可以直接访问组件的props

javascript 复制代码
function AButton({ message, children }) {
    return (
        <button onClick={() => alert(message)}>
            {children}
        </button>
    )

}

export default function ToolBar() {
    return (
        <>
            <AButton message="我被点击了!">点击我</AButton>
            <AButton message="你被点击了!">点击你</AButton>
        </>
    )
}

这段代码描述的是:

定义一个Button组件,接收messagechildren并处理。导出组件ToolBar,这个组件里面使用了Button组件,Button组件传入了message的值。

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

在父组件中定义子组件的事件处理函数

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

javascript 复制代码
//父组件
function AButton({ message, children }) {
    function handleClick() {
    	//使用的是模板字符串``,可以在字符串中使用${}插入变量或者表达式
        alert(`目前弹出的消息是${message}`)
    }
    return (
        <button onClick={handleClick}>
            {children}
        </button>
    )

}
//子组件
export default function ToolBar() {
    return (
        <>
            <AButton message="我被点击了!">点击我</AButton>
            <AButton message="你被点击了!">点击你</AButton>
        </>
    )
}

1.3命名事件处理函数

内置组件比如button、div这种只支持浏览器事件名称如onClick

当构建自己的组件时可以自命名事件处理函数的prop惯例 :事件处理函数的名称需要:on开头后跟一个大写字母。

可能有人疑问这里这个prop是啥,诺,就是这个:
Button是自定义组件,自命名栗子的例如这个:

1.4 事件传播(冒泡)

事件处理函数还将捕获任何来自子组件的事件。

cpp 复制代码
function AButton({ message, children }) {
    function handleClick() {
        // 模板字符串
        alert(`目前弹出的消息是${message}`)
    }

    return (
        <div onClick={() => alert('我是button的父元素')}>
            <button onClick={handleClick}>
                {children}
            </button>
        </div>
    )

}

export default function ToolBar() {
    return (
        <>
            <AButton message="我被点击了!">点击我</AButton>
            <br />
            <AButton message="你被点击了!">点击你</AButton>
        </>
    )
}

上述代码是一个事件冒泡的示例,当我点击两个按钮中的任何一个,他会弹出两个提示框,首先是'xx被点击了',一个是'我是button的父元素'

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

1.5 阻止事件传播(冒泡) e.stopPropagation()

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

如果你想阻止一个事件到达父组件,调用 e.stopPropagation()

javascript 复制代码
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>
        </div>
    );
}

当你点击按钮时:

a.React 调用了传递给<button>的 onClick 处理函数。

b.调用 e.stopPropagation(),阻止事件进一步冒泡。调用 onClick 函数

c.在 Toolbar 组件中定义的函数,显示 alert

d.由于传播被阻止,父级 <div> 的 onClick 处理函数不会执行

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

如果你依赖于事件传播,而且很难追踪哪些处理程序在执行,及其执行的原因

javascript 复制代码
<button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>

可以考虑像上面代码一样,添加一些代码让组件做出额外的行为,这样有利于追踪某个事件触发时的一整个过程(链)。

1.7阻止默认行为e.preventDefault()

比如form表单内部点击按钮会默认重新加载页面

javascript 复制代码
export default function Signup() {
    return (
        <form onSubmit={() => alert('提交表单')}>
            <input type="text" />
            <button>提交</button>
        </form>
    )
}

我们可以使用事件对象中的 e.preventDefault() 来阻止这种情况发生:

javascript 复制代码
export default function Signup() {

    return (
        <form onSubmit={(e) => {
            alert('提交表单')
            e.preventDefault();
        }}>
            <input type="text" />
            <button>提交</button>
        </form>
    )
}

1.8 作业

a. 修复事件处理函数

点击此按钮理论上应该在黑白主题之间切换页面背景

javascript 复制代码
export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick()}>
      切换背景
    </button>
  );
}

b. 修改问题使得点击按钮 只 改变颜色,并且 不 增加计数器。

javascript 复制代码
export default function ColorSwitch({
  onChangeColor
}) {
  return (
    <button>
      改变颜色
    </button>
  );
}

答案:a.去掉handleClick的小括号 b.阻止冒泡

1.9 小结

  • 渲染函数须是纯函数,而事件处理函数不需要。
  • 可以将函数作为 prop 传递给元素如 <button> 来处理事件。
  • 必须传递事件处理函数,而非函数调用! onClick={handleClick} ,不是 onClick={handleClick()}
  • 事件处理函数在组件内部定义,所以它们可以访问 props。
  • 可以在父组件中定义一个事件处理函数,并将其作为 prop 传递给子组件。
  • 事件会向上传播。通过事件的第一个参数调用 e.stopPropagation() 来防止这种情况。
  • 事件可能具有不需要的浏览器默认行为。调用 e.preventDefault() 来阻止这种情况。
  • 从子组件显式调用事件处理函数 prop 是事件传播的另一种优秀替代方案。

2. State

组件通常需要根据交互更改屏幕上显示的内容。表单的输入,插入的图片等,组件需要记录某些数据:当前输入值,当前图片等,我们需要State来记忆这些数据。

要使用新数据更新组件,需要满足:
a.保留 渲染之前的数据
b.触发 React使用新数据渲染组件(即重新渲染)
useState Hook提供了这两个功能:State变量State setter函数

2.1 添加state变量

步骤1:导入useState
import { useState } from 'react';

步骤2:声明变量,数组解构
const [index, setIndex] = useState(0);

步骤3:书写逻辑
setIndex(index + 1);

一个简单小栗子全部代码,帮助你更好的理解。

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

export default function Sum() {
    const [index, setIndex] = useState(0);
    function handleAdd() {
        setIndex(index + 1);
    }
    return (
        <div>
            <button onClick={handleAdd}>
                点击+1
            </button>
            <span>{index}</span>
        </div>
    )
}

2.2 Hook

题外话:读起这个名词就让我想到绿巨人浩克

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

特殊的函数,只在 React 渲染时有效。

注意:不能在条件语句、循环语句或其他嵌套函数内调用 Hook。只能在组件或自定义 Hook 的最顶层调用。

2.3 useState详解

当你调用useState时,就告诉了React你想让组件'记住'某些数据
const [index, setIndex] = useState(0)是让React记住index

a. 惯例命名为 const [thing, setThing]

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

b. 每次你的组件渲染时,useState 都会给你一个包含两个值的数组:
state 变量 (index) 会保存上次渲染的值。
state setter 函数 (setIndex) 可以更新 state 变量并触发 React 重新渲染组件。

2.4 使用多个state变量

很简单,看代码就懂了

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

export default function Sum() {
    const [index, setIndex] = useState(0);
    const [num, setNum] = useState(2);
    function handleAdd() {
        setIndex(index + 2);
    }
    function handleMul() {
        setNum(num * 2)
    }

    return (
        <>
            <div>
                <button onClick={handleAdd}>
                    点击+2
                </button>
                <span>{index}</span>
            </div>
            <div>
                <button onClick={handleMul}>
                    点击*2
                </button>
                <span>{num}</span>
            </div>
        </>
    )
}

如果你发现经常同时更改两个 state 变量,那么最好将它们合并为一个。

后续会学习到如何优化。

State 是组件实例内部的状态。渲染同一个组件两次,两个state不会相互影响,相互独立。

2.5 小结

  • 当一个组件需要在多次渲染间"记住"某些信息时使用 state 变量。
  • State 变量是通过调用 useState Hook 来声明的。
  • Hook是以 use 开头的特殊函数,需要在非条件语句中调用。
  • 调用 Hook 时,包括 useState,仅在组件或另一个Hook 的顶层被调用才有效。
  • useState Hook 返回:当前 state 和更新它的函数。
  • 你可以拥有多个 state 变量,按顺序匹配它们。
  • State 是组件私有的。如果你在两个地方渲染它,则每个副本都有独属于自己的 state

3. 渲染和提交

理解这些处理步骤有利于引发对代码的思考和解释代码的逻辑

分为三步:
步骤一:触发渲染(菜单分配给厨房)
步骤二:渲染组件(做菜)
步骤三:提交到DOM(上菜)

3.1 步骤一:触发一次渲染

导致组件渲染的原因

  • a.组件初次渲染
    通过调用DOM节点的creatRoot,然后组件调用render函数完成。
  • b.组件状态发生改变。
    初次渲染后,可调用set函数更新状态,触发渲染。

3.2 步骤二:React渲染组件

"渲染中" 即 React 在调用你的组件

  • 初次渲染,调用根组件
  • 后续渲染,调用内部状态更新触发了渲染的函数组件

注意:渲染必须始终是一次 纯计算!给定的输入结果返回始终唯一,不更改任何存在于渲染之前的对象或变量。

3.3 步骤三:React把更改提交到DOM

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

  • 初次渲染,使用appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
  • 重渲染,React 将应用最少的必要操作(在渲染时计算),以使得 DOM 与最新的渲染输出相互匹配。

React 仅在渲染之间存在差异时才会更改 DOM 节点。

3.4小结

  • React屏幕更新会发生三个步骤:触发、渲染、提交
  • 可以使用严格模式去寻找组件中的错误
  • 如果渲染结果与上次一样,那么 React 将不会修改 DOM
  • 在渲染完成并且React更新 DOM 之后,浏览器就会重新绘制屏幕

4. state如同一张快照

4.1设置state会触发渲染

当你设置组件的状态时,React检测到状态变化,调用 render 方法来重新渲染组件,从而确保UI能够反映最新的状态。

当你按下 "send" 时,setIsSent(true) 会通知 React 重新渲染 UI

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

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  const [message, setMessage] = useState('Hi!');
  if (isSent) {
    return <h1>Your message is on its way!</h1>
  }
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      setIsSent(true);
      sendMessage(message);
    }}>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

function sendMessage(message) {
  // ...
}

4.2 渲染会及时生成一张快照

React 重新渲染一个组件时:

  • React 会再次调用你的函数
  • 函数会返回新的JSX 快照
  • React 会更新界面以匹配返回的快照
javascript 复制代码
<button onClick={() => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}}>+3</button>

这段代码,点击+3按钮数字会变成1,2,3这样递增
尽管你调用了三次 setNumber(number + 1),但在 这次渲染的 事件处理函数中 number 会一直是 0,所以你会三次将state设置成 1。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number 等于 1 而不是 3 。

4.3 随时间变化的state

  • 这段代码弹出的数字是 0
javascript 复制代码
 <button onClick={() => {
        setNumber(number + 5);
        alert(number);
      }}>+5</button>
  • 这段代码也弹出的是 0
javascript 复制代码
 <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>

他们俩渲染的页面不同。

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

试一试:如下代码,点击send,快速切换AliceBob,你猜会给谁说hello

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

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Hello');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`You said ${message} to ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

还是Alice

因为:React 会使state 的值始终"固定"在一次渲染的各个事件处理函数内部

想要读取最新的state可以使用状态更新函数,之后会学习到。

4.4 小结

  • Reactstate 存储在组件之,就像在架子上一样。
  • 当你调用 useState 时,React 会为你提供该次渲染 的一张state快照。
  • 每个渲染都有自己的事件处理函数。
  • 每个渲染(以及其中的函数)始终"看到"的是 React 提供给这个 渲染的 state 快照。
  • 过去创建的事件处理函数拥有的是创建它们的那次渲染中的 state 值。

4.5 作业

向 click 事件处理函数添加一个 alert 。当灯为绿色且显示"Walk"时,单击按钮应显示"Stop is next"。当灯为红色并显示"Stop"时,单击按钮应显示"Walk is next"。

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

export default function TrafficLight() {
  const [walk, setWalk] = useState(true);

  function handleClick({children}) {
    setWalk(!walk);
    if(walk){
     alert('Stop is next')
    }else{
      alert('walk is next')
    }
  }

  return (
    <>
      <button onClick={handleClick}>
        Change to {walk ? 'Stop' : 'Walk'}
      </button>
      <h1 style={{
        color: walk ? 'darkgreen' : 'darkred'
      }}>
        {walk ? 'Walk' : 'Stop'}
      </h1>
    </>
  );
}

答案:略


5. 把一系列的state更新加入队列

下次渲染之前对state进行操作,如何批量更新state

5.1 React对state更新进行批处理

点击按钮1次,值会变成1而不是3

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

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

这让你可以更新多个 state 变量。这也意味着只有在你的事件处理函数及其中任何代码执行完成 之后,UI才会更新。这种特性也就是 批处理

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

这是一个不常见的用例。

可以像setNumber(n=> n+ 1) 这样传入依赖前一个 state 来计算下一个 state的函数,而不是像 setNumber(number + 1) 这样传入 下一个 state 值。这样就告诉 React state 值做某事"而不是单纯替换。

javascript 复制代码
 <button onClick={() => {
        setNumber(number => n + 1);
        setNumber(number =>  n + 1);
        setNumber(number => n + 1);
  }}>+3</button>

在这里,n => n + 1 被称为 更新函数

当你将它传递给一个 state 设置函数时:

React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。

在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。

5.3 替换 state 后更新 state

javascript 复制代码
 <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>增加数字</button>

得到的是 6
实现步骤 :a. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 将 "替换为 5" 添加到其队列中。

b. setNumber(n => n + 1)n => n + 1 是一个更新函数。React 将该函数添加到其队列中。

c.下一次渲染期间,react遍历state队列:

react保存 6 为最终结果并从useState中返回。

再看一个例子:

javascript 复制代码
 <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
        setNumber(42);
      }}>增加数字</button>

得到的是42
实现步骤 :

a. setNumber(number + 5)number 0,所以 setNumber(0 + 5)。React 将 "替换为 5" 添加到其队列中。

b. setNumber(n => n + 1)n => n + 1 是更新函数。React 将该函数添加到其队列。

c. setNumber(42)React 将 "替换为 42" 添加到其队列中。

在下一次渲染期间,React 会遍历 state 队列:

5.4 关于更新函数

更新函数必须是纯函数,而且只返回结果。

命名通常可以通过相应state 变量的第一个字母 来命名更新函数的参数:比如state变量是number那么更新函数参数命名为n

5.5小结

  • 设置state不改变现有渲染中变量,但会请求一次新渲染。
  • React会在事件处理函数执行完成之后处理state更新。这被称为批处理。
  • 要在一个事件中多次更新某些state,你可以使用setNumber(n => n + 1) 更新函数

6.更新state中的对象

state中可以保存任意类型的 JS 值,包括对象。

不要直接修改state中的对象,需要新创建一个(可copy一个),将state更新为此对象。

6.1 mutation

目前再state存放过数字、字符串、布尔类型的值,这些再js中是不可变的,可以通过替换它们得值来触发新的渲染。
在 JS 中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。

现在我们来存放一个对象:
const [position, setPosition] = useState({ x: 0, y: 0 });

修改一下position中x的值:position.x = 5。改变对象自身的内容,你这样做时相当于制造了一个mutation

虽然严格来说 React state 中存放的对象是可变的,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此你应该替换它们的值,而不是对它们进行修改。

6.2 将state视为只读

把所有存放在state中的JS对象都视为只读的。

栗子:

我们用一个存放在 state 中的对象来表示指针当前的位置。当你在预览区触摸或移动光标时,红色的点本应移动。但是实际上红点仍停留在原处

javascript 复制代码
import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        position.x = e.clientX;
        position.y = e.clientY;
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

问题出在这⬇:这段代码直接修改了 上一次渲染中 分配给 position 的对象。但是因为并没有使用 state 的设置函数,React 并不知道对象已更改。所以React没有做出任何响应。这就像在吃完饭之后才尝试去改变要点的菜一样。虽然在一些情况下,直接修改 state 可能是有效的,但我们并不推荐这么做。你应该把在渲染过程中可以访问到的 state 视为只读的。

javascript 复制代码
//直接修改上一次渲染的对象 
onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

正确做法是:创建一个新对象,传递给state,触发渲染。

通过setPosition你告诉react:使用这个新对象替换position的值,再次渲染。

javascript 复制代码
onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

全部代码:此时就可以实现要求的效果了

javascript 复制代码
import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

6.3 展开语法复制对象

如下是一个表单,修改输入框数据不能成功,因为是直接修改了state

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

export default function FullName() {
    const [person, setPerson] = useState({
        firstname: 'W',
        lastname: 'yr',
        email: '123@qq.com'
    })
    function handleFirstNameChange(e) {
        person.firstname = e.target.value   //直接修改的state
    }
    function handleLastNameChange(e) {
        person.lastname = e.target.value
    }
    function handleEmailChange(e) {
        person.email = e.target.value
    }

    return (
        <form>
            First name <input type="text" value={person.firstname} placeholder='请输入姓' onChange={handleFirstNameChange} /><br />
            Last name <input type="text" value={person.lastname} placeholder='请输入名' onChange={handleLastNameChange} /><br />
            Email <input type="text" value={person.email} placeholder='请输入电子邮件' onChange={handleEmailChange} />
            <p>{person.firstname}{''}{person.lastname}{'的电子邮件是:'}{person.email}</p>
        </form>
    )
}

想实现我们的需求,可以创建一个新的对象并传递给setPerson,这个栗子中我们还需要把其它的属性复制到新对象中,因为我们每次只改变一个字段,比如输入姓的时候其他两个字段是不变的。

javascript 复制代码
setPerson({
            firstname: e.target.value,
            lastname: person.lastname,
            email: person.email

        })

这样写要是属性忒多真的是很麻烦呢

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

注意:加,号,注意书写代码的顺序

javascript 复制代码
...person,   //复制person的所有字段
firstName: e.target.value  //firstName

代码修改后就可以正常运行了

扩展:上述代码是使用三个事件处理函数分别更新字段,也可以一个事件处理函数解决,看下面的代码:

javascript 复制代码
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  //一个事件处理函数就可以更新多个字段了
    });
  }

6.4更新一个嵌套对象

嵌套对象:对象套对象,比如⬇

javascript 复制代码
const [person, setPerson] = useState({
        name: 'ruru',
        age: 18,
        family: {
            boy: 'fufu',
            animal:'shushu'
        }
  })

比如你想更新一下familyanimal的值

使用mutation的方法来实现很容易理解BUT在react将state视为不可变,所有下面代码方法不可取!!!:
person.family.animal = 'mimi'

想要修改animai,创建一个新family对象将family数据复制进去,再创建一个新person对象,将person的数据复制进去。让family中的属性指向新的family对象。上代码:

javascript 复制代码
const newFamily = { ...person.family, animal: 'mimi' }  //创建新对象,赋值,改值
const newPerson = { ...person, family: newFamily }  //创建新对象,复制,改family指向
setPerson(newPerson)

or

写成一个函数调用:

javascript 复制代码
 setPerson({
        ...person,
        family: {
            ...person.family,
            animal: 'mimi'
        }
    })

感觉这个方式我更易用

虽然繁杂,但是原理简单,可以有效解决问题。

思考一下,对象是嵌套的吗?当然代码看起来是嵌套的,其实对象是相互独立的,只是存在 指向 问题

6.5 Immer简洁逻辑

如果state有多层的嵌套,可以考虑使其扁平化,不改变state的数据解构,使用更加便捷的方式实现嵌套展开。
Immer:库,可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。代码看起来就像"打破了规则"而直接修改了对象:

不同于一般的mutation,它不会覆盖原来的state

javascript 复制代码
updatePerson((draft) => {
    draft.family.animal = 'mimi'
})

Immer运行的原理

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


7. 更新state中的数组

可以存储在state中的一种JS对象。

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

7.1 使用数组方法更新数组

React state 中的数组视为只读。这意味着有些数组方法可用,有些不可用

使用Immer库的话,表格中的方法都可以使用了。

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

下面我们展开这些推荐使用的数组方法来更好的了解如何使用它们

7.1.1 像数组中添加元素concat/[...arr]

这个栗子用的是...arr,使用map遍历数组

如果你想改变数组中的某些或全部元素,你可以用 map() 创建一个新数组。

你传入 map 的函数决定了要根据每个元素的值或索引(或二者都要)对元素做何处理。

javascript 复制代码
import { useState } from 'react'
let nextId = 0;

export default function ToDoList() {
    const [lists, setLists] = useState([]);  //清单列表
    const [todo, setTodo] = useState('');  //输入框输入

    return (
        <div>
            <h1>ToDo清单</h1>
            <input type='text' value={todo} onChange={(e) => {
                setTodo(e.target.value)
            }} />
            <button onClick={() => {
                setLists([
                    ...lists,
                    { id: nextId++, todo: todo }
                ]);
            }}>添加</button>
            <ul>
                {lists.map(list => (  //遍历数据展示
                    <li key={list.id}> {list.todo} </li>
                ))}
            </ul>
        </div>
    )
}
7.1.2从数组中删除元素filter/slice

这个栗子用的是filter,基本用法,不多解释,多敲多练

cpp 复制代码
import { useState } from 'react'
let todolists = [
    { id: 0, name: '吃饭' },
    { id: 1, name: '睡觉' },
    { id: 2, name: '打豆豆' }
];
export default function ToDoList() {
    const [list, setList] = useState(todolists);  //初始值就是数组
    return (
        <div>
            <h1>TODO清单</h1>
            <ul>
                {list.map(l => (
                    <li key={l.id}> {l.name}{''}
                        <button onClick={() => {
                            setList(
                                list.filter(a => a.id !== l.id)
                            )
                        }}>
                            删除</button>
                    </li>
                ))}
            </ul>
        </div>
    )
}

注意!我在书写代码的时候,将 list.filter(a => a.id !== l.id)用{}括号包裹起来了,这是错误的!

因为我习惯性的像if语句一样即使是只有一条语句也用{}包起来。真是个坏习惯。包起来效果会有出入,大家可以试试。

7.1.3 替换(修改)数组中的元素

做法:map创建一个数组,map回调函数的第二个参数是元素的索引。

使用索引来判断是返回原来的元素还是替换成其他的值。

javascript 复制代码
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>
  );
}
7.1.4 数组中插入元素

任意位置插入元素,你可以将数组展开运算符...slice() 方法一起使用

下面的例子中,插入按钮总是会将元素插入到数组中索引为 1 的位置。
通过切片,添加新元素,再切片创建一个新的数组

javascript 复制代码
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>
    </>
  );
}
7.1.5 其他改变数组的情况

你可能想翻转数组,或是对数组排序。而 JS 中的 reverse() 和 sort() 方法会改变原数组,所以你无法直接使用 它们。

先复制原数组创建一个新数组,再对新数组使用reverse() sort()方法,因为数组的拷贝是浅拷贝,这种方法也不可取。
可以用类似于 更新嵌套的JS对象 的方式解决这个问题------拷贝想要修改的特定元素,而不是直接修改它。下面是具体的操作。

7.2 更新数组内部的对象

对象并不是 真的 位于数组"内部"。可能他们在代码中看起来像是在数组"内部"

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

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

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

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

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

  return (
    <>
      <h1>艺术愿望清单</h1>
      <h2>我想看的艺术清单:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>你想看的艺术清单:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

效果:

7.3 Immer简洁逻辑

如果state层级很深,可以调整一下数据结构,使数据变得扁平

如果你不想改变 state 的数据结构,可以使用 Immer ,它让你可以继续使用方便的,但会直接修改原值的语法,并负责为你生成拷贝值。

下面是我们用Immer来重写的艺术愿望清单的例子:

JSON数据:

json 复制代码
{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

JS代码:

javascript 复制代码
import { useState } from 'react';
import { useImmer } from 'use-immer';

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

export default function BucketList() {
  const [myList, updateMyList] = useImmer(
    initialList
  );
  const [yourList, updateYourList] = useImmer(
    initialList
  );

  function handleToggleMyList(id, nextSeen) {
    updateMyList(draft => {
      const artwork = draft.find(a =>
        a.id === id
      );
      artwork.seen = nextSeen;
    });
  }

  function handleToggleYourList(artworkId, nextSeen) {
    updateYourList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>艺术愿望清单</h1>
      <h2>我想看的艺术清单:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>你想看的艺术清单:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

7.4 小结

  • 以把数组放入state中不应该直接修改
  • 不要直接修改数组,复制数据创建新数组,然后使用新的数组来更新它的状态
  • 可以使用[...arr, newItem]这种数组展开语法,向数组中添加元素。
  • 可以使用filter()map()来创建数组过滤或变换。
  • 可以使用Immer来保持代码简洁。

总结

多敲多练就可以记住。

下一章:状态管理

码字好累啊,给个三连,然后一起加油吧 ^皿^

相关推荐
多多*几秒前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
2301_801074151 分钟前
TypeScript异常处理
前端·javascript·typescript
小阿飞_2 分钟前
报错合计-1
前端
caperxi4 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男4 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189115 分钟前
前端css样式覆盖
前端·css
学习路上的小刘6 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&6 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
小白小白从不日白18 分钟前
react 组件通讯
前端·react.js
Redstone Monstrosity35 分钟前
字节二面
前端·面试