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 组件,让它能猜到被问候(greeted)者的出生年份:

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>
  )
}

猜测出生年份的逻辑被放到了它自己的函数中,这个函数会在渲染组件时被调用。

用户的年龄不必单独作为参数传递给函数,因为它可以直接访问传递给组件的所有props。

如果仔细观察当前代码,我们会注意到这种辅助函数实际上是在另一个函数中定义的,而这个函数是我们用来定义组件行为的。 在 java 中,在一个函数中定义另一个函数是复杂且笨重的,但在 JavaScript 中,在函数中定义函数是一种常规操作。

Destructuring

【解构】

在我们继续之前,我们将看一看 JavaScript 在 ES6规范中添加的的一个很小、但是有用的特性,它允许我们在赋值时从对象和数组中解构出值。

在前面的代码中,我们必须将 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>
  )
}

传递给组件的props现在直接解构为变量 name 和 age。

这意味着不需要将整个 props 对象赋值给一个名为props 的变量中,然后再将其属性分配到变量 name 和 age 中:

const Hello = (props) => {
  const { name, age } = props

我们只需将 props 对象作为参数传递给组件函数,通过对 props 对象的解构,能够直接将属性值赋给变量:

const Hello = ({ name, age }) => {

Page re-rendering

【页面重渲染】

到目前为止,我们的所有应用都是这样的,即在最初的渲染之后,它们的外观一直是相同的。 如果我们想要创建一个计数器,在这个计数器中的值随着时间的变化而增加,或者通过点击一个按钮而增加,会是什么样呢?

让我们从下面的代码开始, App.js 内容变成了:

import React from 'react'

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

export default App

index.js 变成了:

import ReactDOM from 'react-dom'
import App from './App'

let counter = 1

ReactDOM.render(
  <App counter={counter} />, 
  document.getElementById('root')
)

注意,当你修改 index.js 文件时, React 并不会自动刷新,所以你需要重新加载浏览器页面,新的内容才会展示出来。

App 组件通过counter属性,接收到counter的值。 根组件随即将值渲染到屏幕上。 当计数器的值发生变化时会发生什么呢? 即,如果我们要添加命令

counter += 1

部件并不会重新渲染。 我们可以通过再次调用 ReactDOM.render 方法让组件重新渲染,例如:

let counter = 1

const refresh = () => {
  ReactDOM.render(<App counter={counter} />, 
  document.getElementById('root'))
}

refresh()
counter += 1
refresh()
counter += 1
refresh()

重新渲染命令被包装在了 refresh 函数中,以减少复制粘贴代码的数量。

现在,组件渲染了三次,值由1、2最终变成了3。 但是,值1和2在屏幕上显示的时间非常短,因此无法注意到它们。

我们可以通过使用 setInterval,通过每隔一秒来重渲染一次并让计数器+1,来实现这个有趣的功能 :

setInterval(() => {
  refresh()
  counter += 1
}, 1000)

重复调用 ReactDOM.render-方法并不是重新渲染组件的推荐方法。 接下来,我们将介绍一种更好的,实现相同效果的方法。

Stateful component

【有状态组件】

到目前为止,我们的所有组件都很简单,因为它们没有包含任何组件(生命周期中可能变化)的状态。

接下来,让我们通过 React 的 state hook 向应用的App 组件中添加状态。

我们会把应用做如下修改, index.js 重新变成了:

import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, 
document.getElementById('root'))

App.js 变成了:

import React, { useState } from 'react'
const App = () => {
  const [ counter, setCounter ] = useState(0)
  setTimeout(    () => setCounter(counter + 1),    1000  )
  return (
    <div>{counter}</div>
  )
}

export default App

在第一行中,文件导入了 useState 函数:

import React, { useState } from 'react'

定义组件的函数体以如下函数调用开始:

const [ counter, setCounter ] = useState(0)

函数调用将state 添加到组件,并将其值用0进行初始化。 该函数返回一个包含两个元素的数组。 我们使用前面所讲的解构赋值语法将元素分配给变量 countersetCounter

counter 变量被赋予的初始值state 为零。 变量 setCounter 被分配给一个函数,该函数将用于修改 state

这个应用调用setTimeout函数,并传递给它两个参数: 第一个是增加计数器状态的函数,第二个是1秒钟的超时设置:

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变量的值现在是1,所以将该值增加1本质上等同于将计数器的状态值设置为2。

() => setCounter(2)

与此同时,计数器的旧值“1”被渲染到了屏幕上。

每次 setCounter 修改状态时,它都会导致组件重新渲染。 状态的值将在一秒钟后再次递增,并且在应用运行期间循环往复。

如果组件在该渲染时没有渲染,或者在“错误的时间”进行了渲染,您可以通过将组件变量的值打印到控制台来调试应用。 如果我们在代码中添加了如下内容:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  console.log('rendering...', counter)
  return (
    <div>{counter}</div>
  )
}

很容易就能跟踪和捕获到App 组件 render 函数的调用:

fullstack content

Event handling

【事件处理】

我们已经在第0章中多次提到事件处理程序,它们(被注册为)在特定事件发生时进行调用。 例如,用户与一个网页的不同元素的交互可能会触发一系列不同类型的事件。

让我们修改一下应用,这样当用户单击一个按钮时,计数器就会增加,这可以通过button-元素实现的。

button-元素支持所谓的鼠标事件 ,其中点击是最常见的事件。点击事件同样可能被键盘或者触屏设备所触发,虽然名字叫鼠标事件

在 React 中,将一个事件处理函数注册到click 事件 发生 时,如下:

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 属性的值中直接定义:

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>

我们实现了预期,也就是计数器的值增加了1,而且组件被重新渲染。

让我们再添加一个重置计数器的按钮:

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 中的一个最佳实践是将 状态提升 ,提升到组件层次结构中足够高的位置,文档中是这么说的:

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor. 通常,几个组件需要反映相同的变化数据。 我们建议将共享状态提升到它们最接近的共同祖先。

因此,让我们将应用的状态放在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>
  )
}

由于我们现在有一个易于重用的Button 组件,我们还可以通过添加一个可用于减法的计数器按钮,为应用实现一个新功能。

事件处理程序通过onClick 属性传递给Button 组件。 props的名字本身并不重要,但是我们的命名选择并不是完全随机的,例如 React 自己的官方教程就建议了这些约定。

Changes in state cause rerendering

【状态的改变导致重新渲染】

让我们再次回顾一下应用如何工作的主要内容。

当应用启动时,执行 App 中的代码。 此代码使用useState hook 创建了计数器的应用状态初始值 counter

该组件包含 Display 组件, 显示了当前的计数为0 , 以及三个 Button 组件。button 都包含事件处理,用来改变计数器的状态。

当单击其中一个按钮时,将执行事件处理程序。 事件处理程序使用 setCounter 函数更改 App 组件的状态。

调用一个改变状态的函数会导致组件的重新渲染。

因此,如果用户单击plus 按钮,按钮的事件处理程序将 counter 的值更改为1,并重新渲染 App 组件。

这将导致其子组件 Display 和 Button 也被重新渲染。

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

Refactoring the components

【重构组件】

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

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

该组件只使用其propscounter 字段。

这意味着我们可以使用解构简化组件,如下所示:

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

定义组件的方法只包含 return 语句,因此

我们可以使用更紧凑的箭头函数来定义方法:

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>
)