c
组件状态,事件处理
让我们回到使用React的工作上来。
我们从一个新的例子开始。
const Hello = (props) => {
return (
<div>
<p>
Hello {props.name}, you are {props.age} years old
</p>
</div>
)
}
const App = () => {
const name = 'Peter'
const age = 10
return (
<div>
<h1>Greetings</h1>
<Hello name="Maya" age={26 + 10} />
<Hello name={name} age={age} />
</div>
)
}
Component helper functions
让我们扩展我们的Hello组件,让它猜测被问候者的出生年份。
const Hello = (props) => {
const bornYear = () => { const yearNow = new Date().getFullYear() return yearNow - props.age }
return (
<div>
<p>
Hello {props.name}, you are {props.age} years old
</p>
<p>So you were probably born in {bornYear()}</p> </div>
)
}
猜测出生年份的逻辑被分离到一个自己的函数中,在组件渲染时被调用。
这个人的年龄不需要作为参数传给函数,因为它可以直接访问传给组件的所有prop。
如果我们仔细检查我们当前的代码,我们会注意到这个辅助函数实际上是定义在另一个函数里面,这个函数定义了我们组件行为。在Java编程中,在另一个函数中定义一个函数是很复杂和麻烦的,所以不是那么常见。然而,在JavaScript中,在函数中定义函数是一种常规操作。
Destructuring
在我们继续前进之前,我们将看一下JavaScript语言的一个小但有用的功能,它是在ES6规范中添加的,它允许我们在赋值时从对象和数组中destructure解构取值。
在我们之前的代码中,我们必须将传递给我们组件的数据引用为props.name和props.age。在这两个表达式中,我们不得不在代码中重复props.age两次。
由于props是一个对象
props = {
name: 'Arto Hellas',
age: 35,
}
我们可以通过将属性值直接分配给两个变量name和age来简化我们的组件,然后我们可以在代码中使用。
const Hello = (props) => {
const name = props.name const age = props.age
const bornYear = () => new Date().getFullYear() - age
return (
<div>
<p>Hello {name}, you are {age} years old</p> <p>So you were probably born in {bornYear()}</p>
</div>
)
}
注意,在定义bornYear函数时,我们也利用了箭头函数的更紧凑的语法。如前所述,如果一个箭头函数由一个表达式组成,那么函数体就不需要写在大括号里。在这种更紧凑的形式下,函数只是返回单个表达式的结果。
简而言之,下面显示的两个函数定义是等价的。
const bornYear = () => new Date().getFullYear() - age
const bornYear = () => {
return new Date().getFullYear() - age
}
解构使得变量的赋值更加容易,因为我们可以用它来提取和收集一个对象的属性值到单独的变量中。
const Hello = (props) => {
const { name, age } = props const bornYear = () => new Date().getFullYear() - age
return (
<div>
<p>Hello {name}, you are {age} years old</p>
<p>So you were probably born in {bornYear()}</p>
</div>
)
}
如果我们要解构的对象有以下值
props = {
name: 'Arto Hellas',
age: 35,
}
表达式const { name, age } = props 将'Arto Hellas'的值赋给name,将35岁赋给age。
我们可以把解构再向前推进一步。
const Hello = ({ name, age }) => { const bornYear = () => new Date().getFullYear() - age
return (
<div>
<p>
Hello {name}, you are {age} years old
</p>
<p>So you were probably born in {bornYear()}</p>
</div>
)
}
传递给组件的prop现在被直接解构为变量name和age。
这意味着我们不是把整个prop对象分配到一个叫做props的变量中,然后再把它的属性分配到变量name和age中。
const Hello = (props) => {
const { name, age } = props
我们通过对作为参数传递给组件函数的props对象进行重构,直接将属性值分配给变量。
const Hello = ({ name, age }) => {
Page re-rendering
到目前为止,我们所有的应用都是这样的,在最初的渲染之后,其外观保持不变。如果我们想创建一个计数器,其值随着时间的推移或点击按钮而增加呢?
让我们从下面开始。文件App.js变成。
const App = (props) => {
const {counter} = props
return (
<div>{counter}</div>
)
}
export default App
而文件index.js变成了:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
let counter = 1
ReactDOM.createRoot(document.getElementById('root')).render(
<App counter={counter} />
)
App组件通过counterprop得到了计数器的值。这个组件将该值渲染到屏幕上。当 counter 的值改变时,会发生什么?即使我们加入以下内容
counter += 1
,该组件也不会重新渲染。我们可以通过第二次调用render方法来让组件重新渲染,例如用以下方式。
let counter = 1
const refresh = () => {
ReactDOM.createRoot(document.getElementById('root')).render(
<App counter={counter} />
)
}
refresh()
counter += 1
refresh()
counter += 1
refresh()
重新渲染的命令已经被包裹在refresh函数中,以减少复制粘贴的代码量。
现在这个组件渲染了三次,首先是数值1,然后是2,最后是3。 然而,数值1和2在屏幕上显示的时间非常短,以至于它们无法被注意到。
我们可以通过使用setInterval来实现更有趣的功能,每秒钟重新渲染并增加计数器。
setInterval(() => {
refresh()
counter += 1
}, 1000)
重复调用render方法并不是重新渲染组件的推荐方式。接下来,我们将介绍一种更好的方法来实现这一效果。
Stateful component
到目前为止,我们所有的组件都是简单的,即它们不包含任何在组件生命周期中可能发生变化的状态。
接下来,让我们在React的状态钩子的帮助下,给我们的应用的App组件添加状态。
我们将改变应用的内容如下。 index.js返回到
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
而App.js则改为如下。
import { useState } from 'react'
const App = () => {
const [ counter, setCounter ] = useState(0)
setTimeout( () => setCounter(counter + 1), 1000 )
return (
<div>{counter}</div>
)
}
export default App
在第一行,文件导入了useState函数。
import { useState } from 'react'
定义该组件的函数体以函数调用开始。
const [ counter, setCounter ] = useState(0)
该函数调用将state添加到组件中,并将其初始化为0值。该函数返回一个包含两个项目的数组。我们通过使用前面显示的解构赋值语法将这些项目赋值给变量counter和setCounter。
counter变量被分配了state的初始值,即0。变量setCounter被分配给一个函数,该函数将被用来修改状态。
应用调用setTimeout函数并传递给它两个参数:一个用于增加计数器状态的函数和一个一秒钟的超时。
setTimeout(
() => setCounter(counter + 1),
1000
)
作为第一个参数传递给setTimeout函数的函数在调用setTimeout函数一秒钟后被调用
() => setCounter(counter + 1)
当修改状态的函数setCounter被调用时,React重新渲染组件,这意味着组件函数的函数体被重新执行。
() => {
const [ counter, setCounter ] = useState(0)
setTimeout(
() => setCounter(counter + 1),
1000
)
return (
<div>{counter}</div>
)
}
第二次执行组件函数时,它会调用useState函数并返回状态的新值。1.再次执行函数体的时候,也会对setTimeout进行新的函数调用,执行一秒钟的超时并再次增加counter状态。因为counter变量的值是1,增加1的值与将counter的值设置为2的表达式本质上是一样的。
() => setCounter(2)
同时,counter的旧值--"1"--被渲染在屏幕上。
每次setCounter修改状态都会导致组件重新渲染。一秒钟后,状态的值将被再次增加,只要应用在运行,这个过程就会一直重复下去。
如果组件在你认为应该渲染的时候没有渲染,或者在 "错误的时间 "渲染,你可以通过将组件的变量值记录到控制台来调试应用。如果我们对我们的代码做如下补充。
const App = () => {
const [ counter, setCounter ] = useState(0)
setTimeout(
() => setCounter(counter + 1),
1000
)
console.log('rendering...', counter)
return (
<div>{counter}</div>
)
}
我们可以很容易地跟踪对App组件的渲染函数的调用。
Event handling
我们已经提到了事件处理程序,这些程序在第0章节中被注册,以便在特定事件发生时被调用。例如,用户与网页中不同元素的交互会导致一系列不同类型的事件被触发。
让我们改变应用,使增加计数器在用户点击按钮时发生,这是用按钮元素实现的。
按钮元素支持所谓的鼠标事件,其中点击是最常见的事件。尽管名字叫 "鼠标事件",但按钮上的点击事件也可以用键盘或触摸屏来触发鼠标事件。
在React中,为点击事件注册一个事件处理函数是这样的:
const App = () => {
const [ counter, setCounter ] = useState(0)
const handleClick = () => { console.log('clicked') }
return (
<div>
<div>{counter}</div>
<button onClick={handleClick}> plus </button> </div>
)
}
我们将按钮的onClick属性的值设定为对代码中定义的handleClick函数的引用。
现在每次点击plus按钮都会导致handleClick函数被调用,这意味着每次点击事件都会向浏览器控制台记录一个clicked消息。
事件处理函数也可以直接在onClick-attribute的赋值中定义。
const App = () => {
const [ counter, setCounter ] = useState(0)
return (
<div>
<div>{counter}</div>
<button onClick={() => console.log('clicked')}> plus
</button>
</div>
)
}
通过改变事件处理程序为以下形式
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
我们实现了期望的行为,也就是说,counter的值增加了一个,组件被重新渲染了。
我们还可以添加一个按钮来重设计数器。
const App = () => {
const [ counter, setCounter ] = useState(0)
return (
<div>
<div>{counter}</div>
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
<button onClick={() => setCounter(0)}> zero </button> </div>
)
}
我们的应用现在已经准备好了!
Event handler is a function
我们为我们的按钮定义事件处理程序,并声明它们的onClick属性。
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
如果我们试图以更简单的形式来定义事件处理程序呢?
<button onClick={setCounter(counter + 1)}>
plus
</button>
这将完全破坏我们的应用。
这是怎么回事?一个事件处理程序应该是一个函数或者一个函数引用,而当我们写道:
<button onClick={setCounter(counter + 1)}>
该事件处理程序实际上是一个函数调用。在很多情况下这是可以的,但在这种特殊情况下就不行了。在一开始,counter变量的值是0。当React第一次渲染组件时,它执行了函数调用setCounter(0+1),并将组件的状态值改为1。
这将导致该组件被重新渲染,React将再次执行setCounter函数调用,并且状态将改变,导致再次重新渲染......
让我们像之前那样定义事件处理程序。
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
现在按钮的属性定义了当按钮被点击时会发生 - onClick - 有值 () => setCounter(counter + 1)。
只有当用户点击按钮时,才会调用setCounter函数。
通常在JSX模板中定义事件处理程序并不是一个好主意。
这里可以,因为我们的事件处理程序非常简单。
无论如何,让我们把事件处理程序分成独立的函数。
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1) const setToZero = () => setCounter(0)
return (
<div>
<div>{counter}</div>
<button onClick={increaseByOne}> plus
</button>
<button onClick={setToZero}> zero
</button>
</div>
)
}
在这里,事件处理程序已经被正确定义。onClick属性的值是一个变量,包含对一个函数的引用。
<button onClick={increaseByOne}>
plus
</button>
Passing state to child components
我们建议编写的React组件要小,并且可以在整个应用甚至项目中重用。让我们重构我们的应用,使其由三个小的组件组成,一个组件用于显示计数器,两个组件用于按钮。
我们首先实现一个Display组件,负责显示计数器的值。
React的一个最佳做法是在组件层次结构中将状态提升。文档中说。
通常,几个组件需要反映相同的变化数据。我们建议将共享状态提升到它们最接近的共同祖先。
所以让我们把应用的状态放在App组件中,并通过props把它传递给Display组件。
const Display = (props) => {
return (
<div>{props.counter}</div>
)
}
使用该组件是直接的,因为我们只需要将 计数器 的状态传递给它。
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1)
const setToZero = () => setCounter(0)
return (
<div>
<Display counter={counter}/> <button onClick={increaseByOne}>
plus
</button>
<button onClick={setToZero}>
zero
</button>
</div>
)
}
一切仍在运作。当按钮被点击,App被重新渲染时,它所有的子节点包括Display组件也被重新渲染。
接下来,让我们为我们应用的按钮制作一个Button组件。我们必须通过组件的props来传递事件处理程序以及按钮的标题。
const Button = (props) => {
return (
<button onClick={props.onClick}>
{props.text}
</button>
)
}
我们的App组件现在如下所示:
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1)
const decreaseByOne = () => setCounter(counter - 1) const setToZero = () => setCounter(0)
return (
<div>
<Display counter={counter}/>
<Button onClick={increaseByOne} text='plus' /> <Button onClick={setToZero} text='zero' /> <Button onClick={decreaseByOne} text='minus' /> </div>
)
}
因为我们现在有了一个容易重用的按钮组件,我们也在我们的应用中实现了新的功能,增加了一个可以用来减少计数器的按钮。
事件处理程序是通过 onClick prop传递给Button组件的。这个prop的名字本身并不重要,但我们的命名选择并不是完全随机的。React的官方 tutorial建议采用这种惯例。
Changes in state cause rerendering
让我们再一次回顾一下应用工作的主要原则。
当应用启动时,App中的代码被执行。这段代码使用一个useState钩子来创建应用的状态,设置变量counter的初始值。
这个组件包含Display组件--显示计数器的值0--和三个Button组件。这些按钮都有事件处理程序,用来改变计数器的状态。
当其中一个按钮被点击时,事件处理程序被执行。该事件处理程序通过setCounter函数改变App组件的状态。
调用一个改变状态的函数会导致组件重新渲染。
所以,如果用户点击了plus按钮,按钮的事件处理程序将counter的值改为1,并且App组件被重新渲染。
这导致其子组件Display和Button也被重新渲染。
Display接收计数器的新值,1,作为prop。Button组件接收事件处理程序,可以用来改变计数器的状态。
Refactoring the components
显示计数器数值的组件如下。
const Display = (props) => {
return (
<div>{props.counter}</div>
)
}
该组件只使用其props的counter字段。
这意味着我们可以通过使用解构来简化组件,像这样。
const Display = ({ counter }) => {
return (
<div>{counter}</div>
)
}
定义该组件的函数只包含返回语句,所以
我们可以用箭头函数的更简洁的形式来定义这个函数。
const Display = ({ counter }) => <div>{counter}</div>
我们也可以简化Button组件。
const Button = (props) => {
return (
<button onClick={props.onClick}>
{props.text}
</button>
)
}
我们可以使用解构来从props中只获得所需的字段,并使用更紧凑的箭头函数形式。
注意: 在构建自己的组件时,你可以自由命名他们的事件处理器属性,对此你可以参考 react 命名事件处理器属性 的说明。具体如下:
根据约定,事件处理程序属性应以
on
开头,后面跟着一个大写字母。 例如,按钮组件的onClick
属性可以称为onSmash
。
const Button = ({ onClick, text }) => (
<button onClick={onClick}>
{text}
</button>
)
定义该组件的函数只包含返回语句,所以我们可以进一步简化按钮组件,只需将返回语句声明在一行中。
const Button = ({ onSmash, text }) => <button onClick={onSmash}>{text}</button>
注意: 然而,要小心不要过于简化您的组件,因为这会使日后增加复杂性变得更加繁琐。