react 组件化开发_生命周期_表单处理

组件基本介绍

我们从上面可以清楚地看到,组件本质上就是类和函数,但是与常规的类和函数不同的是,组件承载了渲染视图的 UI 和更新视图的 setState 、 useState 等方法。React 在底层逻辑上会像正常实例化类和正常执行函数那样处理的组件。

因此,函数与类上的特性在 React 组件上同样具有,比如原型链,继承,静态属性等,所以不要把 React 组件和类与函数独立开来。

接下来,我们一起着重看一下 React 对组件的处理流程。

对于类组件的执行,是在react-reconciler/src/ReactFiberClassComponent.js中:

对于函数组件的执行,是在react-reconciler/src/ReactFiberHooks.js中

从中,找到了执行类组件和函数组件的函数。那么为了搞清楚 React 底层是如何处理组件的,首先来看一下类和函数组件是什么时候被实例化和执行的?

在 React 调和渲染 fiber 节点的时候,如果发现 fiber tag 是 ClassComponent = 1,则按照类组件逻辑处理,如果是 FunctionComponent = 0 则按照函数组件逻辑处理。当然 React 也提供了一些内置的组件,比如说 Suspense 、Profiler 等。

什么是组件化开发

  • 组件化是一种分而治之的思想
  • 组件特点:可复用,独立,可组合
  • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
  • 任何的应用都会被抽象成一颗组件树

React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:

  • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
  • 函数组件又叫做无状态组件   函数组件是不能自己提供数据【前提:基于hooks之前说的】
  • 类组件又叫做有状态组件 类组件可以自己提供数据,组件内部的状态(数据如果发生了改变,内容会自动的更新)数据驱动视图
  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
  • 展示型组件就是我给你一些数据你给我渲染出来即可,无需过多的操作;容器型组件就是一般情况下我们包含的东西是非常多的,比如维护自己的状态、发送网络请求、监听一些全局的事件等等
  • 函数组件、无状态组件、展示型组件主要关注UI的展示
  • 类组件、有状态组件、容器型组件主要关注数据逻辑

使用模块化开发

在开发过程中,一些头部或者底部等等共用的部分需要进行复用,在vue或者是react中可以将这样的复用的部分封装进一个组件中,然后将这些组件组合起来就形成了一个网页。这样可以减少代码量,达到代码的复用性,也方便维护

什么是模块化

在早期的js中,没有模块化的概念,多人协作开发,可能会有变量名冲突问题,可以使用插件达到模块化效果,发展到es6时,出现了js自带的模块化export和import, 在node中就是requiremodule

注意:

  1. 定义react组件时,建议首字母大写
  1. 使用组件时,首字母也要大写,并且用驼峰,不要用横杠

React创建组件的两种方式

类与继承

class 基本语法

  • 在 ES6 之前通过构造函数创建对象
  • 在 ES6 中新增了一个关键字 class, 类 和构造函数类似,用于创建对象
    • 类与对象的区别
    • 类:指的是一类的事物,是个概念,比如车 手机 水杯等
    • 对象:一个具体的事物,有具体的特征和行为,比如一个手机,我的手机等, 类可以创建出来对象。
  • 类创建对象的基本语法
    • 基本语法class 类名{}
    • 构造函数constructor的用法,创建对象
    • 在类中提供方法,直接提供即可
    • 在类中不需要使用,分隔

extends 实现继承

  • extends 基本使用
  • 类可以使用它继承的类中所有的成员(属性和方法)
  • 类中可以提供自己的属性和方法
  • 注意:如果想要给类中新增属性,必须先调用 super 方法

类组件

类组件:使用ES6的class语法创建组件

  • 约定1:类组件的名称必须是大写字母开头
  • 约定2:类组件应该继承React.Component父类,从而可以使用父类中提供的方法或者属性
  • 约定3:类组件必须提供render方法
  • 约定4:render方法必须有返回值,表示该组件的结构

基本使用

在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义

使用class定义一个组件:

  • constructor是可选的,我们通常在constructor中初始化一些数据
  • this.state中维护的就是我们组件内部的数据
  • render() 方法是 class 组件中唯一必须实现的方法

定义组件:

import React from 'react'
class Hello extends React.Component {render() {return <div>这是一个类组件</div>}
}// 或者
import React, { Component } from 'react'
class Hello extends Component {render() {return <div>这是一个类组件</div>}
}

使用组件:

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

当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

React 元素:

  • 通常通过 JSX 创建的都叫做react元素
  • 例如,
  • 无论是

数组或 fragments:使得 render 方法可以返回多个元素

Portals:可以渲染子节点到不同的 DOM 子树中

字符串或数值类型:它们在 DOM 中会被渲染为文本节点

布尔类型或 null:什么都不渲染

组件化

思考:项目中的组件多了之后,该如何组织这些组件呢?

  • 选择一:将所有组件放在同一个JS文件中
  • 选择二:将每个组件放到单独的JS文件中--推荐
  • 组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中

components/Password.jsx

import React, { Component } from 'react';class Password extends Component {render() {return (<>密码:<input placeholder="请输入密码" /></>)}
}export default Password;
  • 类组件需要继承自Component
  • Component:是类组件的父类,所有的类组件都需要继承自它才能进行开发。
  • render:渲染DOM元素的方法,必须return返回一个DOM元素

App.jsx中引入使用

import Account from "./components/Account";
import Password from "./components/Password";function App() {return (<div><Account /><br /><Password></Password></div>);
}export default App;

注意:react中两种空标签有区别

<></>:不能在标签上添加属性

<React.Fragment></React.Fragment>:可以在标签上添加属性

有状态/无状态组件

  • 函数组件又叫做无状态组件   函数组件是不能自己提供数据【前提:基于hooks之前说的】
  • 类组件又叫做有状态组件 类组件可以自己提供数据,组件内部的状态(数据如果发生了改变,内容会自动的更新)数据驱动视图
  • 状态(state)即组件的私有数据,当组件的状态发生了改变,页面结构也就发生了改变。
  • 函数组件是没有状态的,只负责页面的展示(静态,不会发生变化)性能比较高
  • 类组件有自己的状态,负责更新UI,只要类组件的数据发生了改变,UI就会发生更新。
  • 在复杂的项目中,一般都是由函数组件和类组件共同配合来完成的。【增加了使用者的负担,所以后来有了hooks】

比如计数器案例,点击按钮让数值+1, 0和1就是不同时刻的状态,当状态从0变成1之后,UI也要跟着发生变化。React想要实现这种功能,就需要使用有状态组件来完成。

 

类组件的状态

  • 状态state即数据,是组件内部的私有数据,只有在组件内部可以使用
  • state的值是一个对象,表示一个组件中可以有多个数据
  • state的基本使用

class Hello extends React.Component {constructor() {super()// 组件通过state提供数据this.state = {msg: 'hello react'}}render() {return <div>state中的数据--{this.state.msg}</div>}
}

  • 简洁的语法
class Hello extends React.Component {state = {msg: 'hello react'}render() {return <div>state中的数据--{this.state.msg}</div>}
}

深入剖析

在 class 组件中,除了继承 React.Component ,底层还加入了 updater 对象,组件中调用的 setState 和 forceUpdate 本质上是调用了 updater 对象上的 enqueueSetState 和 enqueueForceUpdate 方法。

那么,React 底层是如何定义类组件的呢?

react/src/ReactBaseClasses.js

如上可以看出 Component 底层 React 的处理逻辑是,类组件执行构造函数过程中会在实例上绑定 props 和 context ,初始化置空 refs 属性,原型链上绑定setState、forceUpdate 方法。对于 updater,React 在实例化类组件之后会单独绑定 update 对象。

如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢?

// 假设我们在 constructor 中这么写
constructor() {super()console.log(this.props) // 打印 undefiend 为什么?
}

答案很简单,刚才的 Component 源码已经说得明明白白了,绑定 props 是在父类 Component 构造函数中,执行 super 等于执行 Component 函数,此时 props 没有作为第一个参数传给 super() ,在 Component 中就会找不到 props 参数,从而变成 undefined ,在接下来 constructor 代码中打印 props 为 undefined 。

// 解决问题
constructor() {super(props)console.log(this.props)
}

为了更好地使用 React 类组件,我们首先看一下类组件各个部分的功能:

上述绑定了两个 handleClick ,那么点击 div 之后会打印什么呢?

  • 结果是 111 。因为在 class 类内部,箭头函数是直接绑定在实例对象上的,而第二个 handleClick 是绑定在 prototype 原型链上的,它们的优先级是:实例对象上方法属性 > 原型链对象上方法属性。

函数组件

函数组件:使用JS的函数或者箭头函数创建的组件

  • 通过 function 来进行定义的
  • 为了区分和普通标签的差异,函数组件的名称必须大写字母开头
  • 函数组件必须有返回值,表示该组件的结构
  • 如果返回值为null,表示不渲染任何内容

基本使用

使用函数创建组件

function Hello () {return (<div>这是我的函数组件</div>)
}

使用箭头函数创建组件

const Hello = () => <div>这是一个函数组件</div>

使用组件

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

特点(基于hooks之前):

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数
  • this关键字不能指向组件实例(因为没有组件实例)(不存在this)
  • 没有内部状态(state)

组件化

components/Account.jsx

import Account from "./components/Account";function App() {return (<div><Account /><br />密码:<input placeholder="请输入密码" /></div>);
}export default App;

App.jsx中使用

import Account from "./components/Account";function App() {return (<div><Account /><br />密码:<input placeholder="请输入密码" /></div>);
}export default App;

一个函数组件里导出多个函数:

components/Head.jsx

export function Demo(){return (<div>123</div>)
}export function Demo1() {return (<div>789</div>)
}

函数组件与类组件的区别

对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。

为了能让函数组件可以保存一些状态,执行一些副作用钩子,React Hooks 应运而生,它可以帮助记录 React 中组件的状态,处理一些额外的副作用。

编写形式

两者最明显的区别在于编写形式的不同,同一种功能的实现可以分别对应类组件和函数组件的编写形式

状态管理

在 hooks 出来之前,函数组件就是无状态组件,不能保管组件的状态,不像类组件中调用 setState

如果想要管理 state 状态,可以使用 useState,如下:

在使用 hooks 情况下,一般如果函数组件调用 state,则需要创建一个类组件或者 state 提升到你的父组件中,然后通过 props对象传递到子组件

生命周期

在函数组件中,并不存在生命周期,这是因为这些生命周期钩子都来自于继承的 React.Component 所以,如果用到生命周期,就只能使用类组件

但是函数组件使用 useEffect 也能够完成替代生命周期的作用,这里给出一个简单的例子:

上述简单的例子对应类组件中的 componentDidMount 生命周期

如果在 useEffect 回调函数中 return 一个函数,则 return 函数会在组件卸载的时候执行,正如 componentwillUnmount

调用方式

如果是一个函数组件,调用则是执行函数即可:

如果是一个类组件,则需要将组件进行实例化,然后调用实例对象的 render 方法:

获取渲染的值

首先给出一个示例

函数组件对应如下:

类组件对应如下:

两者看起来实现功能是一致的,但是在类组件中,输出 this.props.user , Props 在 React 中是不可变的所以它永远不会改变,但是 this 总是可变的,以便您可以在 render 和生命周期函数中读取新版本

因此,如果我们的组件在请求运行时更新。this.props 将会改变。showMessage 方法从“最新”的 props 中读取 user

而函数组件,本身就不存在 this,props 并不发生改变,因此同样是点击,alert 的内容仍旧是之前的内容

两种组件都有各自的优缺点:

  • 函数组件语法更短、更简单,这使得它更容易开发、理解和测试
  • 而类组件也会因大量使用 this 而让人感到困惑

组件中图片的使用

在 html 中可以按照如下方式使用图片:

<img src="./images/xx.png" />

但是在 react 中这样做是无法正常渲染的,因为在打包后图片的地址发生了改变,无法正确找到路径

require引入图片

<img src={require('./assets/images/logo192.png')} />

如果是以前的react版本,需要这样引入图片:

<img src={require('./assets/images/logo192.png').default} />

使用import方式引入图片

import Logo from './assets/images/logo192.png'<img src={Logo} />

在行内样式中使用图片

也要使用上述两种方式引入图片

<div style={{ width: '100px', height: '100px', backgroundImage: `url(${require('./assets/images/logo192.png')})` }}></div>

React性能优化SCU

react渲染流程:

 

更新机制

react更新流程:

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树

React需要基于这两棵不同的树之间的差别来判断如何有效的更新UI:

如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^2),其中 n 是树中元素的数量

参考地址:https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf

如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围

这个开销太过昂贵了,React的更新性能会变得非常低效

于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

  • 同层节点之间相互比较,不会垮节点比较
  • 不同类型的节点,产生不同的树结构
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定

keys的优化

我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:

◼ 方式一:在最后位置插入数据

 这种情况,有无key意义并不大

◼ 方式二:在前面插入数据

 这种做法,在没有key的情况下,所有的li都需要进行修改;

◼ 当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

 在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;

 将key为333的元素插入到最前面的位置即可;

◼ key的注意事项:

 key应该是唯一的;

 key不要使用随机数(随机数在下一次render时,会重新生成一个数字);

 使用index作为key,对性能是没有优化的;

render函数被调用

我们使用之前的一个嵌套案例:

  • 在App中,我们增加了一个计数器的代码
  • 当点击+1时,会重新调用App的render函数
  • 而当App的render函数被调用时,所有的子组件的render函数都会被重新调用

那么,我们可以思考一下,在以后的开发中,我们只要是修改了,App中的数据,但是其里面的所有的组件(包括子组件)都需要重新render,进行diff算法,这样一来其性能必然是很低的:

  • 事实上,很多的组件没有必须要重新调用render函数进行渲染
  • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法

如何来控制render方法是否被调用呢

  • 通过shouldComponentUpdate方法即可,简称SCU

shouldComponentUpdate

该方法有两个参数:

  • 参数一:nextProps 修改之后,最新的props属性
  • 参数二:nextState 修改之后,最新的state属性

该方法返回值是一个boolean类型:

  • 返回值为true,那么就需要调用render方法;
  • 返回值为false,那么久不需要调用render方法;
  • 默认返回的是true,也就是只要state发生改变,就会调用render方法;

比如我们在App中增加一个message属性:

  • jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染
  • 但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了;

在 类组件 中使用

import React, { Component } from 'react'export default class demoClassComponent extends Component {constructor(props) {super(props)this.state = {count: 0,}}shouldComponentUpdate(nextProps, nextState) {// 自定义逻辑判断是否重新渲染组件if (nextState.count === this.state.count) {return false // 不重新渲染}return true // 重新渲染}handleClick = () => {this.setState((prevState) => ({count: prevState.count + 1,}))}render() {const { count } = this.statereturn (<div><button onClick={this.handleClick}>点击增加</button><p>Count: {count}</p></div>)}
}

在 函数组件 中使用

在函数组件中比较特殊,我们可以使用 React 的 memo 函数来实现类似于 shouldComponentUpdate 的功能。memo 是一个高阶组件(Higher-Order Component),它接收一个组件作为参数,并返回一个新的优化后的组件。

import React, { memo } from 'react'
// import PropTypes from 'prop-types'function demoFuncClassComponent(props) {return (<div><p>{props.text}</p></div>);
}// demoFuncClassComponent.propTypes = {}export default memo(demoFuncClassComponent)
  • 在上面的示例中,使用 memo 包装了 demoFuncClassComponent 组件,使其成为一个优化后的组件。只有当传入 demoFuncClassComponent 的属性 text 发生变化时,才会重新渲染组件
  • 需要注意的是,memo 只进行浅层比较,即只检查属性的值是否相等。如果属性是一个对象或数组,只有当引用发生变化时,才会重新渲染组件。如果需要进行深层比较,可以考虑使用其他方式,例如使用 useMemo 钩子。
  • 对于 Hooks  useMemo 的相关介绍将在后面学react 的 Hooks 函数时学到

PureComponent

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量

  • 我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
  • props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;

事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?

将class继承自PureComponent

import React, { PureComponent } from 'react'export default class demoClassComponent extends PureComponent {constructor(props) {super(props)this.state = {count: 0,}}handleClick = () => {this.setState((prevState) => ({count: prevState.count + 1,}))}render() {const { count } = this.statereturn (<div><button onClick={this.handleClick}>点击增加</button><p>Count: {count}</p></div>)}
}

注意:

只做了浅层比较,也就是只比较第一层,只针对于类组件

但是如果是函数组件呢?这里就需要用到react的高阶组件 memo

import {memo} from 'react'
const funHello = memo(function (props) {return (<div><h3>函数组件</h3></div>)
})
export default funHello

注意:

const newBook = {name: 'zz', price: 20}// 1. 直接修改原有的state,即重新设置一遍,在PureComponent里是不能引入重新渲染的
this.state.books.push(newBook)
this.setState({books: this.state.books})// 2. 赋值一份books,在新的books中修改,设置新的books
const books = [...this.state.books]
books.push(newBook)
this.setState({books: books})// 
const books = [...this.state.books]
newBook.price += 5;
books.push(newBook)
this.setState({books: books})

组件的生命周期

前言

在讲 React 生命周期之前,有必要先来简单聊聊 React 两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历 React fiber 树,目的就是发现不同( diff ),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行 render 函数。在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。

如果在一次调和的过程中,发现了一个fiber tag = 1类组件的情况,就会按照类组件的逻辑来处理。对于类组件的处理逻辑,首先判断类组件是否已经被创建过,首先来看看源码里怎么写的。

react-reconciler/src/ReactFiberBeginWork.js

几个重要的概念:

  • instance 类组件对应实例。
  • workInProgress 树,当前正在调和的 fiber 树 ,一次更新中,React 会自上而下深度遍历子代 fiber ,如果遍历到一个 fiber ,会把当前 fiber 指向 workInProgress。
  • current 树,在初始化更新中,current = null ,在第一次 fiber 调和之后,会将 workInProgress 树赋值给 current 树。React 来用workInProgress 和 current 来确保一次更新中,快速构建,并且状态不丢失。
  • Component 就是项目中的 class 组件。
  • nextProps 作为组件在一次更新中新的 props 。
  • renderExpirationTime 作为下一次渲染的过期时间。

上面这个函数流程我已经标的很清楚了,同学们在学习React的过程中,重要的属性一定要拿小本本记下来,比如说类组件完成渲染挂载之后, React 用什么记录组件对应的 fiber 对象和类组件实例之间的关系。只有搞清楚这些,才能慢慢深入学习 React 。

在组件实例上可以通过_reactInternals属性来访问组件对应的 fiber 对象。在 fiber 对象上,可以通过stateNode来访问当前 fiber 对应的组件实例。两者的关系如下图所示。

这里主要针对于类组件,函数组件没有生命周期,也就没有生命周期函数,但是可以用相关hooks(主要是:useEffect和useLayoutEffect)去进行模拟。

概述

  • 意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
  • 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
  • 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
  • 只有 类组件 才有生命周期。

react生命周期主要分为三个阶段:

  1. 组件的挂载阶段:初始化组件数据,以及渲染元素
  1. 组件的更新阶段:这个节点是最长的阶段,只要props或者state数据被修改后就会进入这个阶段
    1. 组件的卸载阶段:当组件被卸载到就会进入这个阶段,可以清理定时器,订阅发布,性能优化等等。

生命周期的整体说明

  • 每个阶段的执行时机
  • 每个阶段钩子函数的执行顺序
  • 每个阶段钩子函数的作用
  • React lifecycle methods diagram

React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:

  • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
  • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
  • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调

我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)

挂载阶段

执行时机:组件创建时(页面加载时)---组件第一次在DOM树中被渲染的过程

执行顺序:

钩子 函数

触发时机

作用

constructor

创建组件时,最先执行【组件的构造器,可以在这里进行数据的初始化,比如state数据的初始化】

1. 初始化state  2. 创建Ref等

render

每次组件渲染都会触发【render函数将DOM元素进行渲染,将数据等等挂载到DOM元素上】

渲染UI(注意: 不能调用setState()

componentDidMount

组件挂载(完成DOM渲染)后【元素挂载完成后执行的生命周期函数】

1. 发送网络请求   2.DOM操作

componentDidMount 可以做的事情有:

  • 发送请求获取后端数据
  • 获取DOM元素
  • 设置定时器,延时器等等
  • 绑定全局事件等等

执行顺序:constructor->render->componentDidMount

import React, { PureComponent } from 'react'export default class demoClassComponent extends PureComponent {constructor() {super()this.state = {}console.log('hello world constructor')}render() {console.log('hello world render')return <div>hello world</div>}componentDidMount() {console.log('hello world componentDidMount')}
}

更新阶段

组件状态发生变化,重新更新渲染的过程

  • 执行时机:1. setState()   2. forceUpdate()    3. 组件接收到新的props
  • 说明:以上三者任意一种变化,组件就会重新渲染
  • 这个节点是最长的阶段,只要props或者state数据被修改后就会进入这个阶段
  • 执行顺序

钩子函数

触发时机

作用

render

每次组件渲染都会触发

渲染UI(与 挂载阶段 是同一个render)

componentDidUpdate

组件更新(完成DOM渲染)后

DOM操作,可以获取到更新后的DOM内容,不要调用setState

shouldComponentUpdate

在props和state数据更新后,会进入到这个生命周期函数,需要返回一个布尔值,为true就会更新渲染组件,为false就不会更新渲染组件

shouldComponentUpdate:

在props和state数据更新后,会进入到这个生命周期函数,需要返回一个布尔值,为true就会更新渲染组件,为false就不会更新渲染组件。

shouldComponentUpdate() {return true; // 要去更新渲染组件
}

根据业务逻辑判断是否需要更新渲染组件,达到性能优化的目的

shouldComponentUpdate(nextProps, nextState) {console.log('nextProps', nextProps);console.log('nextState', nextState);console.log('state', this.state);if (nextState.title === this.state.title) {return false;} else {return true;}
}

componentDidUpdate:

在数据更新完毕,DOM元素更新渲染完毕后执行的生命周期函数

componentDidUpdate(prevProps, prevState) {console.log(prevProps, prevState);console.log('=========componentDidUpdate');
}

在这里,你可以对比新旧数据达到监听数据的效果。

执行顺序: [shouldComponentUpdate->]render->componentDidUpdate

import React, { PureComponent } from 'react'export default class demoClassComponent extends PureComponent {constructor() {super()this.state = {message: 'hello world',}console.log('render')}changeMessage() {this.setState({message: '你好,李银河',})}render() {const { message } = this.statereturn (<div><p>{message}</p><button onClick={() => this.changeMessage()}>更改</button></div>)}// 组件DOM被更新完成componentDidUpdate() {console.log('componentDidUpdate')}
}

卸载阶段

组件从DOM树中被移除的过程

  • 执行时机:组件从页面中消失

钩子函数

触发时机

作用

componentWillUnmount

组件卸载(从页面中消失)

执行清理工作(比如:清理定时器等)

componentWillUnmount

组件卸载会触发的生命周期,可以清理定时器,订阅发布,性能优化等等。

componentWillUnmount() {console.log('======componentWillUnmount');
}

在componentDidMount生命周期中设置setInterval定时器,组件卸载后需要将这个定时器清理掉,否则会一直执行,导致浪费性能。

import React, { Component } from 'react'export default class Life extends Component {timer = null;componentDidMount() {console.log('=======componentDidMount');this.timer = setInterval(() => {console.log(1);}, 1000);}componentWillUnmount() {console.log('======componentWillUnmount');clearInterval(this.timer);}render() {return (<div></div>)}
}

生命周期总结

Constructor:

  • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
  • constructor中通常只做两件事情:
    • 通过给 this.state 赋值对象来初始化内部的state
    • 为事件绑定实例(this)

componentDidMount:

  • componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用
  • componentDidMount中通常进行哪里操作呢?
    • 依赖于DOM的操作可以在这里进行
    • 在此处发送网络请求就最好的地方;(官方建议)
    • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)

componentDidUpdate:

  • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
  • 当组件更新后,可以在此处对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。

componentWillUnmount:

  • componentWillUnmount() 会在组件卸载及销毁之前直接调用
  • 在此方法中执行必要的清理操作;
  • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等

补充-不常用生命周期函数

getDerivedStateFromProps:

  • state 的值在任何时候都依赖于 props时使用;
  • 该方法返回一个对象来更新state;

getSnapshotBeforeUpdate:

  • 在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置)

shouldComponentUpdate:

  • 该生命周期函数很常用
  • 接收三个参数:prevProps、prevState、snapshot

更详细的生命周期相关的内容,可以参考官网:React.Component – React

表单处理

我们在开发过程中,经常需要操作表单元素,比如获取表单的值或者是设置表单的值。

react中处理表单元素有两种方式:

  • 受控组件
  • 非受控组件(DOM操作)

受控组件

基本概念

  • HTML中表单元素是可输入的,即表单用户并维护着自己的可变状态(value)。
  • 但是在react中,可变状态通常是保存在state中的,并且要求状态只能通过setState进行修改
  • React中将state中的数据与表单元素的value值绑定到了一起,由state的值来控制表单元素的值
  • 受控组件:value值受到了react控制的表单元素

 

受控组件使用步骤

在受控组件中,我们要获取到用户录入信息:

  1. 在state中生命数据进行保存
  1. 通过value将数据绑定到DOM元素上
  1. 在DOM元素上绑定onChange事件,通过这个事件可以获取到事件信息,以此来获取用户录入信息e.target.value
  1. 通过setState将值保存到state数据中
  1. 在state中添加一个状态,作为表单元素的value值(控制表单元素的值)
  1. 给表单元素添加change事件,设置state的值为表单元素的值(控制值的变化)
import React, { PureComponent } from 'react'export default class demoClassComponent extends PureComponent {constructor(props) {super(props)this.state = {msg: 'admin', // 表单元素的value值}}handleChange = (e) => {this.setState({msg: e.target.value,})}render() {const { msg } = this.statereturn (<div>账号:<inputtype="text"placeholder="请输入账号"value={this.state.msg}onChange={this.handleChange}/></div>)}
}

常见的受控组件

  • 文本框、文本域、下拉框(操作value属性)
  • 复选框(操作checked属性)
class App extends React.Component {state = {usernmae: '',desc: '',city: "2",isSingle: true}handleName = e => {this.setState({name: e.target.value})}handleDesc = e => {this.setState({desc: e.target.value})}handleCity = e => {this.setState({city: e.target.value})}handleSingle = e => {this.setState({isSingle: e.target.checked})}render() {return (<div>姓名:<input type="text" value={this.state.username} onChange={this.handleName}/><br/>描述:<textarea value={this.state.desc} onChange={this.handleDesc}></textarea><br/>城市:<select value={this.state.city} onChange={this.handleCity}><option value="1">北京</option><option value="2">上海</option><option value="3">广州</option><option value="4">深圳</option></select><br/>是否单身:<input type="checkbox" checked={this.state.isSingle} onChange={this.handleSingle}/></div>)}
}

多表单元素的优化

问题:每个表单元素都需要一个单独的事件处理程序,处理太繁琐

优化:使用一个事件处理程序处理多个表单元素

步骤

  • 给表单元素添加name属性,名称与state属性名相同
  • 根据表单元素类型获取对应的值
  • 在事件处理程序中通过[name]修改对应的state

示例一
import React, { PureComponent } from 'react'export default class demoClassComponent extends PureComponent {state = {username: '',desc: '',city: '2',isSingle: true,}handleChange = (e) => {let { name, type, value, checked } = e.targetconsole.log(name, type, value, checked)value = type === 'checkbox' ? checked : valueconsole.log(name, value)this.setState({[name]: value,})}render() {return (<div>姓名:<inputtype="text"name="username"value={this.state.username}onChange={this.handleChange}/><br />描述:<textareaname="desc"value={this.state.desc}onChange={this.handleChange}></textarea><br />城市:<selectname="city"value={this.state.city}onChange={this.handleChange}><option value="1">北京</option><option value="2">上海</option><option value="3">广州</option><option value="4">深圳</option></select><br /><label htmlFor="isSingle">是否单身:<inputid="isSingle"type="checkbox"name="isSingle"checked={this.state.isSingle}onChange={this.handleChange}/></label></div>)}
}

示例二
import React, { Component } from "react";class classHello extends Component {constructor(){super()this.state = {username: '',password: ''}}changeInputChange = (e) => {this.setState({[e.target.name]: e.target.value})console.log(this.state.username);console.log(this.state.password);}render() {let { username, password } = this.statereturn (<div><p>用户名:<input type="text" name="username" value={username} onChange={e => this.changeInputChange(e)} /></p><p>密码:<input type="password" name="password" value={password} onChange = {e => this.changeInputChange(e)} /></p></div>);}
}export default classHello;

示例三
import React, { Component } from "react";class classHello extends Component {constructor() {super()this.state = {username: "",password: "",isAgree: false,hobbies: [{ value: "sing", text: "唱", isChecked: false },{ value: "dance", text: "跳", isChecked: false },{ value: "rap", text: "rap", isChecked: false }],// fruit: ["orange"] // 多选fruit: "orange"}}handleSubmitClick(event) {// 1.阻止默认的行为event.preventDefault()// 2.获取到所有的表单数据, 对数据进行操作console.log("获取所有的输入内容")console.log(this.state.username, this.state.password)const hobbies = this.state.hobbies.filter(item => item.isChecked).map(item => item.value)console.log("获取爱好: ", hobbies)// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)}handleInputChange(event) {this.setState({[event.target.name]: event.target.value})}handleAgreeChange(event) {this.setState({ isAgree: event.target.checked })}handleHobbiesChange(event, index) {const hobbies = [...this.state.hobbies]hobbies[index].isChecked = event.target.checkedthis.setState({ hobbies })}handleFruitChange(event) {// 多选// const options = Array.from(event.target.selectedOptions)// const values = options.map(item => item.value)// this.setState({ fruit: values })// 额外补充: Array.from(可迭代对象)// Array.from(arguments)const values2 = Array.from(event.target.selectedOptions, item => item.value)console.log(values2)}render() {const { username, password, isAgree, hobbies, fruit } = this.statereturn (<div><form onSubmit={e => this.handleSubmitClick(e)}>{/* 1.用户名和密码 */}<div><label htmlFor="username">用户: <input id='username' type="text" name='username' value={username} onChange={e => this.handleInputChange(e)}/></label><label htmlFor="password">密码: <input id='password' type="password" name='password' value={password} onChange={e => this.handleInputChange(e)}/></label></div>{/* 2.checkbox单选 */}<label htmlFor="agree"><input id='agree' type="checkbox" checked={isAgree} onChange={e => this.handleAgreeChange(e)}/>同意协议</label>{/* 3.checkbox多选 */}<div>您的爱好:{hobbies.map((item, index) => {return (<label htmlFor={item.value} key={item.value}><input type="checkbox"id={item.value} checked={item.isChecked}onChange={e => this.handleHobbiesChange(e, index)}/><span>{item.text}</span></label>)})}</div>{/* 4.select */}{/**<select value={fruit} onChange={e => this.handleFruitChange(e)} multiple></select> 多选 */}<select value={fruit} onChange={e => this.handleFruitChange(e)}><option value="apple">苹果</option><option value="orange">橘子</option><option value="banana">香蕉</option></select><div><button type='submit'>注册</button></div></form></div>)}
}export default classHello;

示例四
import React, { PureComponent } from 'react'export class App extends PureComponent {constructor() {super()this.state = {username: "",password: ""}}handleSubmitClick(event) {// 1.阻止默认的行为event.preventDefault()// 2.获取到所有的表单数据, 对数据进行组件console.log("获取所有的输入内容")console.log(this.state.username, this.state.password)// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)}handleInputChange(event) {this.setState({[event.target.name]: event.target.value})}render() {const { username, password } = this.statereturn (<div><form onSubmit={e => this.handleSubmitClick(e)}>{/* 1.用户名和密码 */}<label htmlFor="username">用户: <input id='username' type="text" name='username' value={username} onChange={e => this.handleInputChange(e)}/></label><label htmlFor="password">密码: <input id='password' type="password" name='password' value={password} onChange={e => this.handleInputChange(e)}/></label><button type='submit'>注册</button></form></div>)}
}export default App

非受控组件-ref

非受控组件借助于ref,使用原生DOM的方式来获取表单元素的值

基本概念

  • 如果react中的组件全是受控组件,并不合理,因为有时候我们也需要去操作DOM元素,react提供了如何去操作DOM元素的方式。

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:

  • 管理焦点,文本选择或媒体播放
  • 触发强制动画
  • 集成第三方 DOM 库
  • 我们可以通过refs获取DOM

创建 ref 的形式有三种:

非受控组件基本使用

不推荐!!!----相当于直接操作DOM

import React, { createRef, PureComponent } from 'react'export class App extends PureComponent {constructor() {super()this.state = {intro: "哈哈哈"}this.introRef = createRef()}componentDidMount() {this.introRef.current.addEventListener}handleSubmitClick(event) {// 1.阻止默认的行为event.preventDefault()// 2.获取到所有的表单数据, 对数据进行组件console.log("获取结果:", this.introRef.current.value)// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)}render() {const { intro } = this.statereturn (<div><form onSubmit={e => this.handleSubmitClick(e)}>{/* 5.非受控组件 */}<input type="text" defaultValue={intro} ref={this.introRef} /><div><button type='submit'>注册</button></div></form></div>)}
}export default App

获取DOM常见的几种方法

传入字符串

给元素绑定ref字符串(不推荐使用)--使用时通过 this.refs.传入的字符串格式获取对应的元素;

<input ref="inputRef" />this.refs.inputRef

传入一个函数

该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存

使用时,直接拿到之前保存的元素对象即可

关键代码:

<input ref={(el) => this.accountRef = el} placeholder='请输入账号' />

全代码:

import React, { Component } from 'react'export default class LoginForm extends Component {accountRef = null;passwordRef = null;login = () => {let accountValue = this.accountRef.value;console.log(accountValue);let passwordValue = this.passwordRef.value;console.log(passwordValue);}render() {return (<div>账号:<input ref={(el) => this.accountRef = el} placeholder='请输入账号' /><br />密码:<input ref={(el) => this.passwordRef = el} placeholder='请输入密码' /><br /><button onClick={this.login}>登录</button></div>)}
}

传入一个对象

通过react提供的createRef函数去绑定ref--推荐

使用时获取到创建的对象其中有一个current属性就是对应的元素

关键代码:

constructor(props) {super(props);this.accountRef = React.createRef();this.passwordRef = React.createRef();
}<input ref={this.accountRef} placeholder='请输入账号' />

通过createRef拿到的数据保存在了current中,通过current才能获取到真实的DOM元素
全代码:

import React, { Component } from 'react'export default class LoginForm extends Component {constructor(props) {super(props);this.accountRef = React.createRef();this.passwordRef = React.createRef();}login = () => {console.log(this.accountRef.current.value);console.log(this.passwordRef.current.value);}render() {return (<div>账号:<input ref={this.accountRef} placeholder='请输入账号' /><br />密码:<input ref={this.passwordRef} placeholder='请输入密码' /><br /><button onClick={this.login}>登录</button></div>)}
}

传入hook

通过 uSeRef 创建一个 ref,整体使用方式与 React.createRef一致

上述三种情况都是 ref 属性用于原生 HTML 元素上,如果 ref 设置的组件为一个类组件的时候, ref对象接收到的是组件的挂载实例

ref的类型

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例

函数式组件是没有实例的,所以无法通过ref获取他们的实例:

  • 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
  • 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref
import React, {forwardRef} from 'react'const funHello =  forwardRef(function (props,ref) {return (<div><h3 ref={ref}>函数组件</h3></div>)}
)
export default funHello

父组件:

import React, { Component, createRef } from 'react'
import FunHello from './components/funHello';
export default class App extends Component {constructor () {super()this.funRef = createRef()}componentDidMount(){console.log(this.funRef.current);}render() {return (<div><FunHello ref={this.funRef} /></div>)}
}

应用场景

在某些情况下,我们会通过使用 refs 来更新组件,但这种方式并不推荐,更多情况我们是通过 prop s与 state 的方式进行去重新渲染子元素

过多使用 refs,会使组件的实例或者是 DOM 结构暴露,违反组件封装的原则

例如,避免在 Dialog 组件里暴露 open()和 close()方法,最好传递 isOpen 属性但下面的场景使用 refs 非常有用:

state解析(针对于类组件)

React 项目中 UI 的改变来源于 state 改变,类组件中setState是更新组件,渲染视图的主要方式。

基本用法

setState(obj,callback)

第一个参数:当 obj 为一个对象,则为即将合并的 state ;如果 obj 是一个函数,那么当前组件的 state 和 props 将作为参数,返回值用于合并新的 state。

第二个参数 callback :callback 为一个函数,函数执行上下文中可以获取当前 setState 更新后的最新 state 的值,可以作为依赖 state 变化的副作用函数,可以用来做一些基于 DOM 的操作。

// 第一个参数为function类型
this.setState((state,props) => {return {number: 1}
})// 第一个参数为object类型
this.setState({number:1}, () => {console.log(this.state.number); // 获取最新的 number
})

假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?

  • 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。
  • 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
  • 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。
  • 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。

render 阶段 render 函数执行 -> commit 阶段真实 DOM 替换 -> setState 回调函数执行 callback 。

类组件如何限制 state 更新视图

对于类组件如何限制 state 带来的更新作用的呢?

① pureComponent 可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新。

② shouldComponentUpdate 生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false。

setState原理揭秘

知其然,知其所以然,想要吃透 setState,就需要掌握一些 setState 的底层逻辑。 上一章节讲到对于类组件,类组件初始化过程中绑定了负责更新的Updater对象,对于如果调用 setState 方法,实际上是 React 底层调用 Updater 对象上的 enqueueSetState 方法。

因为要弄明白 state 更新机制,所以接下来要从两个方向分析。

  • 一是揭秘 enqueueSetState 到底做了些什么?
  • 二是 React 底层是如何进行批量更新的?

首先,这里极致精简了一波 enqueueSetState 代码。如下

react-reconciler/src/ReactFiberClassComponent.js

enqueueSetState作用实际很简单,就是创建一个 update ,然后放入当前 fiber 对象的待更新队列中,最后开启调度更新,进入上述讲到的更新流程。

那么问题来了,React 的 batchUpdate 批量更新是什么时候加上去的呢?

这就要提前聊一下事件系统了。正常state 更新、UI 交互,都离不开用户的事件,比如点击事件,表单输入等,React 是采用事件合成的形式,每一个事件都是由 React 事件系统统一调度的,那么 State 批量更新正是和事件系统息息相关的。

react-dom/src/events/DOMLegacyEventPluginSystem.js

重点来了,就是下面这个 batchedEventUpdates 方法。

react-dom/src/events/ReactDOMUpdateBatching.js

如上可以分析出流程,在 React 事件执行之前通过isBatchingEventUpdates=true打开开关,开启事件批量更新,当该事件结束,再通过isBatchingEventUpdates = false;关闭开关,然后在 scheduleUpdateOnFiber 中根据这个开关来确定是否进行批量更新。

举一个例子,如下组件中这么写:

export default class Index extends React.Component{state = { number: 0 }handleClick = () => {this.setState({number: this.state.number+1}, () => {console.log('callback1', this.state.number);})console.log(this.state.number);this.setState({number: this.state.number+1}, () => {console.log('callback2', this.state.number);})console.log(this.state.number);this.setState({number: this.state.number+1}, () => {console.log('callback3', this.state.number);})console.log(this.state.number);}render() {return (<div>{ this.state.number }<button onClick={ this.handleClick }>number++</button></div>)}
}

点击打印:0,0,0,'callback1',1,'callback2',1,'callback3',1

如上代码,在整个 React 上下文执行栈中会变成这样:

那么,为什么异步操作里面的批量更新规则会被打破呢?比如用 promise 或者 setTimeout 在 handleClick 中这么写:

setTimeout(() => {this.setState({number: this.state.number+1}, () => {console.log('callback1', this.state.number);})console.log(this.state.number);this.setState({number: this.state.number+1}, () => {console.log('callback2', this.state.number);})console.log(this.state.number);this.setState({number: this.state.number+1}, () => {console.log('callback3', this.state.number);})console.log(this.state.number);
})

打印:'callback1',1,1,'callback2',2,2,'callback3',3,3

那么在整个 React 上下文执行栈中就会变成如下图这样:

所以批量更新规则被打破

那么,如何在如上异步环境下,继续开启批量更新模式呢?

React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新,可以将上述 setTimeout 里面的内容做如下修改:

import ReactDOM from "react-dom";
const { unstable_batchedUpdates } = ReactDOM;
setTimeout(() => {unstable_batchedUpdates(() => {this.setState({number: this.state.number+1}, () => {console.log('callback1', this.state.number);})console.log(this.state.number);this.setState({number: this.state.number+1}, () => {console.log('callback2', this.state.number);})console.log(this.state.number);this.setState({number: this.state.number+1}, () => {console.log('callback3', this.state.number);})console.log(this.state.number);})
})

打印:0,0,0,'callback1',1,'callback2',1,'callback3',1

在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。原因很简单,所有的数据交互都是在异步环境下,如果没有批量更新处理,一次数据交互多次改变 state 会促使视图多次渲染。

那么如何提升更新优先级呢?

React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。

接下来,将上述handleClick改版如下样子:

首先flushSync this.setState({ number: 3 })设定了一个高优先级的更新,所以 2 和 3 被批量更新到 3 ,所以 3 先被打印。

更新为 4。

最后更新 setTimeout 中的 number = 1。

flushSync补充说明:flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。

综上所述, React 同一级别更新优先级关系是:

flushSync 中的 setState>正常执行上下文中 setState>setTimeout ,Promise 中的 setState。

state属性

state数据是组件的内部数据,一旦更新后会引起组件内部dom的更新和渲染,类似于vue中的data数据

react中函数组件没有内部状态,只有类组件有内部状态,因此state只针对于类组件

定义/使用数据

在react类组件中定义state有两种方式:

  1. 定义数据:

1)在构造器constructor中定义state

import React, { Component } from 'react'export default class State extends Component {constructor(props) {super(props);this.state = {title: '今天星期五,心情是大不同。'}}render() {return (<div>标题:{this.state.title}</div>)}
}

2)直接给属性state定义数据

import React, { Component } from 'react'export default class State extends Component {state = {title: '今天星期五,心情是大不同。'}render() {return (<div>标题:{this.state.title}</div>)}
}

  1. 使用state数据:
render() {return (<div>标题:{this.state.title}</div>)}

修改数据

在react中修改state数据,如果直接通过=赋值,可以修改数据,但是不能引起组件的更新渲染:

this.state.title = '今天不上晚自习';

正确修改state数据,应该使用this.setState()函数去修改:

changeTitle = () => {this.setState({title: '今天不上晚自习'})
}

setState修改状态

  • 组件中的状态是可变的
  • 语法this.setState({要修改的数据})
  • 注意:不要直接修改state中的值,必须通过this.setState()方法进行修改
  • setState的作用
    • 修改state
    • 更新UI
  • 思想:数据驱动视图

class App extends React.Component {state = {count: 1}handleClick() {this.setState({count: this.state.count + 1})}render() {return (<div><p>次数: {this.state.count}</p><button onClick={this.handleClick.bind(this)}>点我+1</button></div>)}
}

  • react中核心理念:状态不可变
    • 不要直接修改react中state的值,而是提供新的值
    • 直接修改react中state的值,组件并不会更新

state = {count: 0,list: []
}
// 直接修改值的操作
this.state.count++
this.state.list.push('a')// 创建新的值的操作
this.setState({count: this.state.count + 1,list: [...this.state.list, 'b']
})

修改对象

this.setState({user: {name: '永恩'}
})

修改state中的对象属性时,要注意将其余的属性都要加上,如果不加会丢失属性

正确的修改方式:

1)将对象扩展

this.setState({user: {...this.state.user,name: '永恩',}
})

2)直接修改state中对象的属性,然后再通过setState更新渲染

this.state.user.name = '永恩';
this.setState({user: this.state.user
})

减轻state

  • 减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等)
  • 注意:不用做渲染的数据不要放在 state 中,比如定时器 id等
  • 对于这种需要在多个方法中用到的数据,应该直接放在 this 中
    • this.xxx = 'bbb'
    • this.xxx
class Hello extends Component {componentDidMount() {// timerId存储到this中,而不是state中this.timerId = setInterval(() => {}, 2000)}componentWillUnmount() {clearInterval(this.timerId)}render() { … }
}

vue中不要把和渲染无关的数据放到data中

setState

为什么要使用 setState

Vue和React数据管理和渲染流程对比:

为什么使用setState:

开发中我们并不能直接通过修改state的值来让界面发生更新:

  • 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化
  • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化
  • 我们必须通过setState来告知React数据已经发生了变化

在组件中并没有实现setState的方法,为什么可以调用呢?

  • 原因很简单,setState方法是从Component中继承过来的

 

执行流程

执行过程

setState异步/同步

在react18版本,任何情况下setState都是异步

要是想在react18中进行同步操作,需要按照如下操作:

import { flushSync } from 'react-dom'change = () => {flushSync(() => {this.setState({count: this.state.count + 1})this.setState({count: this.state.count + 1})console.log(this.state.count)})
}

在react17版本(18版本以前),如果平常使用就是异步的,但是在定时器或者js原生事件中是同步的

add = (num) => {// setTimeout(() => {//     this.setState({//         count: this.state.count + num//     });//     console.log(this.state.count);// }, 0);this.setState({count: this.state.count + num});console.log(this.state.count);
}

为什么setState设计为异步呢?

  • setState设计为异步,可以显著的提升性能;
    • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
    • 最好的办法应该是获取到多个更新,之后进行批量更新
  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步
    • state和props不能保持一致性,会在开发中产生很多的问题

那么如何可以获取到更新后的值呢?(如何获取异步的结果)

方式一:setState的回调

  • setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行
  • 格式如下:setState(partialState, callback)

方式二:在生命周期函数中获取

  • 当然,我们也可以在生命周期函数中获取更新后的值

setState的合并

在react中连续执行setState会合并成一次执行,官方解释是为了做性能优化。

是通过 Object.assign(this.state, newState) 进行合并,即如果后面存在同名的属性,那么后者覆盖前者,再调用 render() 函数进行渲染

// 挂载完成后执行的,类似于vue的mounted
componentDidMount() {this.setState({count: this.state.count + 1});this.setState({count: this.state.count + 1});this.setState({count: this.state.count + 1});
}

加1加三次的解决方案:

1)将值解构出来进行操作

// 挂载完成后执行的,类似于vue的mounted
componentDidMount() {let { count } = this.state;count += 1;count += 1;count += 1;this.setState({count})
}

2)通过setState第一个参数去更新数据

setState第一个参数可以是一个函数,接收一个原来的state数据作为参数,这个函数需要返回一个对象,作为修改后的数据。

好处:

  • 可以在回调函数中编写新的state的逻辑
  • 当前的回调函数会将之前的state和props传递进来
// 挂载完成后执行的,类似于vue的mounted
componentDidMount() {this.setState((prevState) => {return {count: prevState.count + 1}});this.setState((prevState) => {return {count: prevState.count + 1}});this.setState((prevState) => {console.log(this.state.xxx, this.props)return {count: prevState.count + 1}});
}

setState第二个参数

setState第二个参数是一个函数

这个函数执行的时机是在数据更新完毕,DOM元素更新渲染完毕后执行

在这个函数里面可以获取到最新的数据和最新的DOM元素

  • 场景:在状态更新(页面完成重新渲染)后获取对应的结果并立即执行某个操作
  • 语法:setState(updater[, callback])
this.setState((state) => ({}),() => {console.log('这个回调函数会在状态更新后立即执行')}
)

this.setState({count: this.state.count + num
}, () => {console.log(this.state.count);
});

总结

setState设计为异步,可以显著的提升性能:

  • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
  • 最好的办法应该是获取到多个更新,之后进行批量更新
increment(){this.setState((prevState) => {return {count: prevState.count + 1}});this.setState((prevState) => {return {count: prevState.count + 1}});......
}

如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步:

  • state和props不能保持一致性,会在开发中产生很多的问题

综合案例

评论列表案例

列表展示功能

渲染评论列表(列表渲染)

  • 在state中初始化评论列表数据
  • 使用数组的map方法遍历列表数据
  • 给每个li添加key属性

发表评论功能

获取评论信息,评论人和评论内容(受控组件)

  • 使用受控组件的方式获取评论数据

发表评论,更新评论列表(更新状态)

  • 给comments增加一条数据

边界处理

  • 清空内容
  • 判断非空

清空评论功能

  • 给清空评论按钮注册事件
  • 清空评论列表
  • 没有更多评论的处理

源码:

commit.jsx

/*** 1. 导入react和react-dom* 2. 创建 react 元素* 3. 把 react 元素渲染到页面*/import React from 'react';import ReactDom from 'react-dom/client';import { Component } from 'react';import './index.css'/* 主要实现的功能:1. 展示评论功能1.1 通过 state 提供评论列表数据1.2 通过 map 动态渲染2. 清空评论功能3. 发表评论功能4. 删除评论功能5. 没有更多评论的处理
*/
class App extends Component {state = ({list: [{id: 1,name: '张三',content: '宝,我昨天去输液了。你知道是输的什么液吗?是想你的夜'},{id: 2,name: '李四',content: '哈哈,笑死!!!居然还有这种土味情话'},{id: 3,name: '王五',content: '我要定一个小目标,那就是先挣它一个亿!'},{id: 4,name: '赵六',content: '嚯哟,您这小目标可真是够小的,祝早日实现!!!'}],name: '',content: ''})render() {return (<div className="app"><div><input className="user" type="text" placeholder="请输入评论人" value={this.state.name} onChange={this.handleChange} name="name" /><br /><textareaclassName="content"cols="30"rows="10"placeholder="请输入评论内容"value={this.state.content}onChange={this.handleChange}name="content"/><br /><button onClick={this.add}>发表评论</button><button onClick={this.clearAll}>清空评论</button></div>{this.renderList()}</div>)}// 清空评论clearAll = () => {this.setState({list: []})}// 没有更多评论的处理renderList() {if(this.state.list.length === 0) {return (<div className="no-comment">暂无评论</div>);} else {return (<ul>{this.state.list.map(item =><li key={item.id}><h3>评论人:{item.name}</h3><p>评论内容:{item.content}</p><button onClick={this.del.bind(this, item.id)}>删除</button></li>)}</ul>)}}// 删除评论功能del = (id) => {// console.log(id);this.setState({list: this.state.list.filter(item => item.id !== id)})}// 发表评论功能handleChange = (e) => {const { name, value } = e.target;this.setState({[name]: value})}add = () => {const { name, content, list } = this.state;// 当 name 或者 content 没有值if(!name || !content){return alert('信息不完整!');}// 添加评论this.setState({list: [{id: Date.now(), name: name, content: content} ,...list],name: '',content: '' })}
}// 幽灵节点:节点不会渲染任何的内容,跟 vue 里面的 template 标签一样
const element = (<React.Fragment><App></App></React.Fragment>);// 参数1:渲染的 react 元素即虚拟 DOM
// 参数2:需要渲染到哪个容器中
const root = ReactDom.createRoot(document.getElementById('root'));
root.render(element);

index.css

.app {width: 400px;padding: 10px;border: 1px solid #999;
}.user {width: 100%;box-sizing: border-box;margin-bottom: 10px;
}.content {width: 100%;box-sizing: border-box;margin-bottom: 10px;
}.no-comment {text-align: center;margin-top: 30px;
}

todoList 案例

components/todo.jsx

import React, { Component } from "react";export default class Todo extends Component {state = {inputVal: "",todo: [{id: 1,todo: "吃饭",done: false,},{id: 2,todo: "睡觉",done: true,},],liRef: null,};// 渲染函数show = () => {return this.state.todo.map((item, index) => (<React.Fragment key={item.id}><div className="item" style={{ display: "flex" }}><listyle={item.done ? { color: "red", textDecoration: "line-through" } : {}}onClick={() => this.liBtn(index)}>{item.todo}</li><buttononClick={() => this.delBtn(item.id)}style={{marginLeft: "10px",width: "60px",height: "25px",lineHeight: "25px",}}>删除</button></div></React.Fragment>));};// 监听输入框的值变化inputChange = (e) => {let value = e.target.value;this.setState({inputVal: value,});};// 点击 添加 按钮addBtn = () => {if (this.state.inputVal === "") {alert("不能为空");return;}this.state.todo.push({id:this.state.todo.length === 0? 1: this.state.todo[this.state.todo.length - 1].id + 1,todo: this.state.inputVal,done: false,});this.setState({todo: this.state.todo,});this.setState({inputVal: "",});};// 点击 li 进行操作liBtn = (i) => {// 解构let { todo } = this.state;todo[i].done = !todo[i].done;this.setState({todo,});};// 删除delBtn = (i) => {const arr = this.state.todo.filter((v) => v.id !== i);this.setState({todo: arr,});};// 全部完成changDone = (flag) => {if (flag) {this.state.todo.forEach((item) => {item.done = true;});this.setState({todo: this.state.todo,});} else {this.state.todo.forEach((item) => {item.done = false;});this.setState({todo: this.state.todo,});}};// 全部未完成changeAllNoDone = () => {};render(data) {return (<div className="todo-container"><div className="input"><inputplaceholder="请输入需要添加的任务"type="text"onChange={this.inputChange}value={this.state.inputVal}/><button onClick={this.addBtn}>添加</button></div><div className="todo-body"><ul>{this.show()}</ul></div><footer className="todo-footer"><button onClick={() => this.changDone(true)}>全部完成</button><button onClick={() => this.changDone(false)}>全部未完成</button></footer></div>);}
}

App.js

import './App.css';
import React from 'react';import Todo from './components/todo.jsx'
function App() {return (<div className="App"><Todo /></div>);
}export default App;

简易购物车

版本一

import '../assets/css/shop1.css'
import React from "react";
class Food extends React.Component {constructor() {super();this.state = {goods: [{id: 1,img: require("../assets/images/model1.jpg"),big_title: "Java入门到放弃",small_title: "精通Java的是个步骤",price: 998},{id: 2,img: require("../assets/images/model2.jpg"),big_title: "web入门到住院",small_title: "前端性能优化的一个不会",price: 19999},{id: 3,img: require("../assets/images/model3.jpg"),big_title: "python爬虫实战",small_title: "数据的抓取艺术",price: 88888,},{id: 4,img: require("../assets/images/model4.jpg"),big_title: "python爬虫实战",small_title: "数据的抓取艺术",price: 6666},],cart: [],}}table = () => {return (<table><thead><tr><th>编号</th><th>图片</th><th>标题</th><th>价格</th><th>数量</th><th>操作</th></tr></thead><tbody>{this.show()}</tbody></table>)};show = () => {if (this.state.cart.length <= 0) {return <tr><td colspan="6" style={{textAlign:"center"}}>还没有任何商品</td></tr>} else {return this.state.cart.map(v => <tr key={v.id}><td>{v.id}</td><td><img src={v.img} alt="" width="80px" /></td><td>{v.big_title}</td><td>{v.price}</td><td>{v.num}</td><td><button onClick={()=>this.del(v)}>删除</button></td></tr>)}};// 删除del=(item)=>{item.num--if(item.num===0){this.state.cart=this.state.cart.filter(v=>item.id!==v.id)}this.setState({cart:this.state.cart})}// 添加add = (item) => {let { cart} = this.statelet obj=cart.filter(v => v.id === item.id)[0]if (obj) {obj.num++} else {obj = { ...item }obj.num = 1cart.push(obj)}this.setState({cart})}// 小计求和sum(){let sum=0this.state.cart.forEach(v=>{sum+=v.price*v.num})return sum}render() {return (<div className="container"><h2>产品</h2><div className="list">{this.state.goods.map(v => <div key={v.id} className="item"><img src={v.img} alt="" /><p>{v.big_title}</p><p>{v.small_title}</p><button onClick={() => this.add(v)}>加入购物车</button></div>)}</div><h2>购物车</h2><div className='mytable'>{this.table()}</div><p>总价:{this.sum()}</p></div>)}
}
export default Food

版本二

import React, { Component } from 'react'
import './assets/css/shop.css'export default class App extends Component {state = {productList: [{id: 1,img: require('./assets/images/model1.jpg'),title: 'java入门到放弃',desc: '精通java的是个步骤',price: 1000}, {id: 2,img: require('./assets/images/model2.jpg'),title: 'java入门到放弃',desc: '精通java的是个步骤',price: 1100}, {id: 3,img: require('./assets/images/model3.jpg'),title: 'java入门到放弃',desc: '精通java的是个步骤',price: 1200}, {id: 4,img: require('./assets/images/model4.jpg'),title: 'java入门到放弃',desc: '精通java的是个步骤',price: 1300}],// tableList: [{//   id: 1,//   img: require('./assets/images/model1.jpg'),//   title: 'java入门到放弃',//   desc: '精通java的是个步骤',//   price: 1000,//   count: 1// }]tableList: []}// 增加addCart = (index) => {let { tableList, productList } = this.state;// 判断表格中是否有当前这条数据if (tableList.some(item => item.id === productList[index].id)) {let currIndex = tableList.findIndex(item => item.id === productList[index].id);tableList[currIndex].count += 1;// 方式2// let current = tableList.filter(item => item.id === productList[index].id);// current[0].count += 1;} else {// 表格中没有这条数据,那么就push进去tableList.push({...this.state.productList[index],count: 1});}this.setState({tableList})}// 删除del = (index) => {let { tableList } = this.state;if (tableList[index].count > 1) {tableList[index].count -= 1;} else {tableList.splice(index, 1);}this.setState({tableList})}// 小计// 方式1(推荐)get totalPrice() {return this.state.tableList.reduce((sum, next) => {return sum + (next.price * next.count)}, 0)}// 方式2getTotalPrice() {return this.state.tableList.reduce((sum, next) => {return sum + (next.price * next.count)}, 0)}render() {return (<div className='container'><h3>产品</h3><div className='list'>{this.state.productList.map((item, index) => {return (<div className='item' key={item.id}><img src={item.img} /><p>{item.title}</p><p>{item.desc}</p><button onClick={() => this.addCart(index)}>加入购物车</button></div>)})}</div><h3>购物车</h3><div className='mytable'><table><thead><tr><th>编号</th><th>图片</th><th>标题</th><th>价格</th><th>数量</th><th>操作</th></tr></thead><tbody>{this.state.tableList.map((item, index) => {return (<tr key={item.id}><td>{item.id}</td><td><img width={50} src={item.img} /></td><td>{item.title}</td><td>{item.price}</td><td>{item.count}</td><td><button onClick={() => this.del(index)}>删除</button></td></tr>)})}</tbody></table></div><p>总价:{this.getTotalPrice()}元</p></div>)}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/427973.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unsupervised Deep Representation Learning for Real-Time Tracking

摘要 我们的无监督学习的动机是稳健的跟踪器应该在双向跟踪中有效。具体来说&#xff0c;跟踪器能够在连续帧中前向定位目标对象&#xff0c;并回溯到其在第一帧中的初始位置。基于这样的动机&#xff0c;在训练过程中&#xff0c;我们测量前向和后向轨迹之间的一致性&#xf…

95、k8s之rancher可视化

一、ranker 图形化界面 图形化界面进行k8s集群的管理 rancher自带监控----普罗米修斯 [rootmaster01 opt]# docker load -i rancher.tar ##所有节点 [rootmaster01 opt]# docker pull rancher/rancher:v2.5.7 ##主节点[rootmaster01 opt]# vim /etc/docker/daemon.jso…

C++初阶学习——探索STL奥秘——反向迭代器

适配器模式是 STL 中的重要组成部分&#xff0c;除了容器适配器外&#xff0c;还有 选代器适配器&#xff0c;借助 选代器适配器 &#xff0c;可以轻松将各种容器中的普通迭代器转变为反向迭代器&#xff0c;这正是适配器的核心思想 注:库中的反向迭代器在设计时&#xff0c;为…

HashMap线程不安全|Hashtable|ConcurrentHashMap

文章目录 常见集合线程安全性HashMap为什么线程不安全&#xff1f;怎么保证HashMap线程安全 HashtableConcurrentHashMap 引入细粒度锁代码中分析总结 小结 常见集合线程安全性 ArrayList、LinkedList、TreeSet、HashSet、HashMap、TreeMap等都是线程不安全的。 HashTable是线…

【Python报错已解决】To update, run: python.exe -m pip install --upgrade pip

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

【C++篇】~类和对象(中)

类和对象&#xff08;中&#xff09; 1.类的默认成员函数​ 默认成员函数就是用户没有显式实现&#xff0c;编译器会自动生成的成员函数称为默认成员函数。一个类&#xff0c;我们不写的情况下编译器会默认生成以下6个默认成员函数&#xff0c;需要注意的是这6个中最重要的是前…

【LeetCode每日一题】——401.二进制手表

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时间频度】九【代码实现】十【提交结果】 一【题目类别】 回溯 二【题目难度】 简单 三【题目编号】 401.二进制手表 四【题目描述】 二进制手表顶部…

arcgisPro地理配准

1、添加图像 2、在【影像】选项卡中&#xff0c;点击【地理配准】 3、 点击添加控制点 4、选择影像左上角格点&#xff0c;然后右击填入目标点的投影坐标 5、依次输入四个格角点的坐标 6、点击【变换】按钮&#xff0c;选择【一阶多项式&#xff08;仿射&#xff09;】变换 7…

1.Seata 1.5.2 seata-server搭建

一&#xff1a;Seata基本介绍 Seata是一款开源的分布式事务解决方案&#xff0c;致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 详见官网链接&#xff1a;https://seata.apache.org/zh-cn/ 1.历史项目里的使用经验&#xff1a; 之前公司里的oem用户对应的App…

多重指针变量(n重指针变量)实例分析

0 前言 指针之于C语言&#xff0c;就像子弹于枪械。没了子弹的枪械虽然可以用来肉搏&#xff0c;却失去了迅速解决、优雅解决战斗的能力。但上了膛的枪械也非常危险&#xff0c;时刻要注意是否上了保险&#xff0c;使用C语言的指针也是如此&#xff0c;要万分小心&#xff0c;…

【VUE3.0】动手做一套像素风的前端UI组件库---先导篇

系列文章目录 【VUE3.0】动手做一套像素风的前端UI组件库—Button 目录 系列文章目录引言准备素材字体鼠标手势图 创建vue3项目构建项目1. 根据命令行提示选择如下&#xff1a;2. 进入项目根目录下载依赖并启动。3. 设置项目src路径别名&#xff0c;方便后期应用路径。4. 将素…

solana项目counter,测试过程中执行报错记录分享

跟随HackQuest部署counter项目&#xff0c;使用 Solana 官方提供的 playgroud 。这个平台让我们的部署和测试过程变得更加简便高效。 合约代码 lib.rs中复制以下代码 use anchor_lang::prelude::*; use std::ops::DerefMut;declare_id!("CVQCRMyzWNr8MbNhzjbfPu9YVvr97…

Amoco:一款针对二进制源码的安全分析工具

关于Amoco Amoco是一款功能强大的二进制源码静态分析工具&#xff0c;该工具基于Python 3.8开发&#xff0c;可以帮助广大研究人员轻松对二进制程序执行静态符号分析。 工具特性 1、一个通用的指令解码框架&#xff0c;旨在减少实现对新架构的支持所需的时间。例如&#xff0c…

通过springcloud gateway优雅的进行springcloud oauth2认证和权限控制

代码地址 如果对你有帮助请给个start&#xff0c;本项目会持续更新&#xff0c;目标是做一个可用的快速微服务开发平台&#xff0c;成为接私活&#xff0c;毕设的开发神器&#xff0c; 欢迎大神们多提意见和建议 使用的都是spring官方最新的版本&#xff0c;版本如下&#xff1…

F12抓包11:UI自动化 - Recoder(记录器)

课程大纲 使用场景&#xff08;导入和导出&#xff09;: ① 测试的重复性工作&#xff0c;本浏览器录制并进行replay&#xff1b; ② 导入/导出录制脚本&#xff0c;移植后replay&#xff1b; ③ 导出给开发进行replay复现bug&#xff1b; ④ 进行前端性能分析。 1、录制脚…

Virtuoso服务在centos中自动停止的原因分析及解决方案

目录 前言1. 问题背景2. 原因分析2.1 终端关闭导致信号12.2 nohup命令的局限性 3. 解决方案3.1 使用 screen 命令保持会话3.2 使用 tmux 作为替代方案3.3 使用系统服务&#xff08;systemd&#xff09; 4. 其他注意事项4.1 网络配置4.2 日志监控 结语 前言 在使用Virtuoso作为…

mybatisplus映射与数据库表格不一致问题

1.字段映射与属性名不一致 TableField(value"数据库字段名") 2.entity添加了数据库表格不存在的属性 TableField(existfalse) 3.entity对象查询时&#xff0c;有些字段不想要显示在查询结果上 TableField(selectfalse) 4.表名不一致 TableName("数据库表名&…

爬虫--翻页tips

免责声明&#xff1a;本文仅做分享&#xff01; 伪线程 from DrissionPage import ChromiumPage import timepage ChromiumPage() page.get("https://you.ctrip.com/sight/taian746.html") # 初始化 第0页 index_page 0# 翻页点击函数 sleep def page_turn():page…

使用API有效率地管理Dynadot域名,为域名进行隐私保护设置

前言 Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮箱&…

八股文-多线程、并发

八股文-多线程、并发 最近学到了一种方法&#xff0c;可以用于简历项目经验编写以及面试题目的回答 STAR法则&#xff1a;在什么背景下&#xff0c;你需要解决什么问题&#xff0c;你做了啥&#xff0c;得到了什么结果 情境&#xff08;Situation&#xff09;&#xff1a; 描…