React 18
往回啃啃啃,把开发里的实践和之前看的理论文档结合起来,react我学学学!
1 按钮button的禁用(disabled)属性控制

在前端开发中,按钮(button)的禁用状态是非常常用的交互设计,比如当操作不合法时(像你之前遇到的index已经是 0 时,“上一个” 按钮就应该禁用),通过禁用按钮可以直观地告诉用户 “这个操作现在不能执行”,同时防止无效点击。我们来详细了解一下:
1. 如何设置按钮禁用?
在 HTML 中,button元素有一个原生属性disabled,只要给它设置disabled(不管值是什么,甚至不需要值),按钮就会进入禁用状态:
<!-- 禁用状态的按钮 -->
<button disabled>我是禁用的按钮</button><!-- 非禁用状态(默认) -->
<button>我是可用的按钮</button>
在 React 中,因为要通过状态控制,所以会用 JSX 的语法,把disabled作为一个布尔值属性(注意 JSX 中属性名用驼峰式,不过disabled本身比较特殊,大小写不影响,但推荐小写):
// 用变量控制禁用状态
const isDisabled = true;
<button disabled={isDisabled}>禁用按钮</button>
2. 禁用状态的特点
- 视觉上:浏览器会默认给禁用的按钮添加灰色样式(不同浏览器可能略有差异),文字也会变浅,用户一眼就能看出 “不可点击”。
- 交互上:禁用的按钮不会响应点击事件(
onClick不会触发),也无法被选中或聚焦(比如按 Tab 键跳不到这个按钮)。 - 语义上:屏幕阅读器等辅助工具会识别
disabled属性,告诉用户 “这个按钮当前不可用”,符合无障碍设计(a11y)要求。
3. 结合你的场景:动态控制禁用状态
回到你之前的index问题,当index为 0 时,“上一个” 按钮应该禁用。怎么实现呢?
步骤:
- 定义一个变量,判断是否需要禁用(比如
index === 0时禁用)。 - 把这个变量传给按钮的
disabled属性。
示例代码:
function MyComponent() {const [index, setIndex] = useState(0); // 假设初始index是0// 判断“上一个”按钮是否该禁用:当index为0时禁用const isPreviousDisabled = index === 0;function handlePreviousClick() {setIndex(index - 1);}return (<div>{/* 禁用状态由isPreviousDisabled控制 */}<button onClick={handlePreviousClick} disabled={isPreviousDisabled}>上一个</button></div>);
}
这样,当index为 0 时,按钮会变成禁用状态,点击也不会触发handlePreviousClick(即使你不写之前的if判断,也不会有问题,相当于双重保险)。
4. 自定义禁用样式
浏览器默认的禁用样式可能不符合你的 UI 设计,这时可以通过 CSS 自定义:
/* 禁用状态的按钮样式 */
button:disabled {background-color: #f0f0f0; /* 浅灰色背景 */color: #999; /* 灰色文字 */cursor: not-allowed; /* 鼠标悬停时显示“不允许”图标 */border: 1px solid #ddd;/* 可以去掉默认的禁用透明度 */opacity: 1;
}
注意 CSS 中用button:disabled选择器来定位禁用状态的按钮。
5. 为什么需要禁用按钮?
- 提升用户体验:明确告诉用户当前操作不可行,避免用户困惑(比如点了没反应,用户可能以为按钮坏了)。
- 减少无效逻辑:虽然你可以在
onClick里用if判断是否执行操作,但禁用按钮从源头就阻止了点击,更高效。 - 符合交互规范:是前端交互的通用设计模式,用户看到禁用按钮会自然理解 “现在不能点”。
试着在你之前的代码里加上disabled属性控制,看看效果吧~ 比如当index为 0 时,“上一个” 按钮变灰且点不动,是不是更合理了?
2 target属性
target,这是事件对象(比如前面说的e)里一个非常核心的属性,我们可以深入理解一下它的含义和用法。
1. target 到底指什么?
e.target 表示的是触发当前事件的那个最底层的 DOM 元素。简单说,就是 “谁直接引发了这个事件”。
举个例子:
<div onClick={(e) => console.log(e.target)}><button>点击我</button>
</div>
当你点击按钮时,事件会从按钮(最底层元素)向上传递到外层的div。这时候,e.target 指向的是按钮(因为是按钮直接被点击),而不是外层的div。
2. 对比一个容易混淆的属性:currentTarget
为了更好理解target,可以对比一下e.currentTarget:currentTarget 指的是绑定了当前事件处理函数的那个元素。
还是上面的例子:
<div onClick={(e) => {console.log('target:', e.target); // 输出 <button>console.log('currentTarget:', e.currentTarget); // 输出 <div>
}}><button>点击我</button>
</div>
- 事件处理函数绑在
div上,所以currentTarget是div。 - 但事件是由
button触发的,所以target是button。
这就是它们的核心区别:target是事件的 “源头”,currentTarget是事件处理函数的 “宿主”。
3. 实际开发中的用法
target最常用的场景,就是在事件冒泡时精准定位 “到底点了哪个元素”。
比如一个列表,每个列表项里有文字和删除按钮,点击不同部分做不同处理:
<ul onClick={(e) => {// 如果点击的是删除按钮(假设按钮有 class="delete-btn")if (e.target.classList.contains('delete-btn')) {console.log('删除当前项');} // 如果点击的是列表项的文字部分else if (e.target.tagName === 'SPAN') {console.log('查看详情');}
}}><li><span>第一项内容</span><button class="delete-btn">删除</button></li><li><span>第二项内容</span><button class="delete-btn">删除</button></li>
</ul>
这里利用e.target判断点击的具体元素,避免了给每个按钮和文字单独绑定事件,更高效(这种方式叫 “事件委托”)。
4. 总结
e.target 的核心意义是:找到触发事件的具体元素,它让我们能在复杂的 DOM 结构中精准处理用户交互。
你可以试着写一个包含多层嵌套元素的结构,给外层绑定点击事件,然后打印e.target和e.currentTarget,观察它们的区别,这样理解会更深刻~
3 写处理表单错误百出的代码改错
不自己动手写写真以为掌握了,眼睛会了但是手还没会,脑子也没回,来暴露一下基础不牢地动山摇的代码。
export default function Form() {const [firstName ,setFirstName ] =('');const[ lastName ,setLastName] = ('');function handleFirstNameChange(e) {setFirstName(e.target.value);}function handleLastNameChange(e) {setLastName = (e.target.value);}function handleReset() {setFirstName('');setLastName('');}return (<form onSubmit={e => e.preventDefault()}><inputplaceholder="First name"value={firstName}onChange={handleFirstNameChange}/><inputplaceholder="Last name"value={lastName}onChange={handleLastNameChange}/><h1>Hi, {firstName} {lastName}</h1><button onClick={handleReset}>Reset</button></form>);
}
//为啥没有默认显示lastname,还有我写的逻辑对吗你的代码里有几个小问题,导致可能出现 “lastName 没有默认显示” 或者逻辑不生效的情况,我们一步步来分析和修正,顺便梳理表单处理的逻辑~
一、首先看明显的语法错误
useState的使用错误你写的是:const [firstName, setFirstName] = (''); const [lastName, setLastName] = ('');这是错的!
useState是 React 的钩子函数,必须通过useState()调用,正确写法是:const [firstName, setFirstName] = useState(''); // 初始化值为'' const [lastName, setLastName] = useState('');并且需要在文件顶部导入
useState:import { useState } from 'react';(如果不导入,React 会报错 “useState is not defined”)
setLastName的赋值错误在handleLastNameChange里,你写的是:setLastName = (e.target.value); // 错误!setLastName是useState返回的状态更新函数,应该通过调用它来更新状态,而不是给它赋值。正确写法是:setLastName(e.target.value); // 调用函数,传入新值
二、为什么 “lastName 没有默认显示”?
默认情况下,firstName和lastName的初始值都是''(空字符串),所以页面加载时h1里会显示 “Hi,”(两个空字符串拼接),这是正常的。
如果输入内容后lastName依然不显示,很大概率是上面第 2 个错误导致的:因为setLastName = ...是赋值操作,并没有真正更新lastName状态,所以输入时lastName始终是初始的'',自然不显示。
三、整体逻辑是否正确?
修正上述错误后,核心逻辑是对的:
- 用
useState管理两个输入框的状态(firstName和lastName),属于 “受控组件” 模式; onChange事件触发时,通过e.target.value获取输入值,再用setXxx更新状态;handleReset函数通过setXxx('')重置状态,输入框会同步清空,符合预期;form标签添加onSubmit={e => e.preventDefault()}阻止默认提交行为,避免页面刷新,这也是正确的。
四、修正后的完整代码
import { useState } from 'react'; // 导入useStateexport default function Form() {// 正确初始化状态const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');function handleFirstNameChange(e) {setFirstName(e.target.value); // 正确更新状态}function handleLastNameChange(e) {setLastName(e.target.value); // 这里修正为调用函数}function handleReset() {setFirstName('');setLastName('');}return (<form onSubmit={e => e.preventDefault()}><inputplaceholder="First name"value={firstName}onChange={handleFirstNameChange}/><inputplaceholder="Last name"value={lastName}onChange={handleLastNameChange}/><h1>Hi, {firstName} {lastName}</h1><button onClick={handleReset}>Reset</button></form>);
}
五、拓展:优化代码的小技巧
你会发现handleFirstNameChange和handleLastNameChange逻辑几乎一样,只是更新的状态不同。可以合并成一个函数,通过参数区分要更新的状态,减少重复代码:
// 合并后的处理函数:接收一个参数(要更新的状态名)
function handleInputChange(e, field) {if (field === 'firstName') {setFirstName(e.target.value);} else if (field === 'lastName') {setLastName(e.target.value);}
}// 调用时传入对应的字段名
<inputplaceholder="First name"value={firstName}onChange={(e) => handleInputChange(e, 'firstName')} // 注意这里用箭头函数传参
/>
<inputplaceholder="Last name"value={lastName}onChange={(e) => handleInputChange(e, 'lastName')}
/>
这样当表单字段增多时(比如添加邮箱、电话),不需要写多个重复的处理函数,代码更简洁~
试着按照修正后的代码运行一下,看看输入内容后firstName和lastName是否能正常显示,重置按钮是否能清空内容。如果还有问题,可以告诉我具体现象,我们再进一步分析~
【事件处理函数中参数的匹配问题?为什么handleInputChange(e, field) 里的 e 和 field 为什么不会混淆,为什么 e 总能拿到事件对象,而 field 是我们手动传的参数。】
这其实涉及到 React 事件处理的参数传递机制,我们一步步理清楚:
1. 先明确:事件处理函数的第一个参数默认是事件对象
当你给元素绑定事件(比如 onChange、onClick)时,React 会在事件触发时自动传递一个事件对象作为第一个参数。这个规则和原生 JavaScript 一致。
比如最简单的情况:
<input onChange={(e) => console.log(e)} />
这里的 e 不需要我们手动传,React 会自动把事件对象塞进去,所以你总能通过第一个参数拿到事件对象(比如 e.target.value)。
2. 当需要额外参数时,如何避免混淆?
像你之前的 handleInputChange(e, field),需要同时用到事件对象 e 和我们自定义的 field 参数。这时候要注意:React 只会自动传事件对象作为第一个参数,额外的参数需要我们手动传递。
具体做法是用箭头函数包裹,显式传递参数:
// 正确写法:用箭头函数接收 React 自动传的 e,再手动加上 field 参数
onChange={(e) => handleInputChange(e, 'firstName')}
这里的执行过程是:
- 输入框变化时,React 触发
onChange,自动传入事件对象e。 - 箭头函数
(e) => ...接收这个e。 - 箭头函数调用
handleInputChange时,手动把e和'firstName'作为参数传进去。 - 所以
handleInputChange的第一个参数e就是事件对象,第二个参数field就是'firstName',不会混淆。
3. 为什么不会误判成 value?
value 是事件对象 e.target 的一个属性(e.target.value),而我们传的 field 是一个独立的参数(比如字符串 'firstName'),两者在 “身份” 和 “用途” 上完全不同:
e是事件对象,包含了事件的所有信息(触发元素、值、类型等),是 React 自动传递的。field是我们自己定义的标记(比如用来区分 “firstName” 还是 “lastName”),是手动传递的额外参数。
函数内部通过参数位置(第一个是 e,第二个是 field)来区分,所以不会把 field 误当成 e.target.value。
4. 反例:如果不注意参数顺序会怎样?
如果写成这样(参数顺序反了):
// 错误示例:参数顺序反了
function handleInputChange(field, e) { // 这里的 field 会变成事件对象 e,e 会变成 'firstName',完全混乱console.log(field.target.value); // 报错!因为 'firstName' 没有 target 属性
}// 调用时
onChange={(e) => handleInputChange('firstName', e)}
这时候就会混淆,因为参数顺序和函数定义的顺序不匹配。所以关键是保证传递的参数顺序和函数接收的顺序一致。
总结
- React 事件处理函数的第一个参数默认是事件对象
e(自动传递)。 - 额外的参数需要通过箭头函数手动传递,且要保证参数顺序和函数定义一致。
e.target.value是事件对象里的属性,而自定义参数(如field)是独立的,通过参数位置区分,不会混淆。
你可以试着故意写错参数顺序,看看控制台会报什么错,这样能更直观地理解参数匹配的重要性~
4 模板字符串(Template Literals) 语法
你问的${name}涉及到 JavaScript 中的模板字符串(Template Literals) 语法,这是 ES6 引入的一种字符串处理方式,非常实用。我们来详细理解它:
1. 什么是模板字符串?
模板字符串用 ** 反引号()** 包裹(不是单引号或双引号),里面可以通过 ${变量或表达式}` 的形式嵌入变量或计算结果,比传统的字符串拼接更简洁。
比如你的代码里:
alert(`Hello, ${name}!`);
等价于用传统字符串拼接的写法:
alert("Hello, " + name + "!"); // 效果完全一样
但模板字符串的写法更直观,尤其是拼接多个变量时优势明显。
2. 模板字符串的核心特点
- 用反引号(`)包裹:这是区分模板字符串和普通字符串的标志。
${}用于嵌入内容:${}内部可以放变量、函数调用、算术运算等,会自动计算并转换为字符串。
举几个例子:
const age = 20;
const message = `I am ${age + 5} years old`; // 嵌入表达式,结果是 "I am 25 years old"function getGreeting() {return "Hello";
}
const text = `${getGreeting()}, world!`; // 嵌入函数调用,结果是 "Hello, world!"
3. 你的代码里需要注意的点
虽然模板字符串的用法是对的,但你的代码有个小问题:alert 里的 name 可能不是最新值。
原因是:setName 是异步的(React 会批量更新状态以提高性能),所以在 handleClick 中,调用 setName 后立即访问 name,拿到的还是更新前的旧值。
比如初始 name 是 '',当你在 prompt 中输入 Alice 后:
setName('Alice')会请求更新状态,但不会立即生效。- 此时
alert(Hello, ${name}!)中的name还是旧值'',所以会弹出Hello, !。
解决办法:用 prompt 的返回值直接拼接(因为此时我们已经知道新值了):
function handleClick() {const newName = prompt('What is your name?'); // 先保存新值setName(newName); // 更新状态alert(`Hello, ${newName}!`); // 用新值拼接,确保正确
}
总结
${}是模板字符串中嵌入变量 / 表达式的语法,用反引号包裹字符串。- 优势:比
+拼接更简洁,尤其适合多变量拼接场景。 - 注意:React 状态更新是异步的,更新后立即访问状态可能拿到旧值,需用临时变量保存新值。
你可以试着修改代码,用临时变量 newName 来显示 alert,看看是否能正确弹出输入的名字~
