(React原理)setState异步更新、组件更新机制、组件性能优化、虚拟DOM与Diff算法

168 阅读8分钟

一、setState()

更新数据

  • setState()更新数据是异步的
  • 注意:使用该语法,后面的setState不要依赖前面setState的值
  • 多次调用setState,只会触发一次render重新渲染(为了性能)
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  setState() 异步更新数据
*/

class App extends React.Component {
  state = {
    count: 1
  }

  handleClick = () => {
    // 此处更新state是异步更新数据的(会更新但不是马上更新,点击一次,控制台的两个输出都是1)
    this.setState({
      count: this.state.count + 1 // 1 + 1
    })
    console.log('count:', this.state.count) // 1
    this.setState({
      count: this.state.count + 1 // 1 + 1
    })
    console.log('count:', this.state.count) // 1
  }

  render() {
    console.log('render') //多次调用setState,只会触发一次render
    return (
      <div>
        <h1>计数器:{this.state.count}</h1> {/*点击一次,虽然调用了两次setState,但显示2 */ }
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

推荐语法

使用 setState((state,props) => {}) 语法,参数是个回调函数,在该函数里返回更新后的状态

  • 参数state: 表示最新的state
  • 参数props: 表示最新的props 推荐语法.png
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  setState() 推荐语法
*/

class App extends React.Component {
  state = {
    count: 1
  }

  handleClick = () => {
    // 注意:这种语法也是异步更新state的:先打印 'count:1',再打印 '第二次调用:{ count:2 }'
    this.setState((state, props) => { //state:1
      return {
        count: state.count + 1 // 1 + 1
      }
    })
    this.setState((state, props) => { //参数state表示最新的state:2
      console.log('第二次调用:', state) // 2
      return {
        count: state.count + 1 // 2 + 1
      }
    })
    console.log('count:', this.state.count) // 1  这种语法也是异步更新state的
  }

  render() {
    return (
      <div>
        <h1>计数器:{this.state.count}</h1> {/*点击一次,调用了两次setState,显示3 */}
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

setState的第二个参数

  • 场景:在状态更新(页面DOM完成重新渲染)后立即执行某个操作(类似componentDidUpdate)
  • 语法:setState(update[,callback])

第二个参数.png

import React from 'react'
import ReactDOM from 'react-dom'

/* 
  setState() callback
*/

class App extends React.Component {
  state = {
    count: 1
  }

  handleClick = () => {
    this.setState(
      (state, props) => {
        return {
          count: state.count + 1
        }
      },
      // 状态更新后并且DOM重新渲染后,立即执行:
      () => {
        console.log('状态更新完成:', this.state.count) // 2
        console.log(document.getElementById('title').innerText) // 计数器:2
      }
    )
    console.log(this.state.count) // 1  先打印1,在状态更新完后,调用第二个参数的回调函数
  }

  render() {
    return (
      <div>
        <h1 id="title">计数器:{this.state.count}</h1>
        <button onClick={this.handleClick}>+1</button> 
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

二、JSX语法的转化过程

  • JSX仅仅是createElement() 方法的语法糖(简化语法)
  • JSX语法被 @babel/preset-react 插件编译为createElement() 方法
  • React 元素: 是一个对象,用来描述你希望在屏幕上看到的内容

语法糖.png

三、组件更新机制

  • setState() 的两个作用

    • 修改state
    • 更新组件
  • 过程:父组件Parent2重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)

  • 根组件更新时,所有的子组件都会更新 组件更新.png

import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件更新机制
*/

import './index.css'

// 根组件
class App extends React.Component {
  state = {
    color: '#369'
  }

  getColor() {
    return Math.floor(Math.random() * 256)  //获取随机的颜色值
  }

  changeBG = () => {
    this.setState(() => {
      return {
        color: `rgb(${this.getColor()}, ${this.getColor()}, ${this.getColor()})`   //拼接成背景色
      }
    })
  }

  render() {
    console.log('根组件')
    return (
      <div className="app" style={{ backgroundColor: this.state.color }}>
        <button onClick={this.changeBG}>根组件 - 切换颜色状态</button>
        <div className="app-wrapper">
          <Parent1 />
          <Parent2 />
        </div>
      </div>
    )
  }
}

// ------------------------左侧---------------------------

class Parent1 extends React.Component {
  state = {
    count: 0
  }

  handleClick = () => {
    this.setState(state => ({ count: state.count + 1 }))
  }
  render() {
    console.log('左侧父组件')
    return (
      <div className="parent">
        <h2>
          左侧 - 父组件1
          <button onClick={this.handleClick}>点我({this.state.count})</button>
        </h2>
        <div className="parent-wrapper">
          <Child1 />
          <Child2 />
        </div>
      </div>
    )
  }
}

class Child1 extends React.Component {
  render() {
    console.log('左侧子组件 - 1')
    return <div className="child">子组件1-1</div>
  }
}
class Child2 extends React.Component {
  render() {
    console.log('左侧子组件 - 2')
    return <div className="child">子组件1-2</div>
  }
}

// ------------------------右侧---------------------------

class Parent2 extends React.Component {
  state = {
    count: 0
  }

  handleClick = () => {
    this.setState(state => ({ count: state.count + 1 }))
  }

  render() {
    console.log('右侧父组件')
    return (
      <div className="parent">
        <h2>
          右侧 - 父组件2
          <button onClick={this.handleClick}>点我({this.state.count})</button>
        </h2>
        <div className="parent-wrapper">
          <Child3 />
          <Child4 />
        </div>
      </div>
    )
  }
}

class Child3 extends React.Component {
  render() {
    console.log('右侧子组件 - 1')
    return <div className="child">子组件2-1</div>
  }
}
class Child4 extends React.Component {
  render() {
    console.log('右侧子组件 - 2')
    return <div className="child">子组件2-2 </div>
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

四、组件性能优化

1. 减轻state

  • state只存储跟组件渲染相关的数据(比如:count/ 列表数据 /loading等)
  • 注意:不用做渲染的数据不要放在state中,如定时器id等
  • 对于这种需要在多个方法中用到的数据(跟渲染无关的),应该放到this中 减轻state.png

2. 避免不必要的重新渲染

组件更新机制:父组件更新会引起子组件也被更新,导致子组件没有任何变化时也会重新渲染

避免不必要的重新渲染:

A. 使用钩子函数 shouldComponentUpdate(nextProps, nextState)

  • 其返回值(写个条件判断)决定是否重新渲染

    • nextProps:最新的props
    • nextState:最新的状态
  • 作用:这个函数有返回值,如果返回true,代表需要重新渲染,如果返回false,代表不需要重新渲染

  • 触发时机:它是更新阶段的钩子函数,在组件重新渲染前执行

    执行顺序:shouldComponentUpdate => render(调用setState更新组件,先执行shouldComponentUpdate,若返回false,render就不会再执行,避免重新渲染)

shouldComponentUpdata.png

import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件性能优化:
*/

// 根组件
class App extends React.Component {
  state = {
    count: 0
  }

  handleClick = () => {
    this.setState(state => {
      return {
        count: state.count + 1
      }
    })
  }

  // 钩子函数
  shouldComponentUpdate(nextProps, nextState) {
    // 返回false,阻止组件重新渲染,不会执行render
    // return false

    // 最新的状态:控制台输出的值和页面显示的值相同
    console.log('最新的state:', nextState)
    // 更新前的状态:
    console.log('this.state:', this.state)

    return true
  }

  render() {
    console.log('组件更新了')
    return (
      <div>
        <h1>计数器:{this.state.count}</h1>
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

随机数案例

需求:随机生成数字,显示在页面,如果生成的数字与当前显示的数字相同,那么就不需要更新UI,反之更新UI。

1.状态是自己的:

  • 利用nextState参数来判断当前组件是否需要更新
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件性能优化:比较state
*/

class App extends React.Component {
  state = {
    number: 0
  }
// 点击事件,每次点击生成一个随机数
  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3) //Math.random()*3: 0~3之间的随机数,Math.floor()取整,最后取值为0,1,2
        }
    })
  }
  
  // 将要更新UI的时候会执行这个钩子函数
  // 因为两次生成的随机数可能相同,如果相同,不重新渲染(不执行render)
  shouldComponentUpdate(nextProps, nextState) {
    console.log('最新状态:', nextState, ', 当前状态:', this.state)

    return nextState.number !== this.state.number  //前后不相等,返回true
  }

  render() {
    console.log('render')
    return (
      <div>
        <h1>随机数:{this.state.number}</h1>
        <button onClick={this.handleClick}>重新生成</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

2.状态不是自己的:

  • 利用props参数来判断是否需要进行更新 用props,组件APP得接收props(把展示数据的h1处改成单独的子组件NumberBox,让该子组件接收一个叫number的props)
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件性能优化:
*/

// 生成随机数
class App extends React.Component {
  state = {
    number: 0
  }

  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }

  render() {
    return (
      <div>
        <NumberBox number={this.state.number} />
        <button onClick={this.handleClick}>重新生成</button>
      </div>
    )
  }
}

class NumberBox extends React.Component {
  // 将要更新UI的时候会执行这个钩子函数
  shouldComponentUpdate(nextProps) {
    console.log('最新props:', nextProps, ', 当前props:', this.props)
    // 如果当前生成的值 与 页面的值 number相同,就返回false,不更新组件
    return nextProps.number !== this.props.number

  }
  render() {
    console.log('子组件中的render') //查看是否有不必要的渲染
    return <h1>随机数:{this.props.number}</h1>
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

B.使用纯组件PureComponent

与 React.Component 功能相似

  • 区别: PureComponent 内部自动实现了 shouldComponentUpdate钩子,不需要手动比较
  • 原理:纯组件内部通过分别比对前后两次props和state的值,来决定是否重新渲染组件 只要props和state其中有一个变化了,就重新渲染;都相同就不重新渲染 PureComponent.png

实现原理

纯组件内部的对比是 shallow compare(浅层对比)

  • 对于值类型来说:浅层对比是比较两个值是否相同(直接给值类型赋值即可)

值类型比对.png

  • 引用类型:只比较对象的引用(地址)是否相同

引用类型比对.png

注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据

注意点.png

import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件性能优化:
*/

// 引用类型:
const obj = { number: 0 }
const newObj = obj
newObj.number = 2
console.log(newObj === obj) // true

// 生成随机数
class App extends React.PureComponent {
  state = {
    obj: {
      number: 0
    }
  }

  handleClick = () => {
    // 正确做法:创建新对象
    const newObj = { ...this.state.obj, number: Math.floor(Math.random() * 3) }
    this.setState(() => {
      return {
        obj: newObj
      }
    })

    // 错误演示:直接修改原始对象中属性的值
    /* const newObj = this.state.obj
    newObj.number = Math.floor(Math.random() * 3)

    this.setState(() => {
      return {
        obj: newObj
      }
    }) */
  }

  render() {
    console.log('父组件重新render')
    return (
      <div>
        <h1>随机数:{this.state.obj.number}</h1>
        <button onClick={this.handleClick}>重新生成</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

随机数案例

  1. state:
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件性能优化:
*/

// 生成随机数
class App extends React.PureComponent {
  state = {
    number: 0
  }

  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }

  render() {
    console.log('父组件中的render')
    return (
      <div>
        <h1>随机数:{this.state.number}</h1>
        <button onClick={this.handleClick}>重新生成</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))
  1. props:
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件性能优化:
*/

// 生成随机数
class App extends React.Component {
  state = {
    number: 0
  }

  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }

  render() {
    return (
      <div>
        <NumberBox number={this.state.number} />
        <button onClick={this.handleClick}>重新生成</button>
      </div>
    )
  }
}

class NumberBox extends React.PureComponent {
  render() {
    console.log('子组件中的render')
    return <h1>随机数:{this.props.number}</h1>
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

五、虚拟DOM和Diff算法(react高效的原因)

  • React更新视图的思想是:只要state变化就重新渲染视图
  • 问题:组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染吗? 不是这样的
  • 理想状态:部分更新,只更新变化的地方

虚拟DOM

React元素是虚拟DOM,本质上就是一个JS对象,用来描述你希望在屏幕上看到的内容(UI)

使用虚拟DOM,操作真实DOM之前,会把两个有差异的虚拟DOM通过Diff算法进行比较,比较出有差异的部分再去更新页面。

虚拟DOM.png

Diff算法:最小化页面重绘

执行过程

  • 初次渲染时,React会根据初始化的state(model),创建一个虚拟DOM对象(树)
  • 根据虚拟DOM生成真正的DOM,渲染到页面
  • 若有数据变化了(红色部分setState()),会重新根据新的数据,创建新的虚拟DOM对象(树)
  • 与上一次得到的虚拟DOM对象,使用Diff算法比对前后两个虚拟DOM(找不同),得到需要更新的内容
  • 最终,React只将变化的内容更新(patch)到真正的DOM中,重新渲染到页面

diff算法.png 代码演示

组件的render()调用后,根据render里的 状态和JSX结构 生成虚拟DOM对象 (render()方法的调用并不意味着浏览器进行渲染,render方法调用时意味着Diff算法开始比对了)

import React from 'react'
//引入react-dom,用于支持react操作dom
import ReactDOM from 'react-dom'

// 生成随机数
class App extends React.PureComponent {
  state = {
    number: 0
  }

  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 2)
      }
    })
  }

  // render方法的每一次调用并不都意味着浏览器中的全部重新渲染
  // render方法调用仅仅说明要执行diff算法将变化了的内容更新
  render() {
    const el = (
      <div>
        <h1>随机数:</h1>
        <p>{this.state.number}</p>
        //点击按钮后状态发生变化,会重新生成虚拟DOM
        <button onClick={this.handleClick}>重新生成</button>
      </div>
    )
    return el
  }
}
//ReactDOM.render的第一个参数:虚拟dom对象,第二个参数:要渲染到的目标容器
ReactDOM.render(<App />, document.getElementById('root'))