只要调用了this.setState(),就会调用render()更新DOM。所以如果要修改数据必须通过setState,否则可能数据发生了改变,页面内容并没有改变。

9.1setState是异步的

React组件化开发(6)- setState&性能优化(4)

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

  • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
  • 最好的办法应该是获取到多个更新,之后进行批量更新

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

  • state和props不能保持一致性,会在开发中产生很多的问题;
如何获取异步后的结果

方式一:setState的回调

  • setState函数接受两个参数,第二个参数是一个回调函数,这个回调函数会在更新后执行
  • React组件化开发(6)- setState&性能优化(4)

方式二:在componentDidUpdate函数里

因为执行componentDidUpdate后值必然是更新后的值

9.2setState不一定是异步的

什么样的情况下SetState是同步的?

1.在setTimeout里

Click() {
    setTimeout(() => {
      this.setState({
        message: "你好!"
      })
      console.log(this.state.message);
    }, 0)
  }

2.在原生DOM事件里

componentDidMount() {
    const btnClick = document.getElementById("btn");
    btnClick.addEventListener("click", () => {
      this.setState({
        message: "哈哈哈"
      })
      console.log(this.state.message);
    })
  }

其实分两种情况:

  1. 在组件生命周期函数和合成事件(比如onClick就是合成事件)里,setState是异步的
  2. 在setTimeout和原生DOM事件中,setState是同步的
9.3setState数据合并

如果this.state里有多个数据,此时setState修改了其中一个数据,是不会影响其他数据的。

源码中其实是有对 原对象 和 新对象进行合并的:使用Object.assign()

9.4setState本身的合并

React组件化开发(6)- setState&性能优化(4)

上面的代码按道理来说应该是+1+1+1 最后结果是3,但是实际上却只加了1。

如果要知道为什么只有看源码了,但其实,setState第一个参数可以是对象或者函数,如果是函数就会是这种累加的效果

React组件化开发(6)- setState&性能优化(4)

第一个参数是函数(里面传三个参数):

React组件化开发(6)- setState&性能优化(4)

perState是上一次的值。

9.5state的不可变性
import React, { Component, PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      friendList: [
        { name: "curry", age: 33 },
        { name: "kebi", age: 43 },
        { name: "James", age: 37 }
      ]
    }

  }
  render() {
    return (
      <div>
        <h2>好友列表</h2>
        <ul>
          {
            this.state.friendList.map((item, index) => {
              return (
                <li key={item.name}>
                  姓名:{item.name}
                  年龄:{item.age}
                  <button onClick={e => this.addAge(index)}>age+1</button>
                </li>
              )
            })
          }
        </ul>
        <button onClick={e => this.AddFriend()}>添加数据</button>
      </div>
    )
  }
  AddFriend() {
    // 不推荐
    // this.state.friendList.push({ name: "雷霆嘎巴", age: 20 })
    // this.setState({
    //   friendList: this.state.friendList
    // })

    // 推荐
    const newFriend = [...this.state.friendList, { name: "雷霆嘎巴", age: 20 }];
    this.setState({
      friendList: newFriend
    })
    // 为什么说state的不可变性,因为state里面的数据,没一个数据需要开辟一块空间,如果是引用数据类型,你在原来的数据里面添加东西
    // 只是添加了数据,原来的空间并没有改变,这时候进行浅比较时是相同的,
  }
  addAge(index) {
    const newAge = [...this.state.friendList];
    newAge[index].age += 1;
    this.setState({
      friendList: newAge
    })
  }
}

10.性能优化

React更新机制

jsx -> 虚拟DOM -> 真实DOM

10.1React的更新流程

React组件化开发(6)- setState&性能优化(4)

props和state在发生改变时,会调用render方法,创建一棵新的虚拟DOM树

React需要基于这两棵树之间的差异来判断如何有效的进行更新UI

  • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^3 ),其中 n 是树中元素的数量;
  • 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
  • 这个开销太过昂贵了,React的更新性能会变得非常低效

于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

  • 同层节点之间相互比较,不会垮节点比较;
  • 不同类型的节点,产生不同的树结构;
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
情况一:对比不同类型的元素

当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:

  • 当一个元素从 变成 React组件化开发(6)- setState&性能优化(4)
  • 当更新style属性时,react仅更新有所改变的属性,比如:通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。React组件化开发(6)- setState&性能优化(4)

如果时同类型的组件元素

  • 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps() 和 componentWillUpdate() 方法。
  • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归
情况三:对子节点进行递归
  • 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个

mutation。

  • 如果在最后一条插入数据的情况:前面两条是相同的所以不会产生mutation,最后一个比较产生mutation,将其插入到新的DOM树中即可;React组件化开发(6)- setState&性能优化(4)
  • 如果前面插入一条数据的情况:React会对每一个子元素产生一个mutation,而不是保持 <li>星际穿越</li>和<li>盗梦空间</li>的不变;React组件化开发(6)- setState&性能优化(4)
  • 这种低效的比较方式会带来一定的性能问题(使用keys来优化这个问题);
10.2keys的优化

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

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

key的注意事项:

  • key应该是唯一的;
  • key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
  • 使用index作为key,对性能是没有优化的;
10.3render函数被调用

之前有个嵌套案例:

  • 在App中,增加了一个计数器的代码
  • 当点击+1的时候,就会调用App的render函数;
  • 而当调用render函数后,所有子组件的render函数都会调用

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

  • 事实上,很多的组件没有必须要重新render;
  • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法;

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

  • 通过shouldComponentUpdate方法即可
10.4shouldComponentUpdate

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

该方法有两个参数:

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

该方法的返回值时一个boolean类型

  • 返回值为true,那么就需要调用render方法
  • 返回值为false,那么就不需要调用render方法
  • 默认值时true,也就是state发生改变就会调用render方法

例如下面我们修改了currentText值并不会重新调用render方法

import React, { Component } from 'react'

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 0,
      currentText: "Hello !"
    }
  }
  render() {
    console.log("App调用了render");
    return (
      <div>
        <h2>当前计数:{this.state.counter}</h2>
        <button onClick={e => this.increnment()}>+1</button>
        <button onClick={e => this.btnClick()}>点击更改文件</button>
      </div>
    )
  }
  shouldComponentUpdate(nextProps, nextState) {
    // 这里的this.state.counter是之前的值
    if (this.state.counter != nextState.counter) {
      return true;
    }

    return false;
  }
  increnment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
  btnClick() {
    this.setState({
      currentText: "你好锕"
    })
  }
}
10.5 PureComponent

上面我们只是控制了一个组件的render方法,如果每个组件都要去手动控制shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。

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

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

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

  • 将class继承自PureComponent。
  • 函数不能用PureComponent,使用memo()
import React, { Component, PureComponent } from 'react'



class Header extends PureComponent {

  render() {
    console.log("Header调用了render方法");
    return (
      <div>
        Header组件
      </div>
    )
  }
}

function ProductList() {
  return (
    <ul>
      <li>小程序</li>
      <li>H5</li>
      <li>企业官网</li>
    </ul>
  )
}



class Main extends PureComponent {
  render() {
    console.log("Main调用了render方法");
    return (
      <div>
        <h2></h2>
        <ProductList />
      </div>
    )
  }
}



class Footer extends PureComponent {
  render() {
    console.log("Footer调用了render方法");
    return (
      <div>我是底部</div>
    )
  }
}

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 0
    }
  }
  render() {
    return (
      <div>
        <h2>当前计数:{this.state.counter}</h2>
        <button onClick={e => this.btnClick()}>+1</button>
        <Header />
        <Main />
        <Footer />
      </div>
    )
  }
  btnClick() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}
10.6 memo()

需要使用一个高阶函数组件memo:

Footer组件并没有被memo包裹,所以发生点击后,Footer会重新调用render方法

import React, { Component, memo, PureComponent } from 'react'

const MemoHeader = memo(
  function Header() {
    console.log("Header调用了render方法");
    return (
      <div>
        Header组件
      </div>
    )
  }
)

function ProductList() {
  return (
    <ul>
      <li>小程序</li>
      <li>H5</li>
      <li>企业官网</li>
    </ul>
  )
}

const MemoMain = memo(
  function Main() {
    console.log("Main调用了render方法");
    return (
      <div>
        <h2></h2>
        <ProductList />
      </div>
    )
  }
)

function Footer() {
  console.log("Footer调用了render方法");
  return (
    <div>我是底部</div>
  )

}

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 0
    }
  }
  render() {
    return (
      <div>
        <h2>当前计数:{this.state.counter}</h2>
        <button onClick={e => this.btnClick()}>+1</button>
        <MemoHeader />
        <MemoMain />
        <Footer />
      </div>
    )
  }
  btnClick() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}