React学习笔记 --- React更新流程

1,959 阅读10分钟

一、 React更新机制

wZzvWR.png

React的渲染机制

根据JSX代码生成对应的ReactElement对象,也就是常说的VDOM, 在依照内部的DIFF算法来进行新旧DOM的对比,随后在界面上渲染出真实DOM

weS07F.jpg

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树

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

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

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

  1. 同层节点之间相互比较,不会跨节点比较
  2. 不同类型的节点,产生不同的树结构
  3. 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定

1.1 对比不同类型的元素

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

  • 当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程;
  • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;
  • 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法, 紧接着 componentDidMount() 方法;

wepW2n.png

因为元素由div转换为了span,所以整棵子树都会被销毁后再次重建

所以React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用;

1.2 对比同一类型的元素

当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性

  • 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps() componentWillUpdate() 方 法;

  • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;

we97OP.png

通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性

we9zSs.png

当更新 style 属性时,React 仅更新有所更变的属性

通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight

1.3 对子节点进行递归

在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation

weCAkF.png

  1. 前面两个比较是完全相同的,所以不会产生mutation
  2. 最后一个比较,产生一个mutation,将其插入到新的 DOM树中即可

we8uPH.png

React会对每一个子元素产生一个mutation,而不是保 持

星际穿越和盗梦空间的不变会被重新渲染;

这种低效的比较方式会带来一定的性能问题;

1.4 Key的作用

根据之前的例子可以知道,如果在列表的非最后一项进行插入的时候,从插入项到最后一项的每一项都需要进行重新的渲染

此时就需要给列表项添加key关键字,其作用是在列表更新的时候,尤其是插入的时候,可以尽可能的复用组件,提升diff算法的性能

在列表最后位置插入数据

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

在列表最后一项前面插入数据

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

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

  1. 如果存在key比较每一项,react就可以知道哪些元素是新插入的,还是只需要改变位置
  2. 如果是新插入的,就进行新元素的插入
  3. 如果是只需要改变位置的,react就会对元素仅仅进行位移,不需要进行任何的修改

key的注意事项:

  1. key应该是唯一的;

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

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

    因为使用index作为key的时候,在进行非最后一项插入的时候,其index都会发生改变

    例如:[klaus, Alice],其中Klaus的index是0,Alice的索引是1

    在头部插入一个John,数组就变为了['Jhon', 'Klaus', 'Alice'],

    此时Klaus的index由0变为了1,Alice的index由1变为了2

1.5 render函数调用

d7IE28.png

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

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

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

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

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

shouldComponentUpdate

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

该方法有两个参数:

  1. 参数一:nextProps 修改之后,最新的props属性
  2. 参数二:nextState 修改之后,最新的state属性
  3. 参数三: nextContext 修改之后,最新的context属性(不常用)
  4. 该方法返回值是一个boolean类型
    • 返回值为true,那么就需要调用render方法;
    • 返回值为false,那么久不需要调用render方法;
    • 默认返回的是true,也就是只要state发生改变,就会调用render方法;

示例:

需求: 此时界面中 有一个count 和 message 变量

​ 此时界面中只依赖于count变量,但是没有依赖于message变量

      所以在改变的时候,只有在count的数据发生改变的时候,改变界面中的数据

​ 如果是message中的数据发送了改变,就不渲染数据

import React, { Component } from 'react'

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

    this.state = {
      count: 0,
      message: 'Hello World'
    }
  }

  render() {
    return (
      <div>
        <div> Count: { this.state.count }</div>
        <button onClick={ () => this.increment() }> +1 </button>
        <button onClick={ () => this.changeText() }>ChangeText</button>
      </div>
    )
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.state.count !== nextState.count) {
      return true
    }

    return false
  }

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

  changeText() {
    this.setState({
      message: 'Hello React'
    })
  }
}

此时,出现一个问题

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

所以React提供了一个PureComponent类,帮助我们对其进行实现

不使用pureComponent

import React, { Component } from 'react'


class Header extends Component {
  render() {
    console.log('Header render()被调用了')
    return <h2>Header</h2>
  }
}

class Body extends Component {
  render() {
    console.log('Body render()被调用了')
    return <h3>Body</h3>
  }
}


class Footer extends Component {
  render() {
    console.log('Footer render()被调用了')
    return <h4>Footer</h4>
  }
}

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

    this.state = {
      count: 0
    }
  }

  render() {
    console.log('App render()被调用了')
    return (
      <div>
        <div>Count: { this.state.count }</div>
        <button onClick={ () => this.increment() }>increment</button>
        <Header />
        <Body />
        <Footer />
      </div>
    )
  }

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

export default App;

结果

weIYi8.gif

可以看到 初始渲染的时候,App,HeaderBody, Footer组件的render方法都被调用了

在修改App组件中的state中的方法的时候,其余的3个组件的render方法也被重新调用了

也就是说只是修改了界面中的计数器,所以App中的render方法被重新调用了

所以在render函数中的Header, Body, Footer组件也被重新渲染了,

他们的组件也就被重新渲染了

现在将所有继承自Component的类,修改为继承自PureComponent

import React, { PureComponent } from 'react'


class Header extends PureComponent {
  render() {
    console.log('Header render()被调用了')
    return <h2>Header</h2>
  }
}

class Body extends PureComponent {
  render() {
    console.log('Body render()被调用了')
    return <h3>Body</h3>
  }
}


class Footer extends PureComponent {
  render() {
    console.log('Footer render()被调用了')
    return <h4>Footer</h4>
  }
}

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

    this.state = {
      count: 0
    }
  }

  render() {
    console.log('App render()被调用了')
    return (
      <div>
        <div>Count: { this.state.count }</div>
        <button onClick={ () => this.increment() }>increment</button>
        <Header />
        <Body />
        <Footer />
      </div>
    )
  }

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

export default App;

效果

we7mGT.gif

可以看到的时候 只有App的render 被调用了,

但是其下的子组件的render没有被调用,

那是因为PureComponent中自动实现了ComponentShouldUpdate方法。

其会对新旧的props和state进行浅比较,如果不同则调用render方法,如果相同就不调用render方法

那么PureComponentComponent有什么不同呢?

reactReactBaseClasses.js

function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
// 将这个组件进行标识,标识是一个pureComponent
pureComponentPrototype.isPureReactComponent = true;

ReactReactFiberClassComponent

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  
  // 如果使用者 使用了SCU这个生命周期钩子,那么就调用用户定义的SCU
  // 如果使用者没有定义 那么SCU的值就是undefined   
  if (typeof instance.shouldComponentUpdate === 'function') {
    
    // 。。。。
    
    // 实际传入的是三个参数  
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );

    // 。。。 
      
    // 返回是否需要调用render方法进行重新渲染  
    return shouldUpdate;
  }

  // ctor 是 constructor的简写
  // 如果构造函数存在并且是一个PureComponent    
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      // 对props和state进行比较,判断是不是需要进行重新渲染
      // shallow --- 浅  这里进行的是浅比较
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  // 默认值,也就是说不是SCU的默认值是true,而是调用了SCU的这个函数的默认值会返回一个true
  return true;
}

React中的shallowEqual.js

function shallowEqual(objA: mixed, objB: mixed): boolean {
  // 如果objA 和 objB 是同一个对象, 直接返回true  
  if (is(objA, objB)) {
    return true;
  }

  // props 和 state的类型应该为object
  // 如果其中一个不是object类型的数据
  // 那么就可能是没有定义props或state,即不依赖于外部的状态变化来更新组件
  // 这样的组件是不需要进行重新渲染的
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 取出objA和objB这2个对象中所有的key进行比较 
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  // 如果这2个keys的长度不一致,肯定不同,直接返回false
  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      // objB上有没有objA上的对应属性
      // 并且对应的值是否是一致的
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

所以 推荐在以后写 类组件的时候,继承自PureComponent

写函数组件的时候,外层包裹memo函数

上述代码只是针对类组件,那么函数组件是否依旧是适用的呢?

import React, { PureComponent } from 'react'


function Header() {
  console.log('Header 被调用了')
  return <h3>Header</h3>
}

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

    this.state = {
      count: 0
    }
  }

  render() {
    console.log('App render()被调用了')
    return (
      <div>
        <div>Count: { this.state.count }</div>
        <button onClick={ () => this.increment() }>increment</button>
        <Header />
      </div>
    )
  }

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

export default App;

weHDcF.gif

可以看到的是,pureCompoent只会对类组件进行优化,其只能对类组件进行优化

如果需要对函数组件起到同样的效果,就需要使用memo函数

使用memo函数对函数组件进行封装,其会返回一个使用了shallowEqual方法的新组件

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


const Header = memo(function Header() {
  console.log('Header 被调用了')
  return <h3>Header</h3>
})

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

    this.state = {
      count: 0
    }
  }

  render() {
    console.log('App render()被调用了')
    return (
      <div>
        <div>Count: { this.state.count }</div>
        <button onClick={ () => this.increment() }>increment</button>
        <Header />
      </div>
    )
  }

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

export default App;

weHLNt.gif

由此可见,使用了memo函数封装过的函数,起到了相同的效果

上一篇 setState简单使用 下一篇 受控组件和非受控组件及ref