重学React(二):添加交互_react 中 const mybutton = object.assign(button, bas
背景:第一部分更多的是React的基本渲染规则,面向UI的规则居多,当UI页面处理完毕后,接下来就是对数据以及交互的处理,接下来就继续吧~
学习内容:
React官网教程:https://zh-hans.react.dev/learn/adding-interactivity
其他辅助资料(看到再补充)
补充说明:这次学习更多的是以学习笔记的形式记录,看到哪记到哪
响应事件
React 可以在 JSX 中添加 事件处理函数。其中事件处理函数为自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发。
export default function Button() {// 声明了一个函数,在这个函数里做的就是弹出一个弹窗,显示你点击了我 function handleClick() { alert(\'你点击了我!\'); }// 在button这个组件里,handleClick作为一个prop被传入// button里触发onClick(点击)事件,就会调用这个函数,执行函数里面的操作 return ( <button onClick={handleClick}> 点我 </button> );}// 当然,函数也可以直接以内联的方式存在 <button onClick={ function handleClick() { alert(\'你点击了我!\');}}> 点我 </button>// 或者更加简单的箭头函数 <button onClick={()=> alert(\'你点击了我!\')}> 点我 </button>
按照惯例,通常将事件处理程序命名为 handle,后接事件名。所以会经常看到 onClick={handleClick},onMouseEnter={handleMouseEnter} 等,这是约定俗成的命名方式,不是必须的
传递给事件处理函数的函数应直接传递,而非调用
这两者的区别在于,右边在传递函数时多了一个(),就使得这个函数变成了一个立即执行函数,渲染时就会触发,而不是点击才执行
由于事件处理函数声明于组件内部,可以直接访问组件的 props。
甚至我们可以把事件处理函数作为prop传到组件中去,
按照惯例,事件处理函数 props 应该以 on 开头,后跟一个大写字母,我们也可以自定义,但内置的组件(比如button,div这种html元素)仅支持浏览器事件名称
// 可以直接读到message的值,不用再额外声明什么,这个例子里会根据传入的message不同展示不一样的结果function AlertButton({ message, children }) { return ( <button onClick={() => alert(message)}> {children} </button> );}export default function Toolbar() { return ( <div> <AlertButton message=\"正在播放!\"> 播放电影 </AlertButton> <AlertButton message=\"正在上传!\"> 上传图片 </AlertButton> </div> );}// 甚至我们可以把事件处理函数作为prop传到组件中去,// 这个例子里PlayButton和UploadButton点击时就会触发不一样的函数,展示不一样的结果// 因为它们共用了一个Button组件,所以两个按钮的样式是一样的,遇到需要样式一样的场景可以使用这种模式function Button({ onClick, children }) { return ( // 这里button小写,说明是html内置元素,必须用onClick,但是大括号内的props没有这个规定,可以叫onA,onB都可以,只要统一就行 <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> );}
事件传播
事件处理函数还将捕获任何来自子组件的事件。事件会沿着树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播
在 React 中所有事件都会传播,除了 onScroll,它仅适用于你附加到的 JSX 标签
事件处理函数接收一个 事件对象 作为唯一的参数。它通常被称为 e ,代表 “event”(事件),可以使用此对象来读取有关事件的信息。
// 在这个例子中,点击了播放电影这个按钮,不单触发了按钮的点击事件,还会触发div的点击事件,也就是事件传播到了div这里// 想象一下,button也是div的一部分,你点了button,也就相当于点了div,// 但如果只点击div,就不会触发button的点击export default function Toolbar() { return ( <div className=\"Toolbar\" onClick={() => { alert(\'你点击了 toolbar !\'); }}> <button onClick={() => alert(\'正在播放!\')}> 播放电影 </button> <button onClick={() => alert(\'正在上传!\')}> 上传图片 </button> </div> );}// 如果想要阻止这个冒泡,就需要event事件的帮忙// 此时可以在触发的事件中添加 e.stopPropagation()export default function Toolbar() { return ( <div className=\"Toolbar\" onClick={() => { alert(\'你点击了 toolbar !\'); }}> <button onClick={(e) => { // 添加这一句等于告诉浏览器我只需要触发这一个函数,别帮我往上传话 e.stopPropagation() alert(\'正在播放!\') }}> 播放电影 </button> <button onClick={() => alert(\'正在上传!\')}> 上传图片 </button> </div> );}// 如果想在子组件里执行一些操作,同时又触发父组件的一些行为,可以在子组件的函数中添加来自父组件的props// 这是事件传播的另一种替代方案,在这段代码里又能执行子组件事件也能执行父组件事件function Button({ onClick, children }) { return ( <button onClick={e => { e.stopPropagation(); onClick(); }}> {children} </button> );}//极少数情况下,需要捕获子元素上的所有事件,即便它们阻止了传播。// 例如,对每次点击进行埋点记录,那可以通过在事件名称末尾添加 Capture 来实现这一点<div onClickCapture={() => { /* 这会首先执行 */ }}> <button onClick={e => e.stopPropagation()} /> <button onClick={e => e.stopPropagation()} /></div>// 每个事件分三个阶段传播:// 它向下传播,调用所有的 onClickCapture 处理函数。// 它执行被点击元素的 onClick 处理函数。// 它向上传播,调用所有的 onClick 处理函数。
阻止默认行为
某些浏览器事件具有与事件相关联的默认行为。例如,点击 表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面
可以调用事件对象中的 e.preventDefault() 来阻止这种情况发生
export default function Signup() { return ( <form onSubmit={e => { // 不加这句的话,触发完表单事件,会重新加载页面 // 加上后只出现弹窗,没有任何后续的行为 e.preventDefault(); alert(\'提交表单!\'); }}>> <input /> <button>发送</button> </form> );}
再次强调一下
- e.stopPropagation() 阻止触发绑定在外层标签上的事件处理函数。
- e.preventDefault() 阻止少数事件的默认浏览器行为。
State: 组件的记忆
组件通常需要根据交互更改屏幕上显示的内容,通常需要“记住”当前的一些东西,比如当前展示什么图片,当前翻页在哪一页,在 React 中,这种组件特有的记忆被称为 state
// 在理想情况下,每次进行点击,index都会加1,页面上的数字也会随着增大// 但现实却是,页面永远展示1// 原因如下:// index是作为局部变量存在的// 局部变量无法在多次渲染中持久保存。 当 React 再次渲染这个组件时,它会从头开始渲染——不会考虑之前对局部变量的任何更改。// 更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。export default function Gallery() { let index = 0; function handleClick() { index = index + 1; } return ( <> <button onClick={handleClick}> Next </button> <h3> {index + 1} </h3> </> );}
要修改这个问题,需要保存渲染间的数据,还需要触发 React 使用新数据渲染组件(重新渲染的时候展示新数据),这就引出最基础我们最常用的一个hook——useState
在 React 中,useState 以及任何其他以“use”开头的函数都被称为 Hook
Hook 是特殊的函数,只在 React 渲染时有效。它们能让你 “hook” 到不同的 React 特性中去
Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。
Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块
(详细的解析在后面,这里只需要有那么一个概念就好)
useState Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
// 添加 state 变量,先从文件顶部的 React 中导入 useStateimport { useState } from \'react\';export default function Gallery() {// index 是一个 state 变量,setIndex 是对应的 setter 函数// useState 的唯一参数是 state 变量的初始值// 这里代表的是,index的初始化为0,每次修改index都需要调用setIndex才触发// [ 和 ] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项// 惯例是将这对返回值命名为 const [thing, setThing],这样容易理解,但也支持自定义 const [index, setIndex] = useState(0); function handleClick() { setIndex(index + 1); } return ( <> <button onClick={handleClick}> Next </button> <h3> {index + 1} </h3> </> );}// 多次点击时发生的state变化如下:// 组件进行第一次渲染。 因为0作为 index 的初始值传递给 useState,它将返回 [0, setIndex]。 React 记住 0 是最新的 state 值。// 当用户点击按钮时,调用 setIndex(index + 1)。 index 是 0,所以它是 setIndex(1)。这告诉 React 现在记住 index 是 1 并触发下一次渲染。// 组件进行第二次渲染。React 仍然看到 useState(0),但是因为 React 记住 了你将 index 设置为了 1,它将返回 [1, setIndex]。这个时候1就是最新的state值
我们可以在一个组件中拥有任意多种类型的 state 变量,但state变量越多,意味着越难管理,因此可以在一定程度上进行变量合并,比如可能同时会有多个变量一起改变,可以改用一个state统一管理。
为了使语法更简洁,在同一组件的每次渲染中,Hooks 都依托于一个稳定的调用顺序。这在实践中很有效,因为如果你遵循只在顶层调用 Hooks的原则,Hooks 将始终以相同的顺序被调用
在 React 内部,为每个组件保存了一个数组,其中每一项都是一个 state 对。它维护当前 state 对的索引值,在渲染之前将其设置为 “0”。每次调用 useState 时,React 都会为你提供一个 state 对并增加索引值。
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
State 不依赖于特定的函数调用或在代码中的位置,它的作用域“只限于”屏幕上的某块特定区域
state 完全私有于声明它的组件。父组件无法更改它。这使你可以向任何组件添加或删除 state,而不会影响其他组件。
import Gallery from \'./Gallery.js\';// 比如之前的Gallery组件,同时渲染两个的话,点击第一个的按钮,不会改变第二个钻的index// 这个Page组件,完全不会知道Gallery组件有什么state,也没办法去干预它// 如果想实现两个Gallery的index同步改变,可以把state声明放到Page组件中,以props的形式保存export default function Page() { return ( <div className=\"Page\"> <Gallery /> <Gallery /> </div> );}
渲染和提交
我们之前一直说的都是代码层面的事情,从React代码到我们能看到的屏幕内容,中间必须被React渲染,渲染这个词已经不是新鲜玩意儿,本质上还是html,css和js转化为可交互的网页。详细了解浏览器渲染原理可以看看这个:浏览器渲染原理,在此就不过多解释。
React官网将React的渲染过程比喻为去餐厅吃饭,可以把用户想象成点餐的客人,React就是服务员,做出来的菜品就是最终渲染出来的页面。首先客人点单(用户操作页面),React服务员将菜单交给后厨(触发交互),后厨准备餐品(React渲染),服务员再将餐品放到客人桌上(渲染结果提交到DOM)
在这过程中如果客人想加菜(对应state状态更新),React服务员还是会重复菜单交后厨(二次触发),后厨准备餐品(二次渲染),出餐的过程
最后的最后,这些渲染的结果,在更新DOM之后,都会触发浏览器的重新绘制,也就是一开始说的浏览器渲染
基本的渲染过程都了解了,接下来我们从专业术语的角度来重新看这个过程。
渲染原因
首先是触发渲染的原因,有两种方式会触发渲染
- 组件的初次渲染。
当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它是通过调用 createRoot 方法并传入目标 DOM 节点,然后用你的组件调用 render 函数完成的。现在很多框架都把这个过程包掉了,比如在Nextjs的代码中,你并不需要手写createRoot这个函数。
import { createRoot } from \'react-dom/client\';const root = createRoot(document.getElementById(\'root\'))root.render(<div />);
- 组件(或者其祖先之一)的 状态发生了改变。
一旦组件被初次渲染,你就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列,之前说的useState这个hook触发的组件更新就是这种渲染。
React渲染过程
在进行初次渲染时, React 会调用根组件。后续渲染时, React 会调用内部状态更新触发了渲染的函数组件,React 将计算它们的哪些属性(如果有的话)自上次渲染以来已更改,在下一步(提交阶段)之前,它不会对这些信息执行任何操作,简单来说就是它会判断是否做了更改,在它看来没有改动的地方它就不会重新渲染,只会渲染它觉得变了的地方。这里也会有陷阱,有时候会导致有些想重新渲染的时候页面没有更新,特别是使用了memo函数的场景下。想详细了解原理的话可以去看看diff算法以及fiber的原理(我这篇文章里写了一些,可以先简单看看)。
渲染过程必须是纯计算
- 输入相同,输出相同。 给定相同的输入,组件应始终返回相同的 JSX。(当有人点了西红柿沙拉时,他们不应该收到洋葱沙拉!)
- 只做它自己的事情。 它不应更改任何存在于渲染之前的对象或变量。(一个订单不应更改其他任何人的订单。)
在 “严格模式” 下开发时,React 会调用每个组件的函数两次,这可以帮助发现由不纯函数引起的错误,但有时也会因此导致一些本地开发环境的问题,所以在开发时,特别是使用useEffect这个hook的时候,要注意不要乱用,避免出现不纯的函数
如果更新的组件在树中的位置非常高,渲染更新后的组件内部所有嵌套组件的默认行为将不会获得最佳性能。
React 把更改提交到 DOM 上
渲染完组件之后,React就开始修改DOM,同样也是两种修改,对于初次渲染,React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上,对于再次渲染,React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。如果渲染结果跟上一次一样,React是不会修改DOM的。
最后的最后,等React把当前的所有更改都提交完后,就会触发浏览器的重新绘制,也就是开头提到的浏览器渲染的过程。
state 如同一张快照
之前说过state会触发重新渲染,接下来会详细的说明它是如何触发重新渲染,以什么形式在什么节点实现更新。
state 变量看起来和一般的可读写的 JavaScript 变量类似。但 state 在其表现出的特性上更像是一张快照。设置它不会更改你已有的 state 变量,但会触发重新渲染,就是当前的值依旧保留,等重新渲染成功之后,再重新替换新的值。
从这张图可以看到,重新渲染时,React首先会执行某个函数,执行之后会返回一个新的jsx快照,React此时会更新界面以匹配新的快照
// 在这个例子中,当点击send按钮,触发了onSubmit函数的执行,// 可以看到setIsSent被执行,通知React,isSent的值被更新成true啦,jsx需要返回一个h1元素// 此时React根据新的返回值,替换dom,展示对应内容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); }}> <textarea placeholder=\"Message\" value={message} onChange={e => setMessage(e.target.value)} /> <button type=\"submit\">Send</button> </form> );}
接下来再看一个例子,思考一下点击+3会发生什么?
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> </> )}
执行之后会惊奇的发现,number会变成1(而不是设想中的3),再点一次,会变成2,也就是说,实际上每次点+3,本质上都是+1,这是为什么呢?
因为设置 state 只会为下一次渲染变更 state 的值,也就是说,在执行这个onClick
事件时,我们拿到的是当前这次渲染时的number值,也就是0,在每个setNumber(number + 1)
中,执行的都是setNumber(0 + 1)
,它的含义是告诉React,下一次渲染时我要将0+1啦
再来看一个例子加深下理解
import { useState } from \'react\';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setTimeout(() => { alert(number); }, 3000); }}>+5</button> </> )}
思考一下,这个时候页面number展示什么?alert会变成什么?
结果会出乎意料但又合乎常理:点击一次+5后,页面会变成5,但3秒后alert的结果是0.这又是为什么呢?
因为一个 state 变量的值永远不会在一次渲染的内部发生变化。也就是说,在onClick
函数内部,就算setTimeout是异步的,在这次执行中,所有的number都锁定了一个值,也就是当前的0,在这个函数执行完毕之前都是它。这就是类似快照的概念。
// 这是一个实际写代码中很容易被忽略的问题,不管函数怎么引用,它都是在同一个渲染周期内执行的// 所以这里两个alert都会是0(刚开始写react经常被这个问题坑)import { useState } from \'react\';export default function Counter() { const [number, setNumber] = useState(0); const somefunc=(num)=>alert(num); const func1=(num)=>{ somefunc(num)}; return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); func1(number) setTimeout(() => { alert(number); }, 3000); }}>+5</button> </> )}
把一系列 state 更新加入队列
React 会等到事件处理函数中的 所有 代码都运行完毕再处理state 更新。 这就是重新渲染只会发生在所有这些 setNumber() 调用之后 的原因
只有在事件处理函数及其中任何代码执行完成 之后,UI 才会更新。这种特性也就是 批处理
回到之前点菜那个例子,一般来说,服务员会在你最终确定好点什么菜之后再统一下单,不会点一个菜下一次单(加菜除外),不然就会出现:
“我想吃牛肉”
通知后厨煮牛肉
“算了,我还是吃鱼吧,牛肉不要了”
通知后厨不煮牛肉改成鱼
“想了一下,还是吃虾比较好”
通知后厨改煮虾
……
想象一下,这会给后厨带来多大的麻烦。在代码中也是一样的,所以state变量会在一定时间内进行统一的批处理,以避免给React带来不必要的麻烦,同时也是对性能的优化。
React 不会跨 多个 需要刻意触发的事件(如点击)进行批处理——每次点击都是单独处理的
// 如果想要实现调用三次setNumber实现+3,要怎么做呢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的使用export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> )}
看完这里,是不是觉得很神奇,这一下子就实现了+3的功能,这里可以看出来,setNumber中获取的不是当前这次快照中的number,而是一个内置的入参n
,这有什么区别呢?
这里内置参数表示的是一个根据队列中的前一个 state 计算下一个 state 的 函数,也就是说,setNumber(n => n + 1);
表示我拿上次的state的值+1,此时下一次state就变成了1,再次调用时,n就变成上一次的state结果1,+1就会变成2,以此类推,最后就能实现+3,而setNumber(number + 1);
相当于执行的是setNumber(n=>number + 1);
,此时number是作为一个常数存在的,n并没有使用,所以并不会返回更新后的值。
import { useState } from \'react\';export default function Counter() { const [number, setNumber] = useState(0);// 这个例子可以看出,第一个setNumber的结果是5,第二个setNumber的n就变成了5,所以最后渲染结果是5+1=6 return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}>增加数字</button> </> )}
事件处理函数执行完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新函数会在渲染期间执行,因此 更新函数必须是 纯函数 并且只 返回 结果。不要尝试从它们内部设置 state 或者执行其他副作用。
更新 state 中的对象
我们通过state来完成一些交互,state里面可以是任意类型的js值,之前我们的例子中都是用的number,string,boolean这种基础类型,这些值都是不可变(immutable)的,这意味着它们不能被改变或是只读的。可以通过替换它们的值以触发一次重新渲染。
const [x, setX] = useState(0);const [position, setPosition] = useState({ x: 0, y: 0 });// 此时state会从0变成5,0本身不可变,还会被保留在上一个快照中,只是5把当前的state从0替换成了5setX(5)// 从技术上来讲,当前的position是对象,更改对象的值是完全成立的,这时候就被称为制造了一个 mutation// 但从state的特性来说,我们每一次做的并不是修改当前值,而是保留快照,从新的值替换当前值,所以这种行为是不符合React约束的// 这样做的后果就是改变了当前的快照值,但是因为没有用到state的设置函数,React不知道数据已经改了// 并没有触发state的重新渲染机制,在交互上不会发生任何的改变position.x = 5;
mutation从我的理解来说,就是直接改变对象的值,直接进行增加,替换,删除操作等都属于mutation。
为什么在 React 中不推荐直接修改 state
有以下几个原因:
- 调试:如果你使用 console.log 并且不直接修改 state,你之前日志中的 state 的值就不会被新的 state 变化所影响。这样你就可以清楚地看到两次渲染之间 state 的值发生了什么变化
- 优化:React 常见的 优化策略 依赖于如果之前的 props 或者 state 的值和下一次相同就跳过渲染。如果你从未直接修改 state ,那么你就可以很快看到 state 是否发生了变化。如果 prevObj === obj,那么你就可以肯定这个对象内部并没有发生改变。
- 新功能:我们正在构建的 React 的新功能依赖于 state 被 像快照一样看待 的理念。如果你直接修改 state 的历史版本,可能会影响你使用这些新功能。
- 需求变更:有些应用功能在不出现任何修改的情况下会更容易实现,比如实现撤销/恢复、展示修改历史,或是允许用户把表单重置成某个之前的值。这是因为你可以把 state 之前的拷贝保存到内存中,并适时对其进行再次使用。如果一开始就用了直接修改 state 的方式,那么后面要实现这样的功能就会变得非常困难。
- 更简单的实现:React 并不依赖于 mutation ,所以你不需要对对象进行任何特殊操作。它不需要像很多“响应式”的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来,或者在初始化时做其他工作。这也是 React 允许你把任何对象存放在 state 中——不管对象有多大——而不会造成有任何额外的性能或正确性问题的原因。
在React的世界里,我们应该把所有存放在 state 中的 JavaScript 对象都视为只读的(数组也是一样,但数组更加复杂,后面再单独讲),在刚刚那个例子中,为了真正地 触发一次重新渲染,需要创建一个新对象并把它传递给 state 的设置函数。
const [position, setPosition] = useState({ x: 0, y: 0 });// 使用这个新的对象替换 position 的值,然后再次渲染这个组件setPosition({x:5,y:5})// 这种也属于mutation,是针对nextPosition这个对象// 但因为改变的是新的对象,而不是原来的position,所以是没有问题的// 还没有其他的代码引用它。改变它并不会意外地影响到依赖它的东西。这叫做“局部 mutation”const nextPosition = {};nextPosition.x = 5;nextPosition.y = 5;setPosition(nextPosition);
在某些场景下,可能我们只想修改对象中的其中一个值,这在填写表单的时候经常会用到,比如下面这个例子
import { useState } from \'react\';export default function Form() { const [person, setPerson] = useState({ firstName: \'Barbara\', lastName: \'Hepworth\', email: \'bhepworth@sculpture.com\' }); function handleFirstNameChange(e) { // 这样写肯定是不行的,理由之前说过了 // person.firstName = e.target.value; // 这样就完全没问题啦 setPerson({ // ... 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着想要更新一个嵌套属性时,需要多层展开 ...person, firstName: e.target.value }); } function handleLastNameChange(e) { // person.lastName = e.target.value; setPerson({ ...person, lastName: e.target.value }); } function handleEmailChange(e) { // person.email = e.target.value; setPerson({ ...person, email: e.target.value }); }// 仔细观察会发现,上面三个函数其实很相似,不一样的是需要修改的key// 其实还可以将函数抽象成这样: function handleChange(e) { setPerson({ ...person, [e.target.name]: 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> </> );}
如果对象有很多层嵌套的时候,要一直浅拷贝一直展开也是个很繁琐的事情,这个时候需要考虑将其扁平化,React官方推荐了一个库Immer
由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象
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} /> </> );}
更新 state 中的数组
跟对象类似的,想要更新state中的数组,需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。接下来就是详细的说明一下要如何进行数组的更新。
和对象一样,虽然数组是可变的,但在state的使用中,还是需要把它视为不可变进行操作,需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
在js中,由于数组的复杂性,诞生了很多快捷操作数组的方法,有些方法能直接改变原始数组,有些方法则不会。当state为数组的时候,我们需要尽量避免使用直接改变原始数组的方法。原因和之前更新state中的对象是一致的。
这里列举了一些会改变原始数组和不会改变原始数组的方法,更多的数组方法可以参考这篇文章,我们要做的是同样的功能,使用右边的方法处理。如果你还是想使用左边的方法,那就使用Immer
吧
// 这几个场景的使用技巧也比较简单,接下来就用代码说话吧 const [artists, setArtists] = useState([]); // 数组新增元素//push setArtists( // 替换 state [ // 是通过传入一个新数组实现的 ...artists, // 新数组包含原数组的所有元素 { id: nextId++, name: name } // 并在末尾添加了一个新的元素 ] // unshiftsetArtists([ { id: nextId++, name: name }, ...artists // 将原数组中的元素放在末尾]););// 删除元素setArtists( artists.filter(a => a.id !== artist.id));// 转化数组,想改变数组中的某些或全部元素// 替换元素,将数组中的某个元素替换成别的// 这个例子是将数组中的name换成label,同时将所有的id都加了100const handleTransNameToLabel=()=>{const newArtists = artists.map(({name,id,...rest})=>{return {label:name,id:id +100,...rest}})setArtists(newArtists)}// 向数组中插入元素 function handleClick() { const insertAt = 1; // 可能是任何索引 const nextArtists = [ // 插入点之前的元素: ...artists.slice(0, insertAt), // 新的元素: { id: nextId++, name: name }, // 插入点之后的元素: ...artists.slice(insertAt) ]; setArtists(nextArtists);// 复杂的操作,比如翻转和排序,直接使用sort和reverse会改变原始数组// 更好的解决方法是先复制数组,再进行操作const initialList = [ { id: 0, title: \'Big Bellies\' }, { id: 1, title: \'Lunar Landscape\' }, { id: 2, title: \'Terracotta Army\' },];const [list, setList] = useState(initialList); function handleReverse() { // 即使你拷贝了数组,你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。 const nextList = [...list]; // nextList[0].title = \'aaaaa\'; 这个行为是不正确的,因为这样还是改变了原来的list nextList.reverse(); setList(nextList); }// 来看一个更加复杂的例子// 运行这个例子会发现两个多选列表会被互相影响,问题就出现在下面的click事件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) { const myNextList = [...myList]; const artwork = myNextList.find( a => a.id === artworkId ); // 这里直接修改了已有的元素 artwork.seen = nextSeen; setMyList(myNextList); } function handleToggleYourList(artworkId, nextSeen) { const yourNextList = [...yourList]; const artwork = yourNextList.find( a => a.id === artworkId ); // 这里直接也修改了已有的元素 artwork.seen = nextSeen; setYourList(yourNextList); } 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> );}// 修改其实也很简单,可以使用 map 在没有 mutation 的前提下将一个旧的元素替换成更新的版本setMyList(myList.map(artwork => { if (artwork.id === artworkId) { // 创建包含变更的*新*对象 return { ...artwork, seen: nextSeen }; } else { // 没有变更 return artwork; }}));// 更加简单的方式就是使用Immer// 这里只写关键代码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; }); }
React 添加交互的过程就到这里啦,现在React的基础渲染,交互大家应该都有了一定的了解(感觉还是不太懂的可以再复习几遍),但这仅仅是React的基础模块,想要深入的使用React,状态管理和脱围机制十分重要。接下来就继续重学React吧~