Skip to content

React更新机制

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

image-20230611200133852

那么React的更新流程呢?

image-20230611200158867

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属性:

image-20230611200405592

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

这种情况,有无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方法即可;

shouldComponentUpdate

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

返回值:

该方法有两个参数:

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

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

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

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

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

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

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

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

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

App.jsx

javascript
import React, { Component } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'


export class App extends Component {
  constructor() {
    super()

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  shouldComponentUpdate(nextProps, newState) {
    // App进行性能优化的点
    if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
      return true
    }
    return false
  }

  changeText() {
    this.setState({ message: "你好啊,李银河!" })
    // this.setState({ message: "Hello World" })
  }

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

  render() {
    console.log("App render")
    const { message, counter } = this.state

    return (
      <div>
        <h2>App-{message}-{counter}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <button onClick={e => this.increment()}>counter+1</button>
        <Home message={message}/>
        <Recommend counter={counter}/>
      </div>
    )
  }
}

export default App

Home.jsx

javascript
import React, { Component } from 'react'

export class Home extends Component {
  constructor(props) {
    super(props)

    this.state = {
      friends: []
    }
  }

  shouldComponentUpdate(newProps, nextState) {
    // 自己对比state是否发生改变: this.state和nextState
    if (this.props.message !== newProps.message) {
      return true
    }
    return false
  }

  render() {
    console.log("Home render")
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    )
  }
}

export default Home

Recommend.jsx

javascript
import React, { Component } from 'react'

export class Recommend extends Component {

  shouldComponentUpdate(nextProps) {
    if (this.props.counter !== nextProps.counter) {
      return true
    }
    return false
  }

  render() {
    console.log("Recommend render")
    return (
      <div>
        <h2>Recommend Page: {this.props.counter}</h2>
      </div>
    )
  }
}

export default Recommend

App的shouldComponentUpdate判断只有当message或counter发生改变时才会重写渲染App组件,在App组件中将message传给了Home组件,将counter传给了Recommend组件,点击修改文本,改变message,就会更新Home组件,Recommend组件不会重新渲染;点击counter+1按钮,counter发生改变,Recommend组件会重新渲染,而Home组件不会。

PureComponent

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

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

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

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

将class继承自PureComponent。

image-20230611203234203

shallowEqual方法

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

如果前后的props或state一样,那么返回true,取反变成false,就不会重新渲染。

如果前后的props或state不是对象或者为null,那么返回false,取反变成true,会重新渲染。

获取前后props或state的所有key,进行遍历;如果长度不一样,说明发生了改变,返回false,取反变成true,会重新渲染。

遍历旧的props或state,如果新的props或state没有旧的props或state的属性,或者旧的props或state和新的props或state不是同一个对象,那么返回false,取反变成true,会重新渲染。

image-20230611203257582

高阶组件memo

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

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

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

当counter发生改变时,Profile组件不会重新渲染。

App.jsx

javascript
import React, { PureComponent } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'


export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  // shouldComponentUpdate(nextProps, newState) {
  //   // App进行性能优化的点
  //   if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
  //     return true
  //   }
  //   return false
  // }

  changeText() {
    this.setState({ message: "你好啊,李银河!" })
    // this.setState({ message: "Hello World" })
  }

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

  render() {
    console.log("App render")
    const { message, counter } = this.state

    return (
      <div>
        <h2>App-{message}-{counter}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <button onClick={e => this.increment()}>counter+1</button>
        <Home message={message}/>
        <Recommend counter={counter}/>
        <Profile message={message}/>
      </div>
    )
  }
}

export default App

Home.jsx

javascript
import React, { PureComponent } from 'react'

export class Home extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      friends: []
    }
  }

  // shouldComponentUpdate(newProps, nextState) {
  //   // 自己对比state是否发生改变: this.state和nextState
  //   if (this.props.message !== newProps.message) {
  //     return true
  //   }
  //   return false
  // }

  render() {
    console.log("Home render")
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    )
  }
}

export default Home

Recommend.jsx

javascript
import React, { PureComponent } from 'react'

export class Recommend extends PureComponent {

  // shouldComponentUpdate(nextProps) {
  //   if (this.props.counter !== nextProps.counter) {
  //     return true
  //   }
  //   return false
  // }

  render() {
    console.log("Recommend render")
    return (
      <div>
        <h2>Recommend Page: {this.props.counter}</h2>
      </div>
    )
  }
}

export default Recommend

Profile.jsx

javascript
import { memo } from "react"

const Profile = memo(function(props) {
  console.log("profile render")
  return <h2>Profile: {props.message}</h2>
})

export default Profile

不可变数据的力量

javascript
import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      books: [
        { name: "你不知道JS", price: 99, count: 1 },
        { name: "JS高级程序设计", price: 88, count: 1 },
        { name: "React高级设计", price: 78, count: 2 },
        { name: "Vue高级设计", price: 95, count: 3 },
      ],
      friend: {
        name: "kobe"
      },
      message: "Hello World"
    }
  }

  // shouldComponentUpdate(nextProps, nextState) {
  //   shallowEqual(nextProps, this.props)
  //   shallowEqual(nextState, this.state)
  // }

  addNewBook() {
    const newBook = { name: "Angular高级设计", price: 88, count: 1 }

    // 1.直接修改原有的state, 重新设置一遍
    // 在PureComponent是不能引起重新渲染(re-render)
    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 })
  }

  addBookCount(index) {
    // this.state.books[index].count++ // 这种写法不会触发render
    const books = [...this.state.books]
    books[index].count++
    this.setState({ books: books })
  }

  render() {
    const { books } = this.state

    return (
      <div>
        <h2>数据列表</h2>
        <ul>
          {
            books.map((item, index) => {
              return (
                <li key={index}>
                  <span>name:{item.name}-price:{item.price}-count:{item.count}</span>
                  <button onClick={e => this.addBookCount(index)}>+1</button>
                </li>
              )
            })
          }
        </ul>
        <button onClick={e => this.addNewBook()}>添加新书籍</button>
      </div>
    )
  }
}

export default App

如何使用ref

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

管理焦点,文本选择或媒体播放;

触发强制动画;

集成第三方 DOM 库;

我们可以通过refs获取DOM;

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

方式一:传入字符串

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

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

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

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

方式三:传入一个函数

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

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

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

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {

    }

    this.titleRef = createRef()
    this.titleEl = null
  }

  getNativeDOM() {
    // 1.方式一: 在React元素上绑定一个ref字符串
    // console.log(this.refs.why)

    // 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素
    // console.log(this.titleRef.current)

    // 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
    console.log(this.titleEl)
  }

  render() {
    return (
      <div>
        <h2 ref="why">Hello World</h2>
        <h2 ref={this.titleRef}>你好啊,李银河</h2>
        <h2 ref={el => this.titleEl = el}>你好啊, 师姐</h2>
        <button onClick={e => this.getNativeDOM()}>获取DOM</button>
      </div>
    )
  }
}

export default App

ref的类型

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

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

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

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

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

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

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

ref获取类组件的实例

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


class HelloWorld extends PureComponent {
  test() {
    console.log("test------")
  }

  render() {
    return <h1>Hello World</h1>
  }
}

export class App extends PureComponent {
  constructor() {
    super()

    this.hwRef = createRef()
  }

  getComponent() {
    console.log(this.hwRef.current)
    this.hwRef.current.test()
  }

  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef}/>
        <button onClick={e => this.getComponent()}>获取组件实例</button>
      </div>
    )
  }
}

export default App

ref的转发

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

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

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

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

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

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


const HelloWorld = forwardRef(function(props, ref) {
  return (
    <div>
      <h1 ref={ref}>Hello World</h1>
      <p>哈哈哈</p>
    </div>
  )
})


export class App extends PureComponent {
  constructor() {
    super()

    this.hwRef = createRef()
  }

  getComponent() {
    console.log(this.hwRef.current) // <h1>Hello World</h1>
  }

  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef}/>
        <button onClick={e => this.getComponent()}>获取组件实例</button>
      </div>
    )
  }
}

export default App

认识受控组件

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

比如下面的HTML表单元素:

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

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

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

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

javascript
import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      username: "coderwhy"
    }
  }

  inputChange(event) {
    console.log("inputChange:", event.target.value)
    this.setState({ username: event.target.value })
  }

  render() {
    const { username } = this.state

    return (
      <div>
        {/* 受控组件 */}
        <input type="checkbox" value={username} onChange={e => this.inputChange(e)}/>

        {/* 非受控组件 */}
        <input type="text" />
        <h2>username: {username}</h2>
      </div>
    )
  }
}

export default App

受控组件基本演练

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进

行更新。

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

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

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

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

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

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

image-20230611221337066

自己提交form表单

javascript
import React, { PureComponent } from 'react'

export class App extends PureComponent {

  constructor() {
    super()

    this.state = {
      username: ""
    }
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为,就不会刷新浏览器
    event.preventDefault()

    // 2.获取到所有的表单数据, 对数据进行组织
    console.log("获取所有的输入内容")
    console.log(this.state.username)

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleUsernameChange(event) {
    this.setState({ username: event.target.value })
  }

  render() {
    const { username } = this.state

    return (
      <div>
        <form onSubmit={e => this.handleSubmitClick(e)}>
          {/* 1.用户名和密码,这里是htmlFor,for是个关键字,防止冲突,使用htmlFor,和className一样 */}
          <label htmlFor="username">
            用户: 
            <input 
              id='username' 
              type="text" 
              name='username' 
              value={username} 
              onChange={e => this.handleUsernameChange(e)}
            />
          </label>

          <button type='submit'>注册</button>
        </form>
      </div>
    )
  }
}

export default App

多个表单同一个函数

我们上面的表单中只有用户名,我们可以加入一个密码输入框

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

  handleUsernameChange(event) {
    this.setState({ username: event.target.value })
  }

  handlePasswordChange(event) {
    this.setState({ password: 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.handleUsernameChange(e)}
            />
          </label>
          <label htmlFor="password">
            密码: 
            <input 
              id='password' 
              type="password" 
              name='password' 
              value={password} 
              onChange={e => this.handlePasswordChange(e)}
            />
          </label>

          <button type='submit'>注册</button>
        </form>
      </div>
    )
  }
}

export default App

但是,我们会发现handleUsernameChange和handlePasswordChange里面的处理逻辑有点相似,我们可以进一步进行优化。

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

  // handleUsernameChange(event) {
  //   this.setState({ username: event.target.value })
  // }

  // handlePasswordChange(event) {
  //   this.setState({ password: event.target.value })
  // }

  // 使用计算属性名,前提是要给input绑定name
  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

受控组件的其他演练

textarea标签

texteare标签和input比较相似:

select标签

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

处理多个输入

多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:

这里我们可以使用ES6的一个语法:计算属性名(Computed property names)

checkbox的单选和多选

javascript
import React, { PureComponent } from 'react'

export class App extends PureComponent {

  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 }
      ]
    }
  }

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

  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>

          <div>
            <button type='submit'>注册</button>
          </div>
        </form>
      </div>
    )
  }
}

export default App

select的单选和多选

javascript
import React, { PureComponent } from 'react'

export class App extends PureComponent {

  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",
      fruit2: ["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) {
    this.setState({ fruit: event.target.value })
  }

  handleFruitChange2(event) {
    const options = Array.from(event.target.selectedOptions)
    const values = options.map(item => item.value)
    this.setState({ fruit2: values })

    // 额外补充: Array.from(可迭代对象)
    // Array.from(arguments) Array.from支持传入第2个参数,就是map高级函数,了解即可
    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>
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>

          {/* 4.select多选 */}
          <select value={fruit2} onChange={e => this.handleFruitChange2(e)} multiple>
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>

          <div>
            <button type='submit'>注册</button>
          </div>
        </form>
      </div>
    )
  }
}

export default App

非受控组件

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

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

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

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

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

使用ref来获取input元素;

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

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

持 defaultValue。

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

export class App extends PureComponent {

  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"],
      intro: "哈哈哈"
    }

    this.introRef = createRef()
  }

  componentDidMount() {
    // 非受控组件就是在操作DOM,开发中建议都使用受控组件
    // this.introRef.current.addEventListener
  }

  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)
    console.log("获取结果:", this.introRef.current.value)

    // 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, intro } = 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>
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>

          {/* 5.非受控组件 */}
          <input type="text" defaultValue={intro} ref={this.introRef} />

          <div>
            <button type='submit'>注册</button>
          </div>
        </form>
      </div>
    )
  }
}

export default App

认识高阶函数

什么是高阶组件呢?

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

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

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

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

输出一个函数;

JavaScript中比较常见的filter、map、reduce都是高阶函数。

那么什么是高阶组件呢?

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

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

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

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

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

高阶组件的定义

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

image-20230612092855904

高阶函数的编写过程类似于这样:

image-20230612092914668

组件的名称问题:

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

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

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

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

比如redux中的connect;(后续会讲到)

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

javascript
import React, { PureComponent } from 'react'

// 定义一个高阶组件
function hoc(Cpn) {
  // 1.定义类组件
  class NewCpn extends PureComponent {
    render() {
      {/*返回的新组件都有一个name属性*/}
      return <Cpn name="why"/>
    }
  }
  return NewCpn

  // 定义函数组件
  // function NewCpn2(props) {

  // }
  // return NewCpn2
}

class HelloWorld extends PureComponent {
  render() {
    return <h1>Hello World</h1>
  }
}

const HelloWorldHOC = hoc(HelloWorld)

export class App extends PureComponent {
  render() {
    return (
      <div>
        <HelloWorldHOC/>
      </div>
    )
  }
}

export default App

应用一 – props的增强

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

定义一个组件增强函数,参数为类组件或函数式组件,返回一个新的组件。

相当于做了拦截,给想要使用增强函数的组件加了userInfo,但是我们自己也有可能会传入一些属性,比如banners,那么也是需要传入到高阶组件中,用法是{...this.props},那么返回的新组件也会有自己传入的props。

App.jsx

javascript
import React, { PureComponent } from 'react'
import enhancedUserInfo from './hoc/enhanced_props'
import About from './pages/About'


const Home = enhancedUserInfo(function(props) {
  return <h1>Home: {props.name}-{props.level}-{props.banners}</h1>
})

const Profile = enhancedUserInfo(function(props) {
  return <h1>Profile: {props.name}-{props.level}</h1>
})

const HelloFriend = enhancedUserInfo(function(props) {
  return <h1>HelloFriend: {props.name}-{props.level}</h1>
})

export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home banners={["轮播1", "轮播2"]}/>
        <Profile/>
        <HelloFriend/>

        <About/>
      </div>
    )
  }
}

export default App

hoc/enhanced_props.js

javascript
import { PureComponent } from 'react'

// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
  class NewComponent extends PureComponent {
    constructor(props) {
      super(props)

      this.state = {
        userInfo: {
          name: "coderwhy",
          level: 99
        }
      }
    }

    render() {
      return <OriginComponent {...this.props} {...this.state.userInfo}/>
    }
  }

  return NewComponent
}

export default enhancedUserInfo

pages/About.jsx

javascript
import React, { PureComponent } from 'react'
import enhancedUserInfo from '../hoc/enhanced_props'

export class About extends PureComponent {
  render() {
    return (
      <div>About: {this.props.name}</div>
    )
  }
}

export default enhancedUserInfo(About)

利用高阶组件来共享Context

App.jsx

javascript
import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './pages/Product'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{color: "red", size: 30}}>
          <Product/>
        </ThemeContext.Provider>
      </div>
    )
  }
}

export default App

context/theme_context.js

javascript
import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext

pages/Product.jsx

javascript
import React, { PureComponent } from 'react'
import ThemeContext from '../context/theme_context'
import withTheme from '../hoc/with_theme'

// export class Product extends PureComponent {
//   render() {
//     return (
//       <div>
//         Product:
//         <ThemeContext.Consumer>
//           {
//             value => {
//               return <h2>theme:{value.color}-{value.size}</h2>
//             }
//           }
//         </ThemeContext.Consumer>
//       </div>
//     )
//   }
// }

// export default Product

export class Product extends PureComponent {
  render() {
    const { color, size } = this.props

    return (
      <div>
        <h2>Product: {color}-{size}</h2>
      </div>
    )
  }
}

export default withTheme(Product)

hoc/with_theme.js

javascript
import ThemeContext from "../context/theme_context"

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

export default withTheme

应用二 – 渲染判断鉴权

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

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

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

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

App.jsx

javascript
import React, { PureComponent } from 'react'
import Cart from './pages/Cart'

export class App extends PureComponent {
  constructor() {
    super()

    // this.state = {
    //   isLogin: false
    // }
  }

  loginClick() {
    localStorage.setItem("token", "coderwhy")

    // this.setState({ isLogin: true })
    this.forceUpdate() // 强制刷新,开发中不推荐
  }

  render() {
    return (
      <div>
        App
        <button onClick={e => this.loginClick()}>登录</button>
        <Cart/>
      </div>
    )
  }
}

export default App

pages/Cart.jsx

javascript
import React, { PureComponent } from 'react'
import loginAuth from '../hoc/login_auth'

export class Cart extends PureComponent {
  render() {
    return (
      <h2>Cart Page</h2>
    )
  }
}

export default loginAuth(Cart)

hoc/login_auth.js

javascript
function loginAuth(OriginComponent) {
  return props => {
    // 从localStorage中获取token
    const token = localStorage.getItem("token")

    if (token) {
      return <OriginComponent {...props}/>
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>
    }
  }
}

export default loginAuth

应用三 – 生命周期劫持

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

查看每个页面渲染花了多少时间。

App.sjx

javascript
import React, { PureComponent } from 'react'
import Detail from './pages/Detail'

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

export default App

pages/Detail.jsx

javascript
import React, { PureComponent } from 'react'
import logRenderTime from '../hoc/log_render_time'

export class Detail extends PureComponent {
  // UNSAFE_componentWillMount() {
  //   this.beginTime = new Date().getTime()
  // }

  // componentDidMount() {
  //   this.endTime = new Date().getTime()
  //   const interval = this.endTime - this.beginTime
  //   console.log(`当前页面花费了${interval}ms渲染完成!`)
  // }

  render() {
    return (
      <div>
        <h2>Detail Page</h2>
        <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 default logRenderTime(Detail)

hoc/log_render_time.js

javascript
import { PureComponent } from "react";

function logRenderTime(OriginComponent) {
  return class extends PureComponent { // 匿名类
    UNSAFE_componentWillMount() {// 不推荐使用这个生命周期,这里只是为了测试
      this.beginTime = new Date().getTime()
    }
  
    componentDidMount() {
      this.endTime = new Date().getTime()
      const interval = this.endTime - this.beginTime
      // OriginComponent.name可以获取传进来的组件的名字
      console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`)
    }

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

export default logRenderTime

高阶函数的意义

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

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

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

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

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

当然,HOC也有自己的一些缺陷:

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

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

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

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

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

Portals的使用

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

元素上的)。

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

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

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

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

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

Modal组件案例

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

步骤一:修改index.html添加新的节点

public/index.html

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

步骤二:编写这个节点的样式

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

步骤三:编写组件代码

App.jsx

javascript
import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"
import Modal from './Modal'

export class App extends PureComponent {
  render() {
    return (
      <div className='app'>
        <h1>App H1</h1>
        {
          createPortal(<h2>App H2</h2>, document.querySelector("#why"))
        }

        {/* 2.Modal组件 */}
        <Modal>
          <h2>我是标题</h2>
          <p>我是内容, 哈哈哈</p>
        </Modal>
      </div>
    )
  }
}

export default App

Modal.jsx

javascript
import React, { 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

fragment

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

image-20230612105628950

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

使用Fragment

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

React还提供了Fragment的短语法:

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

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

javascript
import React, { PureComponent, Fragment } from 'react'

export class App extends PureComponent {
  constructor() {
    super() 

    this.state = {
      sections: [
        { title: "哈哈哈", content: "我是内容, 哈哈哈" },
        { title: "呵呵呵", content: "我是内容, 呵呵呵" },
        { title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
        { title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
      ]
    }
  }

  render() {
    const { sections } = this.state

    return (
      <>
        <h2>我是App的标题</h2>
        <p>我是App的内容, 哈哈哈哈</p>
        <hr />

        {
          sections.map(item => {
            return (
              {/*有key的情况下Fragment不能省略*/}
              <Fragment key={item.title}>
                <h2>{item.title}</h2>
                <p>{item.content}</p>
              </Fragment>
            )
          })
        }
      </>
    )
  }
}

export default App

StrictMode

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

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

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

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

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

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

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

image-20230612111027152

严格模式检查的是什么?

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

1.识别不安全的生命周期:

2.使用过时的ref API

3.检查意外的副作用

这个组件的constructor会被调用两次;

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

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

4.使用废弃的findDOMNode方法

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

5.检测过时的context API

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

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

App.jsx

javascript
import React, { PureComponent, StrictMode } from 'react'
// import { findDOMNode } from "react-dom"
import Home from './pages/Home'
import Profile from './pages/Profile'

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

export default App

pages/Home.jsx

javascript
import React, { PureComponent } from 'react'

export class Home extends PureComponent {
  // UNSAFE_componentWillMount() {
  //   console.log("Home UNSAFE_componentWillMount")
  // }

  constructor(props) {
    super(props)

    console.log("Home Constructor")
  }

  componentDidMount() {
    console.log("Home componentDidMount")
  }

  render() {
    console.log("Home Render")

    return (
      <div>
        {/* <h2 ref="title">Home Title</h2> */}

        <h2>Home</h2>
      </div>
    )
  }
}

export default Home

哪个组件想开启严格模式,就在哪个组件外面包裹StrictMode即可。如果给App开启严格模式,就意味着对所有组件都开启严格模式。

严格模式下,使用过期的生命周期,比如UNSAFE_componentWillMount,还有使用过时的ref API在控制台都会有警告提示。

严格模式下,Home组件的constructor,componentDidMount和render都会执行两次,是故意执行2次的,为了检查意外的副作用。

后面安装devtools的插件,就可以看到,第2次的打印会有灰色的显示进行区分。