重学React(二):添加交互
背景:第一部分更多的是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} 等,这是约定俗成的命名方式,不是必须的
传递给事件处理函数的函数应直接传递,而非调用
传递一个函数(正确) | 调用一个函数(错误) |
---|---|
<button onClick={handleClick}> | <button onClick={handleClick()}> |
<button onClick={() => alert('...')}> | <button onClick={alert('...')}> |
这两者的区别在于,右边在传递函数时多了一个(),就使得这个函数变成了一个立即执行函数,渲染时就会触发,而不是点击才执行
由于事件处理函数声明于组件内部,可以直接访问组件的 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 中导入 useState
import { 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>);
}