彻底理解React脚手架和组件化

237 阅读13分钟

脚手架

传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架;编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构

认识

对于现在比较流行的三大框架都有属于自己的脚手架:作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好

  • Vue的脚手架:@vue/cli

  • Angular的脚手架:@angular/cli

  • React的脚手架:create-react-app

目前这些脚手架都是使用node编写的,并且都是基于webpack

创建React项目

  • 先安装脚手架依赖node,无论是windows还是Mac OS,都可以通过node官网nodejs.org/en/download… 直接下载

  • create-react-app 项目名称,项目名称不能包含大写字母

  • 创建完后cd 项目,然后npm start运行项目

  • 目录结构如下: image.png

webpack

React脚手架默认是基于Webpack来开发的,但是并没有在目录结构中看到任何webpack相关的内容?原因是React脚手架将webpack相关的配置隐藏起来了(从Vue CLI3开始,也进行了隐藏),

  • 如果希望看到webpack的配置信息,应该怎么来做呢?

  • 可以使用npm run eject命令执行一个package.json文件中的一个脚本:"eject": "react-scripts eject"

  • 这个命令执行是不可逆的,执行过程中会给提示,后面会学习其他修改webpack配置的方法 image.png image.png

  • 删除不需要的文件,将src下的所有文件都删除,将public文件下除favicon.icoindex.html之外的文件都删除掉

  • 再在src目录下,创建一个index.js文件,因为这是webpack打包的入口,就可以在index.js中开始编写React代码

组件化

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

  • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展

  • 但如果将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了

React的组件化

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

  • 根据组件的定义方式,可以分为:函数组件(Functional Component)和类组件(Class Component)

  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)

  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)

这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离

  • 函数组件、无状态组件、展示型组件主要关注UI的展示

  • 类组件、有状态组件、容器型组件主要关注数据逻辑

  • 当然还有很多组件的其他概念:比如异步组件、高阶组件等

类组件

  • 类组件的定义有如下要求

    • 组件的名称是大写字符开头(无论类组件还是函数组件)

    • 类组件需要继承自 React.Component

    • 类组件必须实现render函数

  • 使用class定义一个组件

    • constructor是可选的,通常在constructor中初始化一些数据

    • this.state中维护的就是组件内部的数据

    • render()方法是 class 组件中唯一必须实现的方法

  • render函数的返回值,当 render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一

    • React 元素 通常通过 JSX 创建,例如<div />会被 React 渲染为 DOM 节点, <MyComponent />会被 React 渲染为自定义组件,<div /><MyComponent />均为 React 元素

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

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

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

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

import React, { Component } from 'react'

export class ComClass extends Component {
  render() {
    return (
      <div>类组件</div>
    )
  }
}

export default ComClass

函数组件

函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容

  • 函数组件有自己的特点,后面学习Hooks时,会针对函数式组件进行更多的学习
    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数

    • this关键字不能指向组件实例(因为没有组件实例)

    • 没有内部状态state

  • 定义一个函数组件
    import React from 'react'
    // rfc
    export default function ComFunc() {
      return (
        <div>函数组件</div>
      )
    }
    

生命周期

事物从创建到销毁的整个过程称之为生命周期React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能 image.png

  • 生命周期和生命周期函数的关系:谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的

    • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段

      比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程

      比如更新过程(Update),组件状态发生变化,重新更新渲染的过程

      比如卸载过程(Unmount),组件从DOM树中被移除的过程

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

      比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调

      比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调

      比如实现componentWillUnmount函数:组件即将被移除时,就会回调

  • 生命周期函数解析

    • construct:如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数

      constructor中通常只做两件事情:通过给 this.state 赋值对象来初始化内部的state并为事件绑定实例(this

    • componentDidMount会在组件挂载后(插入 DOM 树中)立即调用

      依赖于DOM的操作可以在这里进行

      在此处发送网络请求就最好的地方(官方建议)

      可以在此处添加一些订阅(会在componentWillUnmount取消订阅)

    • componentDidUpdate:会在更新后会被立即调用,首次渲染不会执行此方法

      当组件更新后,可以在此处对 DOM 进行操作

      如果你对更新前后的 props 进行了比较,可以选择在此处进行网络请求(当 props 未发生变化时,则不会执行网络请求)

    • componentWillUnmount会在组件卸载及销毁之前直接调用

      在此方法中执行必要的清理操作,例如清除 timer取消网络请求、清除在 componentDidMount() 中创建的订阅等

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

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

    • shouldComponentUpdate:该生命周期函数很常用,但是等讲性能优化时再来详细讲解

    • 更详细的生命周期相关的内容,可以参考官网: zh-hans.reactjs.org/docs/reactc…

    import React, { Component } from 'react'
    
    export class ComLife extends Component {
      constructor() {
        console.log('ComLife constructor');
        super()
        this.state = {
          message: '组件生命周期',
        }
      }
      componentDidMount() {
        // 请求在此处
        console.log('ComLife componentDidMount', this);
      }
      componentDidUpdate(prevProps, prevState,snapshot) {
        console.log('ComLife componentDidUpdate', snapshot)
      }
      componentWillUnmount() {
        console.log('ComLife componentWillUnmount')
      }
      // 不常用生命周期
      getSnapshotBeforeUpdate(prevProps, prevState, snapshot) {
        // 组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
        console.log('ComLife getSnapshotBeforeUpdate', prevProps, prevState, snapshot);
        return 'snapshotValue'
      }
      shouldComponentUpdate() {
        // 返回true render会更新反之不会
        console.log('ComLife shouldComponentUpdate不会执行render');
        return false
      }
      renderUpdate() {
        this.setState({
          message: 'ComLife组件更新了'
        })
      }
      render() {
        console.log('ComLife render')
        return (
          <div>
            <span>{this.state.message}</span> 
            <button onClick={e=> this.renderUpdate()}>ComLife更新</button>
          </div>
        )
      }
    }
    
    export default ComLife
    

父子组件通信

在一个React项目中,组件之间的通信是非常重要的环节,父组件在展示子组件,可能会传递一些数据给子组件:

  • 父组件通过 属性=值 的形式来传递给子组件数据

  • 子组件通过 props 参数获取父组件传递过来的数据

  • 案例实现如下image.png

    // ComTab
    import React, { Component } from 'react'
    import ComTabSon from './ComTabSon'
    
    export class ComTab extends Component {
      constructor() {
        super()
        this.state = {
          tabList: [
            {name: '流行', type: 'liuxing'},
            {name: '新款', type: 'xinkuan'},
            {name: '精选', type: 'jingxuan'},
          ],
          currentIndex: 0
        }
      }
      sonItemClick(currentIndex) {
        this.setState({
          currentIndex
        })
      }
      getItemSlot(item) {
        if(item.type === 'liuxing') {
          return <i>流行</i>
        }else if(item.type === 'xinkuan') {
          return <strong>新款</strong>
        }else {
          return <button>精选</button>
        }
      }
      render() {
        const {currentIndex, tabList} = this.state
        return (
          <div>
            <h3>{this.props.contextColor ? this.props.contextColor : 'ComTab-父子通信案例'}</h3> 
            <ComTabSon  
              tabList={tabList} 
              currentIndex={currentIndex}
              itemClick={index=>this.sonItemClick(index)}
              itemSlot={item=>this.getItemSlot(item)}
            />
            <h2>{tabList[currentIndex].name}</h2>
          </div>
        )
      }
    }
    
    export default ComTab
    
    
    
    // ComTabSon
    import React, { Component } from 'react'
    import '../style/comTabSon.css'
    
    export class ComTabSon extends Component {
      itemClick(mi) {
        this.props.itemClick(mi)
      }
      render() {
        console.log('ComTabSon',this.props);
        return (
          <div className='tab'>
            {
              this.props.tabList.map((m,mi)=> {
                return (
                  <div 
                    key={m.type}
                    className={`tab_item ${this.props.currentIndex === mi ? 'active' : ''}`}
                    onClick={e=> this.itemClick(mi)}
                  >
                    {/* <span className={this.props.currentIndex === mi ? 'active' : ''}>{m.name}</span> */}
                    {this.props.itemSlot(m)}
                  </div>
                )
              })
            }
          </div>
        )
      }
    }
    export default ComTabSon
    

插槽(slot)

在开发中抽取了一个组件,但是为了让这个组件具备更强的通用性,不能将组件中的内容限制为固定的div、span等等这些元素,React对于这种需要插槽的情况非常灵活,有两种方案可以实现:

  • 组件的children子元素每个组件都可以获取到 props.children,它包含组件的开始标签和结束标签之间的内容,有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生

  • props属性传递React元素;通过具体的属性名,可以让我们在传入和获取时更加的精准

  • 代码如下:

// ComSlot
import React, { Component } from 'react'
import ComSlotSon from './ComSlotSon'

export class ComSlot extends Component {
  render() {
    const btn = <button>按钮2</button>
    return (
      <div>
        <ComSlotSon
          leftSlot={btn}
          centerSlot={<strong>嘻嘻</strong>}
          rightSlot={<i>哒哒</i>}
        >
          <button>按钮1</button>
          <i>哈哈</i>
          <strong>啦啦</strong>
        </ComSlotSon>
      </div>
    )
  }
}
export default ComSlot


// ComSlotSon
import React, { Component } from 'react'
import '../style/comSlotSon.css'

export class ComSlotSon extends Component {
  leftSlotClick(left) {
    console.log(left);
  }
  render() {
    console.log('ComSlotSon',this.props);
    return 
      (<div>
        <h3>ComSlot-插槽使用children</h3> 
        <div className='slot'>
          {/* 只有一个元素时这样取 */}
          {/* <div>{this.props.children}</div> */}
          <div>{this.props.children[0]}</div>
          <div>{this.props.children[1]}</div>
          <div>{this.props.children[2]}</div>
        </div>

        <h3>ComSlot-插槽使用prop传值</h3> 
        <div className='slot'>
          <div>{this.props.leftSlot}</div>
          <div>{this.props.centerSlot}</div>
          <div>{this.props.rightSlot}</div>
        </div>

      </div>
    )
  }
}
export default ComSlotSon

Context应用-组件数据通信

引入

  • 在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递

  • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)

  • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的 操作

  • React提供了一个APIContext,一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props

  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

API

  • React.createContext

    • 创建一个需要共享的Context对象
    • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context
    • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
  • Context.Provider

    • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化

    • Provider 接收一个 value 属性,传递给消费组件

    • 一个 Provider 可以和多个消费组件有对应关系

    • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据

    • Providervalue 值发生变化时,它内部的所有消费组件都会重新渲染

  • Class.contextType

    • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象

    • 这能让你使用 this.context 来消费最近 Context 上的那个值

    • 你可以在任何生命周期中访问到它,包括 render 函数中

  • Context.Consumer

    • React 组件也可以订阅到 context 变更,这能让你在函数式组件 中完成订阅 context

    • 这里需要函数作为子元素(function as child)这种做法

    • 这个函数接收当前的 context 值,返回一个 React 节点

    • 什么时候使用Context.Consumer呢?

      当使用value的组件是一个函数式组件时

      当组件中需要使用多个Context

  • API练习代码如下

    // themeContext.js
    import { createContext } from "react";
    
    // import React from "react";
    // const ThemeContext = React.createContext("dark");
    const ThemeContext = createContext();
    export default ThemeContext;
    
    
    
    // userContext.js
    import { createContext } from "react";
    
    // import React from "react";
    // const UserContext = React.createContext({
    //   name: "context",
    //   phone: "13323065577",
    // });
    const UserContext = createContext({
      name: "context",
      phone: "13323065577",
    });
    export default UserContext;
    
    
    
    // ComContext.jsx
    import React, { Component } from 'react'
    import ComText from './ComText'
    import ThemeContext from '../utils/themeContext'
    import UserContext from '../utils/userContext'
    
    export class ComContext extends Component {
      constructor() {
        super()
        this.state = {
          contextColor: 'ComContext页面传过来的值颜色'
        }
      }
      render() {
        console.log(UserContext)
        return (
          <div>
            <h3>ComContext-非父子数据共享</h3>
            <ThemeContext.Provider value='light'>
              {/* 使用UserContext的默认value值 */}
              <UserContext.Provider value={UserContext._currentValue}>
                {/* ComTextSon里面使用了传入的值 */}
                <ComText />
              </UserContext.Provider>
            </ThemeContext.Provider>
          </div>
        )
      }
    }
    export default ComContext
    
    
    
    // 
    import React, { Component } from 'react'
    import ComTextSon from './ComTextSon'
    
    export class ComText extends Component {
      render() {
        const btn = <button>按钮2</button>
        return (
          <div>
            <ComTextSon
              leftSlot={btn}
              centerSlot={<strong>嘻嘻</strong>}
              rightSlot={<i>哒哒</i>}
            >
              <button>按钮1</button>
              <i>哈哈</i>
              <strong>啦啦</strong>
            </ComTextSon>
          </div>
        )
      }
    }
    export default ComText
    
    
    
    // ComTextSon.jsx
    import React, { Component } from 'react'
    import '../style/ComTextSon.css'
    import ThemeContext from '../utils/themeContext';
    import UserContext from '../utils/userContext';
    
    export class ComTextSon extends Component {
      leftSlotClick(left) {
        console.log(left);
      }
      render() {
        console.log('ComTextSon',this.props);
        return this.context ? 
          (<div>
            {/* 一个Context时类组件可以使用这个,多个使用Consumer */}
            <h3>ComContext-ThemeContext页面传过来的值{this.context}</h3>
            <UserContext.Consumer>
              {value => {
                return <i>使用Consumer: {value.name}</i>
              }}
            </UserContext.Consumer>
          </div>) 
          : 
          (<div>
            <h3>ComTextSon-没有context</h3> 
          </div>
        )
      }
    }
    // 一个Context时类组件可以使用这个
    ComTextSon.contextType = ThemeContext
    export default ComTextSon
    

setState

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

  • 因为修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改 React并不知道数据发生了变化

  • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化

  • 必须通过setState来告知React数据已经发生了变化

  • 在组件中没有实现setState的方法,为什么可以调用呢?因为 setState方法是从Component中继承过来的

异步更新

  • setState的更新是异步的?

    • 在下面的练习代码中可以看到在changeMsg方法中打印更新之后的message还是旧的值

    • 可见setState是异步的操作,并不能在执行完setState之后立马拿到最新的state的结果

  • 为什么setState设计为异步呢?

    • setState设计为异步其实之前在GitHub上也有很多的讨论

    • React核心成员(Redux的作者)Dan Abramov也有回复,有兴趣可以参考一下github.com/facebook/re…

    • 简单的总结一下就是 setState设计为异步,可以显著的提升性能并保证内部一致性

    • 如果每次调用 setState都进行一次更新,意味着render函数会被频繁调用,界面重新渲染效率是很低的

    • 最好的办法应该是获取到多个更新,之后进行批量更新

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

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

  • 那么如何可以获取到更新后的值呢?

    • setState的回调中获取setState接受两个参数,第二个参数是一个回调函数,会在更新后会执行:setState(partialState, callback)

    • 在生命周期函数中获取componentDidUpdate(prevProps, prevState,snapshot) {console.log(this.state.message);}

一定是异步吗?

  • React18之前

    • 在组件生命周期或React合成事件中,setState是异步
      image.png
    • setTimeout或者原生dom事件中,setState是同步
      image.png
  • React18之后

    • 在React18之后,默认所有的操作都被放到了批处理中(异步处理)
      image.png

    • 如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作 image.png

练习代码如下

import React, { Component } from 'react'

export class ComState extends Component {
  constructor() {
    super()
    this.state = {
      message: 'ComState异步'
    }
  }
  changeMsg() {
    console.log(this.state.message, 'changeMsg-setState之前');
    // 基础用法
    // this.setState({
    //   message: '点击了查看异步信息'
    // })
    /* 
      下面log打印的还是老的message的值不是修改后的,在react18之前setTimeou,
      promise和原生Dom事件是同步的,18之后默认都是异步的 。
      为什么设计成异步的?redux作者在issue中这样回答的:
      1. 批量更新是有益的,如果每次更新都调用render函数,频繁调用效率低
      2. 保证内部一致性,如果state设为同步会立即刷新但还没有执行render函数,那么state和props不能保持一致
    */
    // console.log(this.state.message, 'setState之后');

    // 以下setState另外用法,可以拿到更新后的值
    
    //方法一:
    /* this.setState({
      message: '点击了查看异步信息按钮更新了'
    },()=> {
      console.log('回调里是最新的值',this.state.message);
    }) */

    // 方法二:
    this.setState((state, props)=> {
      // 1.编写一些对新的state处理逻辑
      // 2.可以获取之前的state和props值
      console.log(this.state.message, this.props)
      return {
        message: '点击了查看异步信息按钮更新了'
      }
    })

    console.log('还是老的值', this.state.message);

  }
  // 方法三:
  componentDidUpdate(prevProps, prevState) {
    console.log('ComState-componentDidUpdate',prevState, prevProps, this.state.message);
  }
  render() {
    console.log('render')
    return (
      <div>
        <h3>ComState-setState详细使用</h3>
        <i>{this.state.message}</i>
        <button onClick={e=>this.changeMsg()}>查看异步信息</button>
      </div>
    )
  }
}
export default ComState

render更新

当 props 或 state 发生变化时,React 会自动调用组件的 render 方法来重新渲染组件。这是 React 的核心机制之一,用于确保 UI 与数据保持同步

  • props 变化:当父组件传递的 props 发生变化时,子组件会重新渲染

  • state 变化:当组件内部的 state 发生变化时(例如通过 setState 或 useState 更新状态),组件会重新渲染

  • 下面看下渲染的机制和如何进行性能优化

机制

image.png

  • 生成 Virtual DOMrender 方法会根据当前的 props 和 state 生成新的 Virtual DOM

  • 与旧 Virtual DOM 对比React 会通过 diff 算法对比新旧 Virtual DOM,找出需要更新的部分

    • 同层节点之间相互比较,不会跨节点比较,tag 不同直接删掉重建

    • 开发中可以通过key来指定哪些节点在不同的渲染下保持稳定

    • key应该是唯一的key不要使用随机数和index作为key

    • 在最后位置插入数据,有无key意义并不大

    • 在前面插入数据

      • 在没有key的情况下,所有的li都需要进行修改

      • 当子元素拥有 keyReact 使用 key 来匹配原有树上的子元素以及最新树上的子元素 image.png

  • 更新真实 DOM:只有实际发生变化的部分才会更新到真实 DOM,而不是整个页面重新渲染

优化

  • 组件重新渲染时,默认情况下会递归触发所有子组件的重新渲染

  • 上面情况显然是没有必要的,会造成比较低的性能,理想应该只有依赖的数据(state、 props)发生改变时,再调用自己的render方法

  • 事实上React也考虑到了一点,React 通过以下机制优化性能:

    • Virtual DOM 和 diff 算法:React 会对比新旧 Virtual DOM,只有实际发生变化的部分才会更新到真实 DOM

    • 浅比较(Shallow Comparison

      如果子组件的 props 和 state 没有变化,React 会跳过子组件的渲染

      对于函数组件,可以使用 React.memo 来避免不必要的重新渲染

      对于类组件,可以使用 shouldComponentUpdate 或继承 PureComponent 来优化

shallowEqual

shallowEqual 是 React 中用于浅比较两个对象或数组是否相等的方法,它通常用于优化组件的渲染性能,比如在 React.memoPureComponent 或自定义 shouldComponentUpdate 中

  • shallowEqual 会比较两个对象的顶层属性是否相等

  • 如果属性是基本类型(如 stringnumberboolean),直接比较值

  • 如果属性是引用类型(如 objectarray),比较引用地址(是否指向同一个对象)

  • 不会递归比较嵌套对象或数组的内容,所有尽量将 props 和 state 扁平化

  • 以下是 shallowEqual 的简化实现逻辑

    function shallowEqual(objA, objB) {
      // 如果两个对象是同一个引用,直接返回 true
      if (objA === objB) return true;
    
      // 如果其中一个不是对象或为 null,返回 false
      if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
        return false;
      }
    
      const keysA = Object.keys(objA);
      const keysB = Object.keys(objB);
    
      // 如果属性数量不同,返回 false
      if (keysA.length !== keysB.length) return false;
    
      // 遍历所有属性,比较值
      for (let i = 0; i < keysA.length; i++) {
        const key = keysA[i];
        if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
          return false;
        }
      }
    
      return true;
    }
    
shouldComponentUpdate

React提供了一个生命周期方法 shouldComponentUpdate(简称为SCU),这个方法接受参数,并且需要有返回值,也可以在此方法中使用shallowEqual方法比较

  • 该方法有两个参数
    • nextProps 修改之后,最新的props属性

    • nextState 修改之后,最新的state属性

  • 该方法返回值是一个boolean类型
    • 返回值为true,那么就需要调用render方法

    • 返回值为false,那么就不需要调用render方法

    • 默认返回的是true,也就是只要state发生改变,就会调用render方法

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
  }
  render() {
    return <div>{this.props.value}</div>;
  }
}
PureComponent和memo
  • 针对类组件可以将class继承自PureComponent

  • 针对函数式组件组件可以使用一个高阶组件memo

  • 代码如下:

    import React, { PureComponent, memo } from "react";
    
    const ComRenderFunc = memo((props) => {
      return <div>ComRendeFunc-memo</div>;
    });
    
    export class ComRender extends PureComponent {
      /* 
        只要更改state或者props的值都会再次执行render函数,
        我们可以对这个进行优化,可以在生命周期shouldComponentUpdate中,
        进行判return false则不会更新render,但数据多时就会比较麻烦,
    
        react提供了PureComponent和memo在项目中尽量使用这两个,
        他们的原理:也是进行了判断如果我们写了shouldComponentUpdate,
        react会执行它,没有写就判断是不是使用了PureComponent,使用了react
        就会把isPureComponent设为true,会执行shallowEqual进行判断
       */
      render() {
        return (
          <div>
            <h3>ComRender-对render的优化</h3>
            <ComRenderFunc />
          </div>
        );
      }
    }
    export default ComRender;
    

使用ref

React的开发模式中,通常情况下不需要也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作,这时可以通过refs获取DOM,目前有三种方式ref=XXX

  • 传入字符串:使用时通过 this.refs传入的字符串格式获取对应的元素

  • 传入一个对象:对象是通过 React.createRef() 方式创建出来的,使用时获取到创建的对象其中有一个current属性就是对应的元素

  • 传入一个函数:该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象,可以自己保存,使用时直接拿到之前保存的元素对象即可

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

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性

  • ref 属性用于自定义 class 组件时ref 对象接收组件的挂载实例作为其 current 属性

  • 不能在函数组件上使用 ref 属性,因为他们没有实例,可以通过 React.forwardRef,后面也会学习 hooks 中如何使用ref

练习代码如下:

import React, { PureComponent, createRef, forwardRef, memo } from "react";

const ComRefFun = memo(
  forwardRef((props, ref) => {
    return (
      // 函数组件必须使用forwardRef,想让父组件获取那个元素就在那个元素上加ref
      <div ref={ref}>
        <span>ComRefFun-函数组件</span>
        <i>哒哒哒哒</i>
      </div>
    );
  })
);
export class ComRef extends PureComponent {
  constructor() {
    super();
    this.funel = createRef(); // 2
    this.iel = createRef(); // 2
    this.sel = null; // 3
  }
  getRefClassDom() {
    console.log(this.refs.pel); // 第一种
    console.log(this.iel.current); // 第二种(推荐)如果是个组件可以执行组件的方法
    console.log(this.sel); // 第三种
  }
  getRefFunDom() {
    console.log(this.funel.current);
  }
  render() {
    return (
      <div>
        <h3>ComRef</h3>
        <p ref="pel">第一种:哈哈哈哈</p>
        <i ref={this.iel}>第二种:嘿嘿嘿嘿</i>
        <strong ref={(el) => (this.sel = el)}>第三种:嘻嘻嘻嘻</strong>
        <button onClick={(e) => this.getRefClassDom()}>获取类组件dom</button>
        <ComRefFun ref={this.funel} />
        <button onClick={(e) => this.getRefFunDom()}>获取函数组件dom</button>
      </div>
    );
  }
}
export default ComRef;

表单组件

ReactHTML表单的处理方式和普通的DOM元素不太一样,React推荐大多数情况下使用受控组件来处理表单数据

  • 一个受控组件中,表单数据是由 React 组件来管理的,只能使用 setState() 更新

  • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理,不受setState()的控制

受控组件

适合需要精确控制表单数据、实时验证或复杂逻辑的场景

  • 特点:

    • 状态由React管理:组件的值通过stateprops来控制

    • 数据流单向:组件的值由Reactstateprops决定,用户输入时会触发事件处理函数更新状态

    • 实时同步:组件的值与React的状态始终保持一致

  • 执行情况: image.png

  • 实现方式:

    • 在组件的state中定义表单元素的值

    • state的值绑定到表单元素的value属性

    • 通过onChange事件监听用户输入,更新state

    import React, { useState } from "react";
    
    function ControlledInput() {
      const [value, setValue] = useState("");
    
      const handleChange = (event) => {
        setValue(event.target.value); // 更新state
      };
    
      return (
        <div>
          <input type="text" value={value} onChange={handleChange} />
          <p>输入的内容: {value}</p>
        </div>
      );
    }
    
    export default ControlledInput;
    

非受控组件

适合简单表单、性能敏感或与第三方库集成的场景

  • 特点:
    • 状态由DOM管理:组件的值由DOM节点自身维护,而不是通过Reactstateprops

    • 数据流双向:组件的值可以通过ref从DOM节点中获取

    • 不实时同步:组件的值与React的状态没有直接关联,只有在需要时(如提交表单时)才从DOM中获取值

  • 实现方式:
    • 使用ref来访问DOM节点,在需要时(如表单提交时),通过ref获取表单元素的值

    • 在非受控组件中通常使用defaultValue来设置默认值

练习代码

import React, { PureComponent, createRef } from 'react'

export class ComForm extends PureComponent {
  constructor() {
    super()
    this.state = {
      username: 'shine',
      password: '1313',
      isAgree: false,
      selectHobby: 'dance',
      selectHobbys: ['dance'],
      hobby: [],
      hobbies: [
        { value: "sing", text: "唱", isChecked: false },
        { value: "dance", text: "跳", isChecked: false },
        { value: "rap", text: "rap", isChecked: false }
      ],
      fei: '哒哒哒哒'
    }
    this.feiRef = createRef()
  }
  formSubmit(e) {
    // 阻止默认提交行为
    e.preventDefault()
    // 拿到数据可以做提交动作
    console.log('获取非受控值', this.feiRef.current.value);
    console.log(this.state);
  }
  changeValue(e) {
    this.setState({
      [e.target.name]: e.target.value || !e.target.checked
    })
  }
  changeHobbyValue(e,i) {
    let list = [...this.state.hobbies]
    list[i].isChecked = e.target.checked
    let value = list.filter(f=>f.isChecked).map(m=>m.value)
    this.setState({
      hobbies: list,
      hobby: value
    })
  }
  changeSelectValue(e) {
    // 额外补充: Array.from(可迭代对象)
    // Array.from(arguments)
    // e.target.selectedOptions是HTMLCollection转为数组
    let list = Array.from(e.target.selectedOptions, item => item.value)
    this.setState({
      selectHobbys: list
    })
    console.log(e.target.selectedOptions);
  }
  render() {
    return (
      <div>
        <h3>ComForm-受控组件和非受控组件</h3>
        <form onSubmit={e=> this.formSubmit(e)}>
          <label htmlFor="username">
            用户名:<input id='username' name='username' type="text" value={this.state.username} onChange={e=>this.changeValue(e)} />
          </label>
          <label htmlFor="password">
            密码:<input id='password' name='password' type="password" value={this.state.password} onChange={e=>this.changeValue(e)} />
          </label>
          {/* checkbox单选 */}
          <label htmlFor="isAgree">
            <input id='isAgree' name='isAgree' type="checkbox" checked={this.state.agree} onChange={e=> this.changeValue(e)} />
            同意协议
          </label>
          {/* checkbox多选 */}
          <div>
            {
              this.state.hobbies.map((m,mi)=> {
                return (
                  <label htmlFor={m.value} key={m.value}>
                    <input 
                      id={m.value} 
                      type="checkbox" 
                      checked={m.isChecked} 
                      onChange={e=> this.changeHobbyValue(e, mi)}
                    />
                    {m.text}
                  </label>
                )
              })
            }
          </div>
          {/* select单选 */}
          <select name="selectHobby" id="selectHobby" value={this.state.selectHobby} onChange={e=>this.changeValue(e)}>
            {
              this.state.hobbies.map((m,mi)=> {
                return <option key={m.value} value={m.value}>{m.text}</option>
              })
            }
          </select>
          {/* select多选选 */}
          <select name="selectHobbys" id="selectHobbys" multiple value={this.state.selectHobbys} onChange={e=>this.changeSelectValue(e)}>
            {
              this.state.hobbies.map(m=> {
                return <option key={m.value} value={m.value}>{m.text}</option>
              })
            }
          </select>
          {/* 非受控组件 */}
          <label htmlFor="fei">
            <input id='fet' type="text" defaultValue={this.state.fei} ref={this.feiRef} />
          </label>
          <button type='submit'>提交</button>
        </form>
      </div>
    )
  }
}

export default ComForm

高阶组件

相信都知道也用过高阶函数,高阶组件和它非常相似,高阶函数至少满足以下条件之一:接受一个或多个函数作为输入或者输出一个函数JavaScript中比较常见高阶函数的filter、map、reduce都是高阶函数

  • 高阶组件的英文是 Higher-Order Components,简称为 HOC

  • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数

  • 高阶组件本身不是一个组件,而是一个函数,这个函数的参数是一个组件,返回值也是一个组件

  • 不要修改原组件:高阶组件应该通过组合的方式增强组件功能,而不是直接修改原组件

  • 传递props:确保将高阶组件接收到的props传递给被包裹的组件

  • 显示名称:为高阶组件设置显示名称(displayName),以便在调试时更容易识别

  • 案例代码:

    // propsHoc.js
    import React, { PureComponent } from "react";
    
    export default function propsHoc(OriginCom) {
      // 接收组件
      class PropHoc extends PureComponent {
        constructor(props) {
          super(props);
          this.state = {
            userInfo: {
              name: "propsHoc",
              phone: "17538138619",
            },
          };
        }
        render() {
          console.log(this.props);
          return <OriginCom {...this.props} {...this.state.userInfo} />;
        }
      }
      // 返回组件
      return PropHoc;
    }
    
    
    // authHoc.js
    import React from "react";
    
    function authHoc(OriginCom) {
      // return class NewOriginCom extends PureComponent {
      //   render() {
      //     return <OriginCom />;
      //   }
      // };
    
      // 这里省略了组件名字,参考函数组件返回时可以不写组件名字,类组件也可以
      // return class extends PureComponent {
      //   render() {
      //     return <OriginCom />;
      //   }
      // };
    
      return (props) => {
        // 函数组件返回
        let token = window.localStorage.getItem("token");
        return token ? <OriginCom {...props} /> : <h3>请先登录。。。</h3>;
      };
    }
    export default authHoc;
    
    
    // lifeHoc.js
    import { PureComponent } from "react";
    
    function lifeHoc(OriginCom) {
      return class extends PureComponent {
        UNSAFE_componentWillMount() {
          this.start = new Date().getTime();
        }
        componentDidMount() {
          this.end = new Date().getTime();
          let time = this.end - this.start;
          console.log(`当前${OriginCom.name}页面花费了${time}ms渲染完成!`);
        }
        render() {
          return <OriginCom {...this.props} />;
        }
      };
    }
    
    export default lifeHoc;
    
    
    
    // ComHoc.jsx
    import React, { PureComponent } from 'react'
    import propsHoc from '../utils/propsHoc'
    import authHoc from '../utils/authHoc'
    import lifeHoc from '../utils/lifeHoc'
    
    const Son1 = propsHoc(function Son1(props) { // propsHoc传入函数组件得到组件,则Son1为组件,render直接引入
      console.log('propsHo增加的props值', props);
      return (
        <div>
          <h4>1. propsHoc: 不改原有代码的情况下添加新的props</h4>
          <i>原来prop值:{props.type}</i>
          <i>通过propsHoc加的props:{props.name}-{props.phone}</i>
        </div>
      )
    })
    const Son2 = authHoc(function Son2(props) {
      return (
        <div>
          <strong>已登录,尽情玩吧</strong>
        </div>
      )
    })
    const Son3 = lifeHoc(function Son2(props) {
      return (
        <div>
          <h3>组件渲染耗时</h3>
          <ul>
            <li>数据列表1</li>
            <li>数据列表2</li>
            <li>数据列表3</li>
            <li>数据列表4</li>
            <li>数据列表5</li>
            <li>数据列表6</li>
            <li>数据列表7</li>
            <li>数据列表8</li>
            <li>数据列表9</li>
            <li>数据列表10</li>
          </ul>
        </div>
      )
    })
    export class ComHoc extends PureComponent {
      render() {
        return (
          <div>
            <h3>ComHoc-高阶组件-接收组件返回组件</h3>
            {/* 回忆高阶函数:接收一个或者多个函数,返回一个函数 */}
    
            {/* props增强-不改原有代码的情况下添加新的props, 下面传入的type可以在propsHoc拿到 */}
            <Son1 type='ComHocFunSon1' /> 
    
            {/* 登录鉴权案例 */}
            <Son2 />
    
            {/* 组件渲染耗时 */}
            <Son3 />
          </div>
        )
      }
    }
    export default ComHoc
    

Portals的使用

React中,默认情况下,子组件会作为父组件的子元素渲染到DOM中。然而使用Portals时,React允许你将子组件渲染到其父组件DOM层级之外的地方

  • ReactDOM.createPortal(child, container)

  • 第一个参数(child)是任何你希望渲染渲染的 React 子元素

  • 第二个参数(container)是一个你希望将该React元素渲染到的DOM节点

  • 这种技术特别适用于需要在页面中脱离父组件结构显示的情况,比如模态框(Modal)、工具提示(Tooltip)、下拉菜单(Dropdown)等

  • 练习代码如下:

    import React, { PureComponent } from 'react'
    import { createPortal } from 'react-dom'
    
    class ComPop extends PureComponent {
      render() {
        return createPortal(
          // 以插槽形式传过来的元素都会挂载到pop div上面
          this.props.children,
          document.getElementById('pop')
        )
      }
    }
    export class ComPortals extends PureComponent {
      render() {
        return (
          <div>
            {
              createPortal(
                // h3就会挂在pop父元素上面
                  <h3>ComPortals-将元素挂载到任何dom上</h3>,
                  document.getElementById('pop')
              )
            }
    
            {/* 将内容挂载到pop上组件封装 */}
            <ComPop>
              <h4>ComPop组件</h4>
            </ComPop>
          </div>
        )
      }
    }
    
    export default ComPortals
    

fragment

通常在 React 中,如果你返回多个元素,React 会要求你将它们包裹在一个父元素中,但这个父元素会在真实的 DOM 中被渲染出来,可能会影响布局或结构。而 Fragment 解决了这个问题,它允许你将多个元素分组在一起而不添加额外的 DOM 节点

  • 使用<Fragment>...</Fragment>包裹元素,React 还提供了一个简写的语法来使用 Fragment,即使用空标签 <></> 来包裹元素

  • 当在列表渲染中使用 Fragment 时,可以为每个 Fragment 添加 key 属性,但使用简写时不能加key

  • 练习代码如下:

    import React, { Fragment, PureComponent } from 'react'
    
    export class ComFragment extends PureComponent {
      constructor() {
        super()
        this.state = {
          list: ['啦啦啦','哈哈哈啊哈','哒哒哒哒','嘻嘻嘻嘻']
        }
      }
      render() {
        return (
          // 不会渲染元素
          // <Fragment>
          //   <h3>
          //     ComFragment
          //   </h3>
          // </Fragment>
    
          // 语法糖
          // <>
          //   <h3>
          //     ComFragment
          //   </h3>
          //   <i>哈哈啊哈哈哈</i>
          // </>
    
          <div>
            {
              this.state.list.map(m=>{
                return (
                  // 需要绑定key时,此时不能用语法糖
                  <Fragment key={m}> 
                    <i>{m}/</i>
                  </Fragment>
                )
              })
            }
          </div>
        )
      }
    }
    
    export default ComFragment
    

StrictMode

  • StrictMode 是一个用于帮助开发者识别应用潜在问题的工具

  • 它本身不会渲染任何可见的 UI 元素只会在开发模式下激活额外的检查和警告

  • 用于标识不安全的生命周期方法、意外的副作用和过时的API等问题

  • 可以为应用程序的任何部分启用严格模式,严格模式检查的是什么?

    • 帮助识别不安全的生命周期方法:React 会对使用了过时的生命周期方法的组件发出警告,提醒开发者使用新的方法

    • 检查副作用StrictMode 会在开发环境中对组件的生命周期进行额外检查,帮助开发者确保副作用操作(如订阅事件、定时器、数据请求等)在组件卸载时能够正确清理,防止潜在的内存泄漏或不一致的状态

    • 警告过时的 API:当应用使用一些已经废弃的 API(例如 findDOMNode)时,StrictMode 会出警告,提示开发者使用新的替代方法

    • 检查不一致的渲染StrictMode 会做一些额外的渲染和检测,确保组件的渲染是符合预期的,从而帮助开发者发现潜在的渲染问题

  • 练习代码如下:

    import React, { PureComponent, StrictMode } from 'react'
    
    export class ComStrictSon extends PureComponent {
      constructor() {
        super()
        console.log('111constructor'); // 严格模式会执行两次constructor,严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用,在生产环境中,是不会被调用两次的
      }
      // componentDidMount() {
      //   console.log(this.refs.son); // 严格模式会报错
      // }
    
      // UNSAFE_componentWillMount(){ // 严格模式会报错
      //   console.log('UNSAFE_componentWillMount');
      // }
      render() {
        return (
          <div>
            <h3>ComStrictSon</h3>
          </div>
        )
      }
    }
    export class ComStrict extends PureComponent {
      render() {
        return (
          <div>
            <h3>ComStrictMode-严格模式</h3>
            {/* StrictMode不会渲染元素 */}
            <StrictMode>
              {/* <ComStrictSon ref='son'  严格模式会报错 /> */}
              <ComStrictSon />
            </StrictMode>
          </div>
        )
      }
    }
    
    export default ComStrict
    

app.jsx入口文件如下:

里面是组件化练习的所有组件引入

import React, { Component } from "react";
import ComClass from "./component/ComClass";
import ComFunc from "./component/ComFunc";
import ComLife from "./component/ComLife";
import ComFather from "./component/ComFather";
import ComTab from "./component/ComTab";
import ComSlot from "./component/ComSlot";
import ComContext from "./component/ComContext";
import ComEventBus from "./component/ComEventBus";
import ComState from "./component/ComState";
import ComRender from "./component/ComRender";
import ComRef from "./component/ComRef";
import ComForm from "./component/ComForm";
import ComHoc from "./component/ComHoc";
import ComPortals from "./component/ComPortals";
import ComFragment from "./component/ComFragment";
import ComStrict from "./component/ComStrict";
import ComTransite from "./component/ComTransite";
import ComCss from "./component/ComCss";

export class App extends Component {
  render() {
    return (
      <div>
        <h1>APP</h1>
        <hr />
        {/* 类组件和函数组件 */}
        <ComClass />
        <ComFunc />
        <hr />
        {/* 组件生命周期 */}
        <ComLife />
        <hr />
        {/* 父子通信 */}
        <ComFather />
        <hr />
        {/* 通信案例 */}
        <ComTab />
        <hr />
        {/* 插槽使用 */}
        <ComSlot />
        <hr />
        {/* 非父子数据共享 */}
        <ComContext />
        <hr />
        {/* 非父子事件传值 */}
        <ComEventBus />
        <hr />
        {/* setState详细使用 */}
        <ComState />
        <hr />
        {/* render的优化 */}
        <ComRender />
        <hr />
        {/* ref的使用 */}
        <ComRef />
        <hr />
        {/* 受控和非受控组件 */}
        <ComForm />
        <hr />
        {/* 高阶组件 */}
        <ComHoc />
        <hr />
        {/* ComPortals */}
        <ComPortals />
        <hr />
        {/* ComFragment */}
        <ComFragment />
        <hr />
        {/* 严格模式 */}
        <ComStrict />
        <hr />
        {/* 动画 */}
        <ComTransite />
        <hr />
        {/* react中写css */}
        <ComCss />
        <hr />
      </div>
    );
  }
}

export default App;