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

    state中的数据--{this.state.msg}

    }
    }

  • 简洁的语法

    class Hello extends React.Component {
    state = {
    msg: 'hello react'
    }
    render() {
    return

    state中的数据--{this.state.msg}

    }
    }

深入剖析

在 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.state

    return (
      <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.state

    return (
      <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内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:

  • 比如实现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.state

    return (
      <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.state

     return (
       <div>
         账号:
         <input
           type="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.target
    console.log(name, type, value, checked)
    value = type === 'checkbox' ? checked : value
    console.log(name, value)
    this.setState({
      [name]: value,
    })
  }
  render() {
    return (
      <div>
        姓名:
        <input
          type="text"
          name="username"
          value={this.state.username}
          onChange={this.handleChange}
        />
        <br />
        描述:
        <textarea
          name="desc"
          value={this.state.desc}
          onChange={this.handleChange}
        ></textarea>
        <br />
        城市:
        <select
          name="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">
          是否单身:
          <input
            id="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.state
    return (
      <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.checked
    this.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.state

    return (
      <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.state

    return (
      <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.state

    return (
      <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 (


    函数组件



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


    标题:{this.state.title}

    )
    }

修改数据

在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 (


    次数: {this.state.count}


    <button onClick={this.handleClick.bind(this)}>点我+1</button>

    )
    }
    }

  • 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

      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 />
          <textarea
            className="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" }}>
          <li
            style={
              item.done ? { color: "red", textDecoration: "line-through" } : {}
            }
            onClick={() => this.liBtn(index)}
          >
            {item.todo}
          </li>
          <button
            onClick={() => 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">
          <input
            placeholder="请输入需要添加的任务"
            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.state
        let obj=cart.filter(v => v.id === item.id)[0]
        if (obj) {
            obj.num++
        } else {
            obj = { ...item }
            obj.num = 1
            cart.push(obj)
        }
        this.setState({
            cart
        })
    }

    // 小计求和
    sum(){
        let sum=0
        this.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)
  }

  // 方式2
  getTotalPrice() {
    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>
    )
  }
}
相关推荐
荔枝味-1 小时前
html详细知识
前端·javascript·html
Jason-河山3 小时前
「铭记历史 珍爱和平」勿忘9.18!
前端
国王不在家3 小时前
keyof和infer
前端
猿大撒3 小时前
Spring-data-redis
前端·bootstrap·html
帅过二硕ฅ4 小时前
uniapp点击跳转到对应位置
前端·javascript·uni-app
正小安6 小时前
URL.createObjectURL 与 FileReader:Web 文件处理两大法宝的对比
前端·javascript
赵广陆7 小时前
SprinBoot+Vue宠物寄养系统的设计与实现
前端·vue.js·宠物
A黄俊辉A7 小时前
vue3中把封装svg图标为全局组件
前端·javascript·vue.js
老贾爱编程7 小时前
VUE实现刻度尺进度条
前端·javascript·vue.js
F2E_Zhangmo7 小时前
vue如何做到计算属性传参?
前端·javascript·vue.js