React 事件响应及单组件状态处理
事件响应
事件处理函数
使用
React
可以在 JSX 中添加 事件处理函数。其中事件处理函数为自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发。React 官方文档 – 响应事件
大多元素均可绑定响应的事件,以按钮为例,最常使用的事件为点击事件。在 Ract
中为一个按钮添加点击事件非常简单,只需要在组件内创建一个事件处理函数,并在标签返回时,将其绑定到 onClick
属性上即可:
1 | function App() { |
事件处理函数通常有如下特点:
- 通常在 组件内部 定义。(这样做的好处是可以直接访问组件内部的
props
。) - 名称以
handle
开头,后跟事件名称。(这只是一种惯例,并非强制。)
一种更为简洁的方式为使用 Javascript
的箭头函数,为一个元素内联绑定事件处理函数,这种方式当处理函数比较小的情况下是非常方便的:
1 | function App() { |
通过 props
传递事件处理函数
通常,会在父组件中定义子组件的事件处理函数,这样做提高了子组件的可重用性。例如官网如下示例,根据需要,将不通的事件处理函数传递给按钮组件后,可以实现文件上传、电影点播等不同功能,但是子组件 Button
内部编码却不需要做任何改变。
1 | // 定义公共组件,其点击事件在使用时传入 |
事件传播行为
事件处理函数还会捕获任何来自子组件的事件,即事件从发生的地方开始,沿着 UI
树向上传播。例如:
1 | export default function Toolbar() { |
上述实例中,div
及其子组件 button
均添加了点击事件,虽然行为不同,但是当点击按钮出发其事件处理后,对应的点击事件会沿着UI树向上传播至 div
中,进而触发 div
的点击事件回调其处理函数。所以会看到两次弹窗。
但是,这种传播是可以被中断的。每个事件处理函数接收一个 事件对象 作为唯一的参数,可以使用此对象来读取有关事件的信息。同样的,若想阻止事件向上传播,可以在子组件事件处理函数的开始处,借助事件的 e.stopPropagation()
方法实现。(浏览器内置组件可以通过自定义组件来包装一下。)
1 | function Button({ onClick, children }) { |
阻止组件默认行为
某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form>
表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面,但是这种行为也是可以事件对象的 e.preventDefault()
方法来阻止的:
1 | export default function Signup() { |
State
事件处理函数是执行副作用的最佳位置。同渲染函数不同,事件处理函数不需要是 纯函数,因此它是用来更改某些值的绝佳位置。例如,更改输入框的值以响应键入,或者更改列表以响应按钮的触发。但是,在修改数据前,通常需要记住修改的值,这在 React
中便是通过 State
(状态)来实现的。
通常情况下,在组件内部使用普通变量保存状态数据后,在事件(例如按钮的点击事件)处理过程中改变其数据后,无法实现新内容的渲染,这是因为:
- 局部变量无法在多次渲染中持久保存。 当
React
再次渲染组件时,会从头开始渲染,不会考虑之前对局部变量的任何更改。 - 更改局部变量不会触发渲染。
React
没有意识到它需要使用新数据再次渲染组件。
在 React
中,以上功能可以通过 Hook
函数 useState
实现,该函数接收1个参数,为状态的初始值;返回值可以解构为两个值:其中第一个取值为 状态 ,第二个值为 setter
函数,用于改变状态的取值,进而触发 React
重新渲染组件。
当编写一个存有 state
的组件时,需要考虑使用多少个 state
变量以及它们都是怎样的数据格式。尽管选择次优的 state
结构下也可以编写正确的程序,但有几个原则可以帮助做出更好的决策:
- 合并关联的 state。如果总是同时更新两个或更多的
state
变量,可以考虑将它们合并为一个单独的state
变量。 - 避免互相矛盾的 state。当
state
结构中存在多个相互矛盾或“不一致”的state
时,就可能为此会留下隐患。应尽量避免这种情况。 - 避免冗余的 state。如果能在渲染期间从组件的
props
或其现有的state
变量中计算出一些信息,则不应将这些信息放入该组件的state
中。 - 避免重复的 state。当同一数据在多个
state
变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。 - 避免深度嵌套的 state。深度分层的
state
更新起来不是很方便。如果可能的话,最好以扁平化方式构建state
。
添加一个 state 变量
要添加 state
变量,先从文件顶部的 React
中导入 useState
:
1 | import { useState } from 'react'; |
然后接收 Hook 返回的两个值,这两个取值是相互对应的:
1 | const [state, setXxx] = useState(inieState); |
官网提供的完整实例如下:
1 | import { useState } from 'react'; |
Hooks ——以
use
开头的函数——只能在组件或自定义Hook
的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用Hook
。Hook
是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use”React
特性,类似于在文件顶部“导入”模块。React 文档 – state 组件的记忆
每个组件可以同时定义多个不同的 state
,不会相互影响。但如果发现经常同时更改多个 state
变量,那么可以由一个值为对象的 state
变量的多个字段对应一个 state 变量,在事件处理过程中,通过更改对象的相应字段即可。
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
React 文档 – state 组件的记忆
渲染和提交
在 React
中,UI 的更新通常有三个步骤:
- 触发一次渲染
- 初次渲染。应用启动时,会触发初次渲染。这个过程通过调用目标 DOM 节点的
createRoot
,然后再调用组件调用render
函数完成。 - 状态更新时重新渲染。一旦组件被初次渲染,就可以通过使用
setter
函数更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。
set 函数 仅更新 下一次 要渲染的状态变量。如果在调用setter
函数后读取状态变量,则还是会看到之前的旧值;如果提供的新值与当前state
相同,则不会触发对应组件及子组件的渲染。
- 初次渲染。应用启动时,会触发初次渲染。这个过程通过调用目标 DOM 节点的
React
渲染组件- 在进行初次渲染时,
React
会调用根组件。 - 对于后续的渲染,
React
会调用内部状态更新触发了渲染的函数组件。
这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么React
接下来就会渲染返回的组件,而如果返回的组件又返回了某个组件,那么 React 接下来就会渲染继续渲染后续返回的组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且React
确切知道哪些东西应该显示到屏幕上为止。
- 在进行初次渲染时,
React
把更改提交到 DOM 上- 对于初次渲染,
React
会使用appendChild()
DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重渲染,
React
将应用最少的必要操作(在渲染时计算。),以使得 DOM 与最新的渲染输出相互匹配。
- 对于初次渲染,
state 的进一步理解
state 如同一张快照 是官方对 state 的一个描述。据此,每次状态变化触发渲染时,渲染的结果都是根据上一次渲染后的结果计算得到。因此,即使在事件处理时多次根据状态的值计算的下一次渲染状态,得到的结果都是相同的。例如:
1 | import { useState } from 'react'; |
上述官方示例中,虽然在按钮的事件处理函数中对 number
状态连续做了三次加一,但是因为都是基于上一次渲染结果的快照(即组件加载时的 0)计算更新,所以最终在下一次渲染时,number
的值为 1,而非 3。另一方面,根据官方文档:
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。
React 文档 – state 如同一张快照
在一个新的场景下,如果先对状态进行更新,触发组件重新渲染以后,在直接使用对应的状态(例如弹窗显示等),实际上使用的值是上一次渲染的快照值。
1 | import { useState } from 'react'; |
多次更新状态值
React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。
React 文档 – 把一系列 state 更新加入队列
实际上,事件处理函数及其中任何代码执行完成 之后,UI 才会更新,也就是所谓的批处理,这可以保证渲染结果的正确性。
在上一节中的示例中,更新状态值都是基于快照的值进行的,而传递给更新函数的都是计算后的值。有时候需要连续多次的对状态的值进行更新,例如,这种场景下,可以传入一个函数给到设值函数,这样一个更新函数接收相应的参数,用于累积结果:
1 | import { useState } from 'react'; |
更新 state 中的对象及数组
在 React
中,如果状态对应的是一个对象或者数组,如果单纯相应对象的某个属性发生变化,是不会通知到 React 进行重新渲染的。只有通过设值函数利用修改后的 新对象 或者 新数组 更新状态,让 React
感知到相应变化,才能触发组件的重新渲染。总体上,把握一个原则, 不要妄想直接改变原有对象触发渲染,不管什么时候,要想React重新渲染组件,就通过复制或者能够返回一个新对象/数组的函数,传递一个全新的状态至对应状态的设置函数 。例如:
1 | import { useState } from 'react'; |
如果对象比较复杂,可以使用 展开语法 进行对象复制:
1 | // 复制对象 |
如果对象比较复杂,数据一些逻辑较为啰嗦,可以利用 Immer
库简化相应的更新操作。添加相应的依赖:
1 | { |
Immer
使用举例:
1 | import { useState } from 'react'; |