Skip to content

S13-02 React-组件化

[TOC]

组件化开发

什么是组件化开发

组件化是一种分而治之的思想:

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

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

我们需要通过组件化的思想来思考整个应用程序

  • 我们将一个完整的页面分成很多个组件;

  • 每个组件都用于实现页面的一个功能块;

  • 而每一个组件又可以进行细分;

  • 而组件本身又可以在多个地方进行复用;

image-20230318103923688

React的组件化

组件化概念

组件化是React的核心思想,也是我们后续课程的重点,前面我们封装的App本身就是一个组件:

  • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。

  • 任何的应用都会被抽象成一颗组件树

image-20230318104003313

组件化思想的应用:

  • 有了组件化的思想,我们在之后的开发中就要充分的利用它。

  • 尽可能的将页面拆分成一个个小的、可复用的组件。

  • 这样让我们的代码更加方便组织和管理,并且扩展性也更强。

组件分类:

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

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

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

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

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

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

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

当然还有很多组件的其他概念:比如异步组件高阶组件等,我们后续再学习。

类组件

类组件定义要求:

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

  • 类组件需要继承React.Component

  • 类组件必须实现render函数

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

使用class定义一个组件:

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

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

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

image-20230318104048676

render函数的返回值

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

  • React 元素:通常通过 JSX 创建。

    • <div/> 会被 React 渲染为 DOM 节点,

    • <MyComponent/> 会被 React 渲染为自定义组件;

    • 无论是 <div/> 还是 <MyComponent/> 均为 React 元素。

    js
        // 1. 渲染 React元素: 通过jsx编写的代码会被编译成 React.createElement
        return (
          <div>
            <div>App</div>
            <HelloReact />
          </div>
        )
  • 数组或 fragments:使得 render 方法可以返回多个元素。

    js
        // 2. 渲染 数组
        return ['aaa', ' bbb', ' ccc']
    	// 或者
        // 注意:每行后面有[,号]因为这是数组
        return [
          <div>App</div>,
          <h2>App组件</h2>,
          <span>标签</span>
        ]
  • Portals:可以渲染子节点到不同的 DOM 子树中。

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

    js
        // 3. 渲染字符串、数字
        return 'String'
    	// 或者
        return 900
  • 布尔类型或 null,undefined:什么都不渲染。

    js
        // 4. 渲染 Boolean、null、undefined
        return true
    	// 或者
        return null
    	// 或者
        return undefined

函数组件

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

函数组件特点(无hooks)

函数组件有自己的特点(当然,后面我们会讲hooks,就不一样了):

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数

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

  • 没有内部状态(state)

定义函数组件

js
export default function App () {
  return (
    <div>
      <div>函数组件APP</div>
    </div>
  ) 
}

在前面的学习中,我们主要讲解类组件,后面学习Hooks时,会针对函数式组件进行更多的学习。

组件生命周期

认识生命周期

很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期

React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;

生命周期和生命周期函数的关系:

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

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

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

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

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

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

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

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

  • 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;

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

生命周期解析

我们先来学习一下最基础、最常用的生命周期函数:

image-20230318104155317

Mounting过程:

js
export default class App extends Component {
  constructor() {
+    // 1:constructor
    super()
    this.state = {
      msg: 'Hello React'
    }
  }
  render() {
    const { msg } = this.state
+    // 2:render函数
    console.log('App render', msg)
    return (
      <div>{msg}</div>
    )
  }

+  // 3:componentDidMount
  componentDidMount() {
    console.log('App componentDidMount')
  }
}

注意: 在第一次挂载时,已经可以获取到state中的值

Updating过程

执行顺序:1,2,3

js
export default class App extends Component {
  constructor() {
    super()
    this.state = {
      msg: 'Hello React'
    }
  }
+  // 2. App render 你好React
  render() {
    const { msg } = this.state
    console.log('App render', msg)
    return (
      <div>
        <div>{msg}</div>
+        <button onClick={e => this.changeMsg()}>componentDidUpdate</button>
      </div>
    )
  }

+  // 1. App changeMsg
  changeMsg() {
    console.log('App changeMsg')
    this.setState({ msg: '你好React' })
  }

+  // 3. App componentDidUpdate
  componentDidUpdate() {
    console.log('App componentDidUpdate')
  }
}

Unmounting过程

js
// App.jsx

export default class App extends Component {
  constructor() {
    super()
    this.state = {
      msg: 'Hello React',
      isShowCpn: true
    }
  }
+  // 1. 显示;App render Hello React
  render() {
    const { msg, isShowCpn } = this.state
    console.log('App render', msg)
    return (
      <div>
        <div>{msg}</div>
        <button onClick={e => this.changeMsg()}>更新组件</button>
        <hr />
        <button onClick={e => this.setState({ isShowCpn: !this.state.isShowCpn })}>切换组件</button>
        { isShowCpn && <HelloReact /> }
      </div>
    )
  }

+  // 3. 显示:App componentDidUpdate
  componentDidUpdate() {
    console.log('App componentDidUpdate')
  }
}
js
// 子组件
export default class HelloReact extends Component {
  render() {
    return (
      <div>HelloReact</div>
    )
  }

+  // 2. 显示:HelloReact componentWillUnmount
  componentWillUnmount() {
    console.log('HelloReact componentWillUnmount')
  }
}

生命周期函数

Constructor

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

constructor中通常只做两件事情:

  • 通过给 this.state 赋值对象来初始化内部的state

  • 为事件绑定实例(this);

image-20230318104213920

componentDidMount

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

componentDidMount中通常进行哪里操作呢?

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

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

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

image-20230318104221442

componentDidUpdate

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

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

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

componentWillUnmount

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

  • 在此方法中执行必要的清理操作

  • 例如,清除 timer取消网络请求清除在 componentDidMount() 中创建的订阅等;

不常用生命周期函数

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:

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

来更新state;

  • getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一

些信息(比如说滚动位置);

js
  getSnapshotBeforeUpdate() {
    return {
      scrollPostion: 2000
    }
  }

在componentDidUpdate中获取之前保存的snapshot

js
  componentDidUpdate(prevProps, prevState, snap) {
    // {} {msg: 'Hello React'} {scrollPostion: 2000}
    console.log(prevProps, prevState, snap)
  }
  • shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;

    js
      // 根据return的值决定组件是否执行更新操作
      shouldComponentUpdate() {
        return true
      }

另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。

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

https://zh-hans.reactjs.org/docs/react-component.html

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

image-20230318134151803

组件通信

认识组件的嵌套

组件之间存在嵌套关系:

  • 在之前的案例中,我们只是创建了一个组件App;

  • 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;

  • 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;

  • 再将这些组件组合嵌套在一起,最终形成我们的应用程序;

上面的嵌套逻辑如下,它们存在如下关系:

  • App组件是Header、Main、Footer组件的父组件;

  • Main组件是Banner、ProductList组件的父组件;

image-20230318104447102

认识组件间的通信

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

  • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;

  • 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;

  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;

总之,在一个React项目中,组件之间的通信是非常重要的环节;

父组件在展示子组件,可能会传递一些数据给子组件:**

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

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

父组件中

js
export default class Main extends Component {
  constructor() {
    super()
    this.state = {
+      banners: ['首页', '个人', '分类'],
      news: ['商品1','商品2','商品3' ]
    }
  }
  render() {
    const { banners } = this.state
    return (
      <div>
        <div>Main</div>
        <hr />
+        <MainBanner banners={banners}></MainBanner>
      </div>
    )
  }
}

子组件中

js
export default class MainBanner extends Component {
+  constructor(props) {
+    super(props)
    this.state = {}
  }
  render() {
+    const { banners } = this.props
    return (
      <div>
        <div>MainBanner</div>
        <div>
          {
            banners.map(item => {
              return (
                <span className="item" key={item}>{ item }</span>
              )
            })
          }
        </div>
      </div>
    )
  }
}

组件通信-父传子 - 类组件、函数组件

类组件(真实数据展示)

父组件

js
export default class Main extends Component {
  constructor() {
    super()
    this.state = {
+      banners: [],
+      recommends: []
    }
  }
  render() {
    const { banners, recommends } = this.state
    return (
      <div>
        <div>Main</div>
        <hr />
        <div className="content">
+          <MainBanner banners={banners}></MainBanner>
+          <MainRecommend recommends={recommends}></MainRecommend>
        </div>
      </div>
    )
  }

  // 发送请求
  componentDidMount() {
+    axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
      console.log(res.data.data)
      this.setState({
+        banners: res.data.data.banner.list,
+        recommends: res.data.data.recommend.list
      })
    })
  }
}

子组件

js
export default class MainBanner extends Component {
+  constructor(props) {
+    super(props)
    this.state = {

    }
  }
  render() {
+    const { banners, recommends } = this.props
    return (
      <div>
        <div>轮播图</div>
        <ul>
          {
+            banners.map((item, index) => {
              return (
                <li className="item" key={item.acm}>
                  <img src={item.image} alt="" />
                </li>
              )
            })
          }
        </ul>
      </div>
    )
  }
}

函数组件

image-20230318104602159

propTypes,defaultProps

propTypes

对于传递给子组件的数据,有时候我们可能希望进行类型验证,特别是对于大型项目来说:

  • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证

  • 但是,即使我们没有使用Flow或者TypeScript,也可以通过prop-types库来进行参数验证

从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库

js
import PropTypes from 'prop-types'

MainBanner.propTypes = {
 banners: PropTypes.array.isRequired
}

错误警告:

image-20230318164619282

更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

  • 比如验证数组,并且数组中包含哪些元素;

  • 比如验证对象,并且对象中包含哪些key以及value是什么类型;

  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired

defaultProps

如果没有传递,我们希望有默认值呢?

  • 我们使用defaultProps就可以了
js
MainBanner.defaultProps = {
 banners: [] 
}

组件通信-子传父

某些情况,我们也需要子组件向父组件传递消息:

  • vue中是通过自定义事件来完成的;

  • React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;

我们这里来完成一个案例:

  • 将计数器案例进行拆解;

  • 将按钮封装到子组件中:CounterButton;

  • CounterButton发生点击事件,将内容传递到父组件中,修改counter的值;

这里省略了代码,参考上课代码

image-20230318172320072

父组件

js
export default class App extends Component {
  constructor() {
    super()
    this.state = {
      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
        <div className="count">当前计数:{count}</div>
        <hr />
        {/* 1. 在父组件中传递一个函数体cbChangeCount到子组件中 */}
+        <Counter cbChangeCount={count => this.cbChangeCount(count)}/>
      </div>
    )
  }

  // 2. 定义该函数体cbChangeCount
+  cbChangeCount(count) {
    this.setState({ count: this.state.count + count })
  }
}

子组件

js
export default class Counter extends Component {
  render() {
    return (
      <div>
        {/* 3. 子组件中点击执行函数changeCount */}
+        <button onClick={ e => this.changeCount(1)}> +1 </button>
        <button onClick={ e => this.changeCount(5)}> +5 </button>
        <button onClick={ e => this.changeCount(10)}> +10 </button>
      </div>
    )
  }

  // 4. 定义函数changeCount
  changeCount(count) {
    const { cbChangeCount } = this.props
    // 5. 在子组件中执行父组件中传递过来的回调函数cbChangeCount
+    cbChangeCount(count)
  }
}

案例:TabControl

image-20230318104721162

1、展示界面

父组件

js
export default class App extends Component {
  constructor() {
    super()
    this.state = {
      tabs: ['流行', '新歌', '精选'],
      index: 0
    }
  }
  render() {
    const { tabs, index } = this.state
    return (
      <div>
+        <TabControl tabs={tabs}/>
        <div className='content'>{tabs[index]}</div>
      </div>
    )
  }
}

子组件

js
export default class TabControl extends Component {
  render() {
+    const { tabs } = this.props 
    return (
      <div className='tab-control'>
        {
+          tabs.map((item, index) => {
            return (
              <div className="item" key={item}>
                <span className='text'>{item}</span>
              </div>
            )
          })
        }
      </div>
    )
  }
}

2、初始选中第一项

js
    this.state = {
+      currIndex: 0
    }
jsx
  render() {
+    const { currIndex } = this.state
    const { tabs } = this.props 
    return (
      <div className='tab-control'>
        {
          tabs.map((item, index) => {
            return (
+              <div className={`item ${currIndex === index ? ' active' : ''}`} key={item}>
                <span className='text'>{item}</span>
              </div>
            )
          })
        }
      </div>
    )
  }

3、点击某项选中该项

jsx
            return (
              <div  className={`item ${currIndex === index ? ' active' : ''}`} 
              		key={item} 
+              		onClick={e => this.changeTab(index)}>
                <span className='text'>{item}</span>
              </div>
            )
js
  changeTab(index) {
+    this.setState({ currIndex: index })
  }

4、点击某项展示某项的内容

子组件

jsx
            return (
              <div  className={`item ${currIndex === index ? ' active' : ''}`} 
              		key={item} 
+              		onClick={e => this.changeTab(index)}>
                <span className='text'>{item}</span>
              </div>
            )

在子组件调用父组件传递的回调函数cbGetIndex

js
  // 方法
  changeTab(index) {
    this.setState({ currIndex: index })

    // 传递index到父组件
+    this.props.cbGetIndex(index)
  }

父组件

js
    return (
      <div>
+        <TabControl tabs={tabs} cbGetIndex={(index) => this.setState({ index: index })}/>
+        <div className='content'>{tabs[index]}</div>
      </div>
    )

组件插槽

React中的插槽(slot)

在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。

我们应该让使用者可以决定某一块区域到底存放什么内容。

image-20230318104747645

这种需求在Vue当中有一个固定的做法是通过slot来完成的,React呢?

React对于这种需要插槽的情况非常灵活,有两种方案可以实现:

  • 组件的children子元素;

  • props属性传递React元素;

children实现插槽

每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容

父组件

js
        <NavBar>
+          <div className="back">返回</div>
+          <div className="title">默认标题</div>
+          <div className="menu">目录</div>
        </NavBar>

子组件

js
  render() {
+    const { children } = this.props
    return (
      <div className='nav-bar'>
+        <div className="item left">{children[0]}</div>
+        <div className="item center">{children[1]}</div>
+        <div className="item right">{children[2]}</div>
      </div>
    )
  }

props实现插槽

通过children实现的方案虽然可行,但是有一个弊端通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

另一个方案就是使用 props 实现通过具体的属性名,可以让我们在传入和获取时更加精准

父组件

js
  render() {
    return (
      <div>
        <NavBar 
+          leftSlot=<div className='left'>返回</div>
+          centerSlot=<div className='center'>标题</div>
+          rightSlot=<div className='right'>目录</div>
        />
      </div>
    )
  }

子组件

js
export class NavBar extends Component {
  render() {
    console.log(this.props)
+    const { leftSlot, centerSlot, rightSlot } = this.props
    return (
      <div className='nav-bar'>
+        <div className="tab left">{ leftSlot }</div>
+        <div className="tab center">{ centerSlot }</div>
+        <div className="tab right">{ rightSlot }</div>
      </div>
    )
  }
}

也可以在父组件中传递属性的时候,传入变量

js
  render() {
+    const leftEl = <div className='tab left'>返回</div>
+    const centerEl = <div className='tab center'>标题</div>
+    const rightEl = <div className='tab right'>目录</div>

    return (
      <div>
        <NavBar 
+          leftSlot={leftEl}
+          centerSlot={centerEl}
+          rightSlot={rightEl}
        />
      </div>
    )
  }

作用域插槽

含义: 组件的标签在父组件中编写,但是标签中用到的数据是子组件中的数据

image-20230323104628685

父组件:编写插槽标签的时候,同时使用子组件中传递出来的参数item

js
export class App extends Component {
  constructor() {
    super()
    this.state = {
      tabs: ['首页', '分类', '喜欢', '我的']
    }
  }

+  getSlot(tab) {
+    return <button className='text'>{tab}</button>
+  }

  render() {
    const { tabs } = this.state
    return (
      <div>
        <TabControl 
          tabs={tabs}
+          // tabText={tab => <button className='text'>{tab}</button>}
+          tabText={tab => this.getSlot(tab)}
        />
      </div>
    )
  }
}

子组件:使用父组件传递的插槽,同时传递参数item到组件外部

js
export class TabControl extends Component {
  render() {
+    const { tabs, tabText } = this.props
    return (
      <div className='tab-control'>
        {
          tabs.map((item, index) => {
            return (
+              <div className="tab active" key={item}>{tabText(item)}</div>
            )
          })
        }
      </div>
    )
  }
}

限制单个元素

可以通过 PropTypes.element 来确保传递给组件的 children 中只包含一个元素

js
    import PropTypes from 'prop-types';

    class MyComponent extends React.Component {
      render() {
        // 这必须只有一个元素,否则控制台会打印警告。
        const children = this.props.children;
        return (
          <div>
            {children}
          </div>
        );
      }
    }

    MyComponent.propTypes = {
+      children: PropTypes.element.isRequired
    };

非父子通信

Context应用场景

非父子组件数据的共享:

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

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

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

属性展开

我们实现一个一层层传递的案例:

我这边顺便补充一个小的知识点:Spread Attributes

App.jsx

js
export class App extends Component {
  constructor() {
    super()
    this.state = {
+      userInfo: {
+        name: "Jack",
+        age: 17,
+        gender: 'male'
+      }
    }
  }
  render() {
    const { userInfo } = this.state
    return (
      <div>
+        <Home {...userInfo}/>
      </div>
    )
  }
}

Home.jsx

js
export class Home extends Component {
  render() {
    return (
      <div>
+        <HomeBanner {...this.props}/>
      </div>
    )
  }
}

HomeBanner.jsx

js
export class HomeBanner extends Component {
  render() {
    console.log(this.props)
+    const { name, age, gender } = this.props
    return (
      <div>
+        <div className="name">姓名:{name}</div>
+        <div className="age">年龄:{age}</div>
+        <div className="gender">性别:{gender}</div>
      </div>
    )
  }
}

但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

  • React提供了一个API:Context;

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

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

Context-API

React.createContext

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

Context.Provider

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

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

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

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

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

js
// 语法
<MyContext.Provider value={/* 某个值 */}>

Class.contextType

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

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

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

js
// 语法
class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基于 MyContext 组件的值进行渲染 */
  }
}
MyClass.contextType = MyContext;

Context.Consumer

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

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

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

image-20230318105041692

jsx
// 语法
<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

Context基础-类组件

1、在单独模块创建ThemeContext

js
import React from 'react'
export const InfoContext = React.createContext()

2、在父组件使用 InfoContext.Provider 包裹要传递下去的组件,通过value属性为后代提供数据

js
+ import { InfoContext } from './context'
export class App extends Component {
  constructor() {
    super()
    this.state = {
+      info: {
+        name: 'Jack',
+        age: 18,
+        gender: 'male'
+      }
    }
  }
  render() {
    const { info } = this.state
    return (
      <div>
+        <InfoContext.Provider value={info}>
+          <Home />
+        </InfoContext.Provider>
      </div>
    )
  }
}

3、在子孙组件中,设置组件的contextType,然后获取this.context

js
+  import { InfoContext } from '../context/index'

  export class HomeBanner extends Component {
    render() {
      console.log(this.context)
+      const { name, age, gender } = this.context
      return (
        <div>
+          {/* 使用ClassName.contextType,然后通过this.context获取共享数据 */}
+          <div className="name">姓名:{name}</div>
+          <div className="age">年龄:{age}</div>
+          <div className="gender">性别:{gender}</div>
        </div>
      )
    }
  }

+  HomeBanner.contextType = InfoContext

Context基础-函数组件

什么时候使用Context.Consumer呢?

  • 1.当使用value的组件是一个函数式组件时;

  • 2.当组件中需要使用多个Context时;

1、父组件中的操作和类组件一样

2、在子孙组件中,用法如下:

js
+  import { InfoContext, TabsContext } from '../context/index'

  function HomeProduct(props) {
    return (
      <div>
+        <TabsContext.Consumer>
          {
+            value => {
              return (
                <div className="tabs">
                  {
+                    value.map(item => {
+                      return <div className="tab" key={item}>{item}</div>
+                    })
                  }
                </div>
              )
            }
          }
+        </TabsContext.Consumer>
      </div>
    )
  }
  export default HomeProduct

Context基础-共享多个数据

父组件:嵌套多个数据

js
  <InfoContext.Provider value={info}>
    <TabsContext.Provider value={tabs}>
      <Home />
    </TabsContext.Provider>
  </InfoContext.Provider>

子孙组件:由于组件只能设置一个contextType ,所以其他的context就需要通过 <XxxContext.Consumer> 的value来获取

js
+  import { InfoContext, TabsContext } from '../context/index'

  export class HomeBanner extends Component {
    render() {
      console.log(this.context)
+      const { name, age, gender } = this.context
      return (
        <div>
+          {/* 1. 使用ClassName.contextType,然后通过this.context获取共享数据 */}
          <div className="name">姓名:{name}</div>
          <div className="age">年龄:{age}</div>
          <div className="gender">性别:{gender}</div>

+          {/* 2. 使用TabsContext.Consumer */}
+          <TabsContext.Consumer>
            {
+              value => {
+                return <div>{value}</div>
+              }
            }
+          </TabsContext.Consumer>
        </div>
      )
    }
  }

+  HomeBanner.contextType = InfoContext

函数子孙组件

通过<XxxContext.Consumer> 的value来获取

Context基础-默认值

什么时候使用默认值defaultValue呢?

js
import React from 'react'
export const TabsContext = React.createContext({tabs: ['默认首页', '默认分类', '默认我的']})

App.jsx

js
    return (
      <div>
        <InfoContext.Provider value={info}>
          <TabsContext.Provider value={tabs}>
            <Home />
          </TabsContext.Provider>
        </InfoContext.Provider>
        <hr />
+        {/* 注意:Profile没有被InfoContext包裹 */}
+        <Profile />
      </div>
    )

子组件和其他的一样

EventBus事件总线

安装 hy-event-store

1、创建 eventBus

image-20230321175749223

2、发射事件,并传递参数

image-20230321175924176

3、监听事件,取消监听(常在生命函数中监听)

image-20231104113242895

image-20230321181311616

image-20231104114130128

setState详解

为什么使用setState

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

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

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

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

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

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

image-20230318105119361

setState用法-基本使用

image-20230322112820123

注意: setState在内部是通过 Object.assign(this.state, newState) 来合并2个state

setState用法-传入回调函数

好处:

  • 1、可以在回调函数中编写新state的处理逻辑
  • 2、当前的回调函数会将之前的state和props传递进来
js
    this.setState((state, props) => {
      // 注意:此处的state,props是修改前的数据
      console.log(state, props)
        
      return {
        msg: '你好~' + state.msg // 你好~Hello
      }
    })

setState用法-异步调用

1、setState在React的事件处理中是一个异步调用

js
  changeText() {
    this.setState({ msg: '你好~' })
	// 修改完msg后立即获取msg,此时的msg获取的值依然是之前的数据
+    console.log('立即获取:', this.state.msg) // 立即获取:Hello
  }

2、如果希望在数据更新之后(数据合并),获取到对应的结果执行一些逻辑代码,可以在setState中传入第二个参数:callback

js
  changeText() {
    this.setState({ msg: '你好~' }, () => {
+      console.log('回调结果:', this.state.msg) // 回调结果:你好~
    })

    console.log('立即获取:', this.state.msg) // 立即获取:Hello
  }

setState异步更新

setState的更新是异步的?

  • 最终打印结果是Hello World;

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

image-20230318105136380

为什么setState设计为异步呢?

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

  • React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的同学可以参考一下;

https://github.com/facebook/react/issues/11527#issuecomment-360199710

我对其回答做一个简单的总结:

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

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

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

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

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

*批量更新:*执行了3次setState,但是render函数只执行了1次

js
  increment() {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 })
  }

  render() {
    // 注意:执行了3次setState,但是render函数只执行了1次
    console.log('0000000000')
    const { count } = this.state
    return (
      <div>
        <div>当前计数:{count}</div>
        <button onClick={e => this.increment()}> 增加 </button>
      </div>
    )
  }

解释:在一次点击中执行3次setState()函数,此时会执行批量修改counter,会将这3次setState合并到一次然后一起修改counter。这里就不能使用this.state.counter 来获取counter的值,必须使用传递的参数state获取counter

如何获取异步的结果

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

方式一:setState的回调

  • setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;

  • 格式如下:setState(partialState, callback)

js
  // 获取异步更新
  getAsyncUpdate() {
    this.setState({ msg: '你好~' }, () => {
+      console.log(this.state.msg) // 你好~
    })
  }

方式二:我们也可以在生命周期函数:

js
  // 生命周期
  componentDidUpdate() {
    console.log('生命周期: ', this.state.msg) // 你好~
  }

setState一定是异步吗?(React18之前)

验证一:在setTimeout中的更新:

image-20230318105241656

验证二:原生DOM事件:

image-20230318105249181

其实分成两种情况:

➢ 在组件生命周期或React合成事件中,setState是异步;

➢ 在setTimeout原生dom事件Promise中,setState是同步

setState默认是异步的(React18之后)

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

image-20230318105315615

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

js
+  import { flushSync } from 'react-dom'

 // 2. 获取同步更新:flush
  getSyncUpdate() {
+    flushSync(() => {
+      this.setState({ msg: '我们的世界~' })
+    })
    console.log('同步获取:', this.state.msg) // 我们的世界~
  }

性能优化SCU

React更新机制

我们在前面已经学习React的渲染流程

image-20230318105658533

那么React的更新流程呢?

image-20230318105704001

React的更新流程

  • 1、React在propsstate发生改变时,会调用React的render方法,会创建一颗不同的

  • 2、React需要基于两颗不同的之间的差别来判断如何有效的更新UI:**

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

    • https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf

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

      • 这个开销太过昂贵了,React的更新性能会变得非常低效;
  • 3、于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

    • 同层节点之间相互比较,不会跨节点比较;

      • 不同类型的节点,产生不同的树结构;

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

key的优化

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

image-20230318105736403

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

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

方式二:在前面插入数据

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

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

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

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

key的注意事项:

  • key应该是唯一的;

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

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

render函数被调用

image-20230318105811968

在App中通过setState修改了state.message,此时会重新执行render函数,虽然<Home><Recommend> 组件中没有内容发生变化,但是也会一起重新渲染

js
export class App extends Component {
  constructor() {
    super()
    this.state = {
+      msg: 'Hello'
    }
  }

  callRender() {
      // 1. 此处msg的值:没有变化
+    this.setState({ msg: 'Hello' })
  }

  render() {
    // 2. msg值没有变化,依然执行了App的render函数
    console.log('App - render - 被调用~')
    const { msg } = this.state
    return (
      <div>
        <div>{msg}</div>
        <button onClick={e => this.callRender()}>render函数被调用</button>
		{/* 3. Home,Profile组件没有用到msg,也重新执行了render函数 */}
        <Home />
        <Profile />
      </div>
    )
  }
}

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

  • 在App中,我们增加了一个计数器的代码;

  • 当点击+1时,会重新调用App的render函数;

  • 而当App的render函数被调用时,所有的子组件的render函数都会被重新调用

那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的:

  • 事实上,很多的组件没有必须要重新render

  • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法

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

  • 通过shouldComponentUpdate方法即可;
js
  shouldComponentUpdate(newProp, newState) {
    if(newState.msg !== this.state.msg) {
      return true
    } else {
      return false
    }
  }
js
  shouldComponentUpdate(newProp, newState) {
    if(newProp.msg !== this.props.msg) {
      return true
    } else {
      return false
    }
  }

shouldComponentUpdate

React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:

该方法有两个参数:

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

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

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

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

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

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

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

  • jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染;

  • 但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了;

PureComponent

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

  • 我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?

  • props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;

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

  • 将class继承自PureComponent
js
+ export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      msg: 'Hello'
    }
  }

  changeText() {
    // 1. 点击后msg值发生了变化,就会调用render
+    this.setState({ msg: '你好~' })
  }

  render() {
+    console.log('App Render')
    const { msg } = this.state
    return (
      <div>
        {/* 3. render函数中没有依赖msg,依然会调用render */}
+        {/* <div>{msg}</div> */}
        <button onClick={e => this.changeText()}>修改文本</button>
        <hr />
        {/* 2. 子组件没有依赖父组件,所以props值没有变化,就不会调用render */}
+        <Home />
+        <Profile />
      </div>
    )
  }
}

注意:

  • PureComponent只是进行了浅层比较,没有进行深层比较

image-20240722091315593

shallowEqual方法

这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),这个shallowEqual就是进行浅层比较:

image-20230318105937109

高阶组件memo

目前我们是针对类组件可以使用PureComponent,那么函数式组件呢?

  • 事实上函数式组件我们在props没有改变时,也是不希望其重新渲染其DOM树结构的

我们需要使用一个高阶组件memo

  • 我们将之前的Header、Banner、ProductList都通过memo函数进行一层包裹;

  • Footer没有使用memo函数进行包裹;

  • 最终的效果是,当counter发生改变时,Header、Banner、ProductList的函数不会重新执行;

  • 而Footer的函数会被重新执行;

js
import { memo } from 'react'

const Cate = memo(function(props) {
  console.log('Cate render')
  return <div>Cate</div>
})

export default Cate

不可变数据的力量

在state为对象或数组引用类型时,不要局部修改这些对象和数组,如果要修改就需要整个修改掉,这样就会生成一个新的对象或数组,在渲染时比较state是否改变时就会判断为不能的对象,从而重新渲染

1、修改浅层数据

数据:books和friend

js
    this.state = {
      books: [
        { id: 0, name: '⟪三国演义⟫', pubDate: '2006-2', price: '86.00', count: 1 },
        { id: 1, name: '⟪水浒传⟫', pubDate: '2007-3', price: '76.00', count: 1 },
        { id: 2, name: '⟪西游记⟫', pubDate: '2002-11', price: '45.00', count: 1 },
        { id: 3, name: '⟪红楼梦⟫', pubDate: '2003-8', price: '78.00', count: 1 }
      ]
    }

修改时必须如此修改:

js
  // 1. 添加书籍
  addBook() {
    const book = { id: 4, name: '⟪聊斋志异⟫', pubDate: '2000-8', price: '48.00', count: 2 }
+    const newBooks = [...this.state.books, book]
+    this.setState({ books: newBooks })
  }

image-20230323135028236

2、修改深层数据

下面的写法可以保证render函数会被执行

js
  // 2. 添加书籍数量
  addCount(index) {
    const newBooks = [...this.state.books]
    newBooks[index].count ++
    this.setState({ books: newBooks })
  }

问题: 如果深层的数据没有发生改变的话,会不会依然触发render函数?

回答:依然会触发render函数

js
  // 3. 深层数据没有变化
  updateName(index) {
    const newBooks = [...this.state.books]
    newBooks[index].name = '⟪三国演义⟫'
    this.setState({ books: newBooks })
  }

  render() {
    console.log('App render')
  }

ref获取原生DOM、组件实例

如何使用ref

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

  • 管理焦点文本选择媒体播放
  • 触发强制动画
  • 集成第三方 DOM 库

我们可以通过refs获取DOM;

ref获取DOM

如何创建refs来获取对应的DOM呢?目前有三种方式:

方式一:传入字符串

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

注意: refs已被废弃,不推荐

js
  getDOMByRefs() {
    // 1. 通过refs获取DOM
+    console.log(this.refs.msg)
  }

  render() {
    const { msg } = this.state
    return (
      <div>
+        <div ref="msg">{msg}</div>
        <button onClick={e => this.getDOMByRefs()}>获取DOM-refs</button>
      </div>
    )
  }

方式二:传入一个对象推荐

  • 对象是通过 React.createRef() 方式创建出来的;

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

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

export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      msg: 'Hello'
    }
+    this.msgRef = createRef()
  }

  getDOMByCreateRef() {
+    console.log(this.msgRef.current)
  }

  render() {
    const { msg } = this.state
    return (
      <div>
+        <div ref={this.msgRef}>{msg}</div>
        <button onClick={e => this.getDOMByCreateRef()}>获取DOM-createRef</button>
      </div>
    )
  }
}

import React, { createRef } from 'react'
export class App extends PureComponent {
  constructor() {
    this.state = { }
    this.msgRef = createRef()
  }
  ...
  render() {
    const { msg } = this.state
    return <div ref={this.msgRef}>{msg}</div>
  }

方式三:传入一个函数

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

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

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      msg: 'Hello'
    }
+    this.msgRef = null
  }

  // 3. 通过ref传入函数
  getDOMByFunction() {
+    console.log(this.msgRef)
  }

  render() {
    const { msg } = this.state
    return (
      <div>
+        <div ref={el => this.msgRef = el}>{msg}</div>
        <button onClick={e => this.getDOMByFunction()}>获取DOM</button>
      </div>
    )
  }
}

ref获取类组件实例

应用: 获取到组件实例后,可以通过组件实例调用组件中的方法

js
+  test() {
+    console.log('Home test方法~')
+  }

  render() {
    return (
      <div>Home</div>
    )
  }

获取组件实例

js
+ import React, { createRef, PureComponent } from 'react'
import Home from './cpns/Home'
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      msg: 'Hello'
    }
+    this.homeRef = createRef()
  }

  // 3. 通过ref传入函数
  getCpnDOM() {
    console.log(this.homeRef.current)
+    const homeRef = this.homeRef.current
+    // 调用组件中的方法
+    homeRef.test()
  }

  render() {
    const { msg } = this.state
    return (
      <div>
        <div>{msg}</div>
        <button onClick={e => this.getCpnDOM()}>获取组件DOM</button>
+        <Home ref={this.homeRef} />
      </div>
    )
  }
}

ref的类型

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

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

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

  • 你不能在函数组件上使用 ref 属性,因为他们没有实例;

这里我们演示一下ref引用一个class组件对象:

image-20230318110044532

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

  • 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;

  • 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;

image-20230322171732201

父组件

js
+ import React, { createRef, PureComponent } from 'react'
import Profile from './cpns/Profile'
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      msg: 'Hello'
    }
+    this.profileRef = createRef()
  }

  // 3. 通过ref传入函数
  getFuncCpnRef() {
+    console.log(this.profileRef.current)
  }

  render() {
    const { msg } = this.state
    return (
      <div>
        <div>{msg}</div>
        <button onClick={e => this.getFuncCpnRef()}>获取函数组件ref</button>
+        <Profile ref={this.profileRef}/>
      </div>
    )
  }
}

子组件

js
+    const Profile = React.forwardRef(function(props, ref) {
      return (
        <div>
          <div>Profile</div>
+          <div className="name" ref={ref}>我的名字</div>
        </div>
      )
    })
    
    const Profile = React.forwardRef(function(props, ref) {
      return (
        <div>
          <div>Profile</div>
          <div className="name" ref={ref}>我的名字</div>
        </div>
      )
    })

受控和非受控组件

认识受控组件

在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state

比如下面的HTML表单元素:

image-20230318110102187

  • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;

  • 在React中,并没有禁止这个行为,它依然是有效的;

  • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;

  • 实现这种效果的标准方式是使用“受控组件”;

受控组件-input

HTML 中,表单元素(如<input><textarea><select>)通常自己维护 state,并根据用户输入进行更新。

而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新

  • 我们将两者结合起来,使React的state成为“唯一数据源”;

  • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作

  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。

由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

image-20230318110126352

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      msg: '你好~'
    }
  }
  render() {
    const { msg } = this.state
    return (
      <div>
        {/* 1. 受控组件 */}
+        <input type="text" value={msg} onChange={e => this.inputChange(e)}/>

        {/* 2. 非受控组件 */}
+        <input type="text" defaultValue={msg} />
      </div>
    )
  }

+  inputChange(e) {
    console.log(e.target.value);
+    const val = e.target.value
+    this.setState({ msg: val })
  }
}

受控组件-form提交

1、自己提交form表单

js
// form提交表单
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      username: 'Tom',
      pass: '111111'
    }
  }
  render() {
    const { username, pass } = this.state
    return (
      <div>
+        <form onSubmit={e => this.hdlSubmit(e)}>
+          <label htmlFor="username">
            <span>用户名:</span>
+            <input id='username' type="text" name="username" value={username} onChange={e => this.hdlInputChange(e)}/>
          </label>
+          <label htmlFor="pass">
            <span>密码:</span>
+            <input type="password" id="pass" name="pass" value={pass} onChange={e => this.hdlInputChange(e)} />
          </label>
        </form>
      </div>
    )
  }

  // 处理表单提交
+  hdlSubmit(e) {
    // 1. 阻止表单默认提交行为
+    e.preventDefault()
    // 2. 获取所用表单数据,并组织数据
    console.log(this.state)
    // 3. 发送网路请求,发送数据到服务器(ajax, axiox, fetch。。。) 
  }

  // 绑定input事件 - text/password/
+  hdlInputChange(e) {
+    this.setState({ [e.target.name]: e.target.value }, () => {
      console.log(this.state)
    })
  }
}

2、多个受控组件同一个处理函数

步骤:

  • 1、在input标签中添加name属性
  • 2、在onChange处理函数中通过 [e.target.name]: e.target.value 动态设置state
html
        <form onSubmit={e => this.hdlSubmit(e)}>
          <label htmlFor="username">
            <span>用户名:</span>
+            <input id='username' type="text" name="username" value={username} onChange={e => this.hdlInputChange(e)}/>
          </label>
          <label htmlFor="pass">
            <span>密码:</span>
+            <input type="password" id="pass" name="pass" value={pass} onChange={e => this.hdlInputChange(e)} />
          </label>
        </form>

[e.target.name]: e.target.value

js
  // 绑定input事件 - text/password/
  hdlInputChange(e) {
+    this.setState({ [e.target.name]: e.target.value }, () => {
      console.log(this.state)
    })
  }

受控组件-checkbox-单选、多选

1、checkbox单选

js
  render() {
    const { isAgree, hobbies } = this.state
    return (
        {/* 1. checkbox单选 */}
        <label htmlFor="agree">
          <input
            type="checkbox"
            id="agree"
+            checked={isAgree}
+            onChange={(e) => this.hdlCheckbox(e)}
          />
          <span>同意</span>
        </label>
  }
  hdlCheckbox(e) {
+    this.setState({ isAgree: e.target.checked })
  }

2、checkbox多选

image-20230328172220799

js
  constructor() {
    super()
    this.state = {
+      hobbies: [
+        { text: '篮球', value: 'basketball', isChecked: false },
+        { text: '足球', value: 'football', isChecked: false },
+        { text: '网球', value: 'tennis', isChecked: false }
+      ]
    }
  }
  render() {
    const { hobbies } = this.state
    return (
        {/* 2. checkbox多选 */}
        <span>爱好:</span>
        {hobbies.map((item, index) => {
          return (
            <label htmlFor={item.value} key={item.value}>
              <input
                type="checkbox"
                id={item.value}
+                checked={item.isChecked}
+                onChange={(e) => this.hdlMultiCheckbox(e, index)}
              />
              <span>{item.text}</span>
            </label>
          )
    )
  }
  // 处理checkbox多选
+  hdlMultiCheckbox(e, index) {
+    console.log(e.target.checked)
+    const newHobbies = [...this.state.hobbies]
+    newHobbies[index].isChecked = e.target.checked
+    this.setState({ hobbies: newHobbies })
+  }

受控组件-select-单选、多选

select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。

1、select单选

js
// select组件
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      city: '合肥'
    }
  }
  render() {
    const { city } = this.state
    return (
      <div>
        <form onSubmit={e => this.hdlSubmit(e)}>
+          <select value={city} onChange={e => this.hdlSelectChange(e)}>
+            <option value="hf">合肥</option>
+            <option value="sh">上海</option>
+            <option value="bj">北京</option>
+          </select>
        </form>
      </div>
    )
  }

  // 事件处理
  hdlSubmit(e) {
    e.preventDefault()
  }
+  hdlSelectChange(e) {
+    console.log(e.target.value)
+    this.setState({ city: e.target.value })
+  }
}

2、select多选

多选的数据要是数组类型

js
  constructor() {
    super()
    this.state = {
+      city: ['合肥']
    }
  }

select需要添加 multiple 属性

html
+          <select value={city} multiple onChange={e => this.hdlSelectChange(e)}>
            <option value="hf">合肥</option>
            <option value="sh">上海</option>
            <option value="bj">北京</option>
          </select>

需要通过 e.target.selectedOptions 获取选中的项,同时需转化为Array并遍历获取value

js
  hdlSelectChange(e) {
    const city = Array.from(e.target.selectedOptions).map(item => item.value)
    this.setState({ city })
  }

3、补充:Array.from(可迭代对象)

  • Array.from(HTMLCollection)
  • Array.from(arguments)

Array.from的第二参数,可以传入一个map回调函数

js
  hdlSelectChange(e) {
    // const city = Array.from(e.target.selectedOptions).map(item => item.value)
+    const city = Array.from(e.target.selectedOptions, item => item.value)
    this.setState({ city })
  }

非受控组件

React推荐大多数情况下使用 受控组件 来处理表单数据:

  • 一个受控组件中,表单数据是由 React 组件来管理的;

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

如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据

  • 我们来进行一个简单的演练:

  • 使用ref来获取input元素;

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

同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

1、设置默认值

通过 defaultValuedefaultChecked 设置默认值

js
  constructor() {
    super()
    this.state = {
+      msg: '默认值',
+      isAgree: true
    }
  }
  render() {
    const { msg, isAgree } = this.state
    return (
      <div>
        <form onSubmit={e => this.hdlSubmit(e)}>
          <div>
            非受控组件:
+            <input type="text" defaultValue={msg} />
+            <input type="checkbox" defaultChecked={isAgree} />
          </div>
        </form>
      </div>
    )
  }

2、通过ref获取表单数据

绑定ref

js
  constructor() {
    super()
    this.state = {
      msg: '默认值',
      isAgree: true
    }
+    this.msgRef = createRef()
  }
  render() {
    const { msg, isAgree } = this.state
    return (
      <div>
        <form onSubmit={e => this.hdlSubmit(e)}>
          <div>
            非受控:
+            <input type="text" defaultValue={msg} ref={this.msgRef}/>
            <input type="checkbox" defaultChecked={isAgree} />
          </div>
        </form>
      </div>
    )
  }

原生监听input的变化

js
+  componentDidMount() {
    // 获取input的数据
    console.log(this.msgRef.current.value)
    // 监听input的变化
+    this.msgRef.current.addEventListener('change', e => {
+      console.log(e.target.value)
+    })
  }

获取input的数据

js
console.log(this.msgRef.current.value)

高阶组件

认识高阶函数

什么是高阶组件呢?

  • 相信很多同学都知道(听说过?),也用过 高阶函数

  • 它们非常相似,所以我们可以先来回顾一下什么是 高阶函数。

1、高阶函数

高阶函数的维基百科定义:至少满足以下条件之一:

  • 接受一个或多个函数作为输入

  • 输出一个函数

js
function foo(fn) {
  setTimeout(() => {
      fn()
  }, 2000) 
} 
foo(function () {})
js
function foo() {
    function bar() {}
    return bar
}
const fn = foo()

JavaScript中比较常见的filtermapreduce都是高阶函数。

2、高阶组件

那么什么是高阶组件呢?

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

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

我们可以进行如下的解析:

  • 首先, 高阶组件 本身不是一个组件,而是一个函数

  • 其次,这个函数的参数是一个组件返回值也是一个组件

高阶组件的定义

1、高阶组件的调用过程类似于这样:

js
  // HelloWorld组件
+  class HelloWorld extends PureComponent {
    render() {
      return (<h3>Hello World</h3>)  
    }
  }
+  const HelloWorldHOC = hoc(HelloWorld)

2、高阶组件的定义过程类似于这样:

增强组件Cpn

js
  // 高阶组件
+  function hoc(Cpn) {
+    class NewCpn extends PureComponent {
//    return class extends PureComponent {
      render() {
        return (
          <div>
            <span>高阶组件</span>
+            <Cpn name="mr" />
          </div>
        )
      }
    }
+    NewCpn.displayName = 'PluginDisplayName'
+    return NewCpn
  }

3、组件的名称问题:

  • 在ES6中,类表达式中类名是可以省略的;

  • 组件的名称都可以通过displayName来修改;

image-20230323172750605

高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

高阶组件在一些React第三方库中非常常见:

  • 比如redux中的connect;(后续会讲到)
  • 比如react-router中的withRouter;(后续会讲到)

应用-props增强

不修改原有代码的情况下,添加新的props

1、定义普通组件

定义组件Home

js
export class Home extends PureComponent {
  render() {
    return (
      <div className='cpn'>
        <h3>Home</h3>
        <div>姓名:{this.props.name}</div>
        <div>年龄:{this.props.age}</div>
        <div>性别:{this.props.gender}</div>
      </div>
    )
  }
}
export default Home

APP调用组件Home

js
import React, { PureComponent } from 'react'
import Home from './cpns/Home'
import Profile from './cpns/Profile'


export class App extends PureComponent {
  render() {
    return (
      <div>
+        <Home />
+        <Profile />
      </div>
    )
  }
}

2、定义高阶组件

解释:

  • {...this.props} 是传递调用时传过来的属性
  • {...this.state.userinfo} 是属性增强
js
  import { PureComponent } from "react";

+  export default function hocUserinfo(Cpn) {
+    class NewCpn extends PureComponent {
      constructor(props) {
        super(props)
        this.state = {
+          userinfo: {
+            name: 'Tom',
+             age: 17,
+            gender: 'male'
+          }
        }
      }
      render() {
        return (
+          <Cpn {...this.props} {...this.state.userinfo} />
        )
      }

    }
+    return NewCpn
  }

3、调用高阶组件

js
export default hocUserinfo(Home)

4、通用函数

image-20230318110419270

应用-Context优化

1、创建Context

js
import { createContext } from "react";
const ThemeCtx = createContext()
export default ThemeCtx

2、提供数据

js
export class App extends PureComponent {
  render() {
    return (
      <div>
        App
+        <ThemeCtx.Provider value={{color: 'red', size: 30}}>
+          <Home />
+          <Profile />
+        </ThemeCtx.Provider>
      </div>
    )
  }
}

3、获取数据

js
export class Home extends PureComponent {
  render() {
    return (
      <div>
+        <ThemeCtx.Consumer>
          {
+            value => {
+              return (
+                <h3 style={{color: value.color, size: value.size + 'px'}}>Home</h3>
              )
            }
          }
+        </ThemeCtx.Consumer>
      </div>
    )
  }
}

4、优化: 定义高阶组件 withTheme 优化Context

解释:

  • {...props} 接收 <Home {...props}> 的属性
  • {...value} 接收 <ThemeCtx.Provider value={ {color: 'red', size: 30} } > 的value属性值
js
  import ThemeCtx from "../context/ThemeCtx"

  function withTheme(Cpn) {
    // 1. 返回匿名函数组件
    return function (props) {
      return (
        // 2. 在函数组件中使用Consumer获取context数据
+        <ThemeCtx.Consumer>
          {
+            value => {
               // 3. 传递数据到目标组件的props中
+              return <Cpn {...props} {...value}/>
            }
          }
+        </ThemeCtx.Consumer>
      )
    }
  }

  export default withTheme

5、*优化:*使用高阶组件,获取数据

js
export class Profile extends PureComponent {
  render() {
    return (
      <div>
        <h3>Profile</h3>
         // 2. 在子孙组件中获取Provider中的传递的数据
+        <div>color: {this.props.color}</div>
+        <div>size: {this.props.size}</div>
      </div>
    )
  }
}
  // 1. 使用withTheme高阶函数
+ export default withTheme(Profile)

image-20230328223443288

应用-登录鉴权

1、在开发中,我们可能遇到这样的场景

  • 某些页面是必须用户登录成功才能进行进入;

  • 如果用户没有登录成功,那么直接跳转到登录页面;

js
  constructor() {
    super()
    this.state = {
+      isLogin: false
    }
  }
  render() {
    const { isLogin } = this.state
    return (
      <div>
+        <div>{ isLogin ? <Home /> : '请先登录~' }</div>
      </div>
    )
  }

2、这个时候,我们就可以使用高阶组件来完成登录鉴权操作:

js
function hocLogin(Cpn) {
  return props => {
+    const token = localStorage.getItem('token')

+    if(token) {
+      return <Cpn {...props}/>
    }else {
+      return '请先登录~'
    }
  }
}

export default hocLogin

3、导出<Home /> 组件时,先调用 hocLogin 高阶函数

js
export class Home extends PureComponent {
  render() {
    return (
      <div>
        Home
      </div>
    )
  }
}

+ export default hocLogin(Home)

4、直接使用<Home/> 组件,无需判断

js
  render() {
    return (
      <div>
+        <Home />
      </div>
    )
  }

5、添加token,强制update

js
export class App extends PureComponent {
  render() {
    return (
      <div>
+        <button onClick={e => this.addToken()}>添加token</button>
        <Home />
      </div>
    )
  }

  addToken() {
+    localStorage.setItem('token', '1234')
+    this.forceUpdate()
  }
}

应用-生命周期劫持

我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

1、定义高阶组件:实现生命周期劫持

js
function hocGetRenderTime(Cpn) {
  return class extends PureComponent {
+    UNSAFE_componentWillMount() {
+      this.start = Date.now()
    }

+    componentDidMount() {
+      this.end = Date.now()
+      const time = this.end - this.start
+      console.log('渲染时间:' + time + 'ms')
    }

    render() {
+      return <Cpn {...this.props}/>
    }
  }
}

export default hocGetRenderTime

2、使用高阶组件

js
export class Detail extends PureComponent {
  render() {
    return (
      <div>Detail</div>
    )
  }
}

+ export default hocGetRenderTime(Detail)

高阶函数的意义

我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理。

1、mixin的缺点

其实早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护

  • 不同的Mixin中的方法可能会相互冲突

  • Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;

2、HOC的缺点

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;

  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突

3、Hooks的优点

Hooks的出现,是开创性的,它解决了很多React之前的存在的问题

  • 比如this指向问题、比如hoc的嵌套复杂度问题等等;

后续我们还会专门来学习hooks相关的知识,敬请期待;

应用-forwardRef

在前面我们学习ref时讲过,ref不能应用于函数式组件:

  • 因为函数式组件没有实例,所以不能获取到对应的组件对象

但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?

  • 方式一:直接传入ref属性(错误的做法)

  • 方式二:通过forwardRef高阶函数;

父组件

js
import Profile from './cpns/Profile'

export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      msg: 'Hello'
    }
+    this.profileRef = createRef()
  }

  // 3. 通过ref传入函数
  getFuncCpnRef() {
    console.log(this.profileRef.current)
  }

  render() {
    const { msg } = this.state
    return (
      <div>
        <div>{msg}</div>
        <button onClick={e => this.getFuncCpnRef()}>获取函数组件ref</button>
+        <Profile ref={this.profileRef}/>
      </div>
    )
  }
}

export default App

函数组件

js
+ const Profile = React.forwardRef(function(props, ref) {
  return (
    <div>
      <div>Profile</div>
+      <div className="name" ref={ref}>我的名字</div>
    </div>
  )
})

export default Profile

应用-memo

Portal、Fragment

createPortal()

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点。

然而某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。

html
    <div id="root"></div>
+    <div id="mr"></div>

语法

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

js
ReactDOM.createPortal(child, container)

参数

  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;

  • 第二个参数(container)是一个 DOM 元素;

基本使用

js
+ import { createPortal } from 'react-dom'
import Home from './cpns/Home'
export class App extends PureComponent {
  render() {
    return (
      <div>
        <div>App</div>
        {
+          createPortal(<Home />, document.querySelector('#mr'))
        }
      </div>
    )
  }
}

export default App

image-20230404151423115

image-20230318110545869

案例:Modal组件

比如说,我们准备开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置

1、修改index.html添加新的节点

html
    <div id="root"></div>
+    <div id="modal"></div>

2、编写这个节点的样式(屏幕居中)

css
#modal {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: #f00;
}

3、编写组件Modal代码

js
import { PureComponent } from 'react'
import { createPortal } from 'react-dom'

export class Modal extends PureComponent {
  render() {
    return createPortal(this.props.children, document.querySelector('#modal'))
  }
}

export default Modal

4、使用Modal组件

js
    <Modal>
        <div>我是标题</div>
    	<div>我是文章内容</div>
    </Modal>

image-20230323220937812

Fragment

在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素:

image-20230318110632279

我们又希望可以不渲染这个div应该如何操作呢?

  • 使用Fragment

  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点

React还提供了Fragment的短语法

  • 它看起来像空标签 <> </>

  • 但是,如果我们需要在Fragment中添加key,那么就不能使用短语法

1、语法

js
render() {
  return (
+    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
+    </React.Fragment>
  );
}

render() {
  return (
+    <Fragment>
      <ChildA />
      <ChildB />
+    </Fragment>
  );
}

image-20230323222000826

2、短语法

js
class Columns extends React.Component {
  render() {
    return (
+      <>
        <td>Hello</td>
        <td>World</td>
+      </>
    );
  }
}

3、带key的Fragment

使用显式 <React.Fragment> 语法声明的片段可能具有 key。一个使用场景是将一个集合映射到一个 Fragments 数组 - 举个例子,创建一个描述列表:

key 是唯一可以传递给 Fragment 的属性

js
function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        // 没有`key`,React 会发出一个关键警告
       	// 有key的情况下,React.Fragment不能使用短语法
+        <React.Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
+        </React.Fragment>
      ))}
    </dl>
  );
}

严格模式StrictMode

StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具:

  • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI

  • 它为其后代元素触发额外的检查和警告

  • 严格模式检查仅在开发模式下运行;它们不会影响生产构建;

可以为应用程序的任何部分启用严格模式:

  • 不会对 Header 和 Footer 组件运行严格模式检查;

  • 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查;

image-20230318110651636

严格模式检查的是什么?

但是检测,到底检测什么呢?

1、识别不安全的生命周期

js
export class Content extends PureComponent {
  // 1. 检测不安全的生命周期  
  UNSAFE_componentWillMount() {
	console.log('UNSAFE_componentWillMount')
  }
}

image-20230404160502152

2、识别过时的ref API

js
  // 2. 检测过时的ref API
  getRef() {
+    console.log(this.refs.contentRef)
  }

  render() {
    return (
      <Fragment>
+        <div ref="contentRef">Content</div>
        <hr />
        <button onClick={this.getRef()}>获取Ref</button>
      </Fragment>
    )
  }

image-20230404160911100

3、检查意外的副作用

  • 这个组件的constructor等生命周期函数会被调用两次

  • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用

  • 在生产环境中,是不会被调用两次的;

image-20230324131839689

4、使用废弃的findDOMNode方法

  • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了,可以自行学习演练一下

5、检测过时的context API

  • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的;

  • 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法;