跳到内容

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.nameprops.age。在这两个表达式中,我们不得不在代码中重复props.age两次。

由于props是一个对象

props = {
  name: 'Arto Hellas',
  age: 35,
}

我们可以通过将属性值直接分配给两个变量nameage来简化我们的组件,然后我们可以在代码中使用。

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现在被直接解构为变量nameage

这意味着我们不是把整个prop对象分配到一个叫做props的变量中,然后再把它的属性分配到变量nameage中。

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值。该函数返回一个包含两个项目的数组。我们通过使用前面显示的解构赋值语法将这些项目赋值给变量countersetCounter

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组件的渲染函数的调用。

fullstack content

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>

这将完全破坏我们的应用。

fullstack content

这是怎么回事?一个事件处理程序应该是一个函数或者一个函数引用,而当我们写道:

<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组件被重新渲染。

这导致其子组件DisplayButton也被重新渲染。

Display接收计数器的新值,1,作为prop。Button组件接收事件处理程序,可以用来改变计数器的状态。

Refactoring the components

显示计数器数值的组件如下。

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

该组件只使用其propscounter字段。

这意味着我们可以通过使用解构来简化组件,像这样。

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中只获得所需的字段,并使用更紧凑的箭头函数形式。

const Button = ({ onClick, text }) => (
  <button onClick={onClick}>
    {text}
  </button>
)