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>
    )
  }
}
相关推荐
栈老师不回家14 分钟前
Element UI 组件库详解【Vue】
前端·vue.js·ui
前端青山22 分钟前
webpack进阶(一)
前端·javascript·webpack·前端框架·node.js
前端与小赵28 分钟前
什么是Sass,有什么特点
前端·rust·sass
栈老师不回家37 分钟前
axios 请求跨域问题
前端·vue.js
前端拾光者1 小时前
前端数据可视化思路及实现案例
前端·数据库·信息可视化
沉默璇年1 小时前
react中Fragment的使用场景
前端·react.js·前端框架
前端熊猫2 小时前
transform学习
前端·学习·html
GISer_Jing2 小时前
React渲染流程与更新diff算法
前端·javascript·react.js
郑祎亦2 小时前
JavaWeb开发:HTML 页面与接口对接
前端·后端·java-ee·html
Au_ust2 小时前
css:感觉稍微高级一点的布局
前端·css