为什么使用箭头函数和bind容易出现问题,react性能优化

## User.jsimport React from ‘react‘;// Note how the debugger below gets hit when *any* delete// button is clicked. Why? Because the parent component// uses an arrow function, which means this component//class User extends React.PureComponent { render() { const {name, onDeleteClick } = this.props console.log(`${name} just rendered`); return ( lt;ligt; lt;input type="button" value="Delete" onClick={onDeleteClick} /gt; {name} lt;/ligt; ); }}export default User;

应该怎么做?

先来看个例子比较一下在 render 中不使用箭头函数的差异。

点击 CodeSandbox 查看和运行完整 demo。

import React from 'react';
import { render } from 'react-dom';
import User from './User';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      users: [
        { id: 1, name: 'Cory' }, 
        { id: 2, name: 'Meg' }, 
        { id: 3, name: 'Bob'}
      ],
    };
  }

  deleteUser = id => {
    this.setState(prevState => {
      return { 
        users: prevState.users.filter(user => user.id !== id) 
      };
    });
  };

  renderUser = user => {
    return <User key={user.id} user={user} onClick={this.deleteUser} />;
  }

  render() {
    return (
      <div>
        <h1>Users</h1>
        <ul>
          {this.state.users.map(this.renderUser)}
        </ul>
      </div>
    );
  }
}

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

在这个例子中,index.js 的 render 中没有箭头函数了。相关的数据被传到 User.js:

import React from "react";
import PropTypes from "prop-types";

// Note that the console.log below isn't called
// when delete is clicked on a user.
// That's because pureComponent's shallow
// comparison works properly here because
// the parent component isn't passing down
// an arrow function (which would cause this
// component to see a new function on each render)
class User extends React.PureComponent {
  onDeleteClick = () => {
    // No bind needed since we can compose the relevant data for this item here
    this.props.onClick(this.props.user.id);
  };

  render() {
    console.log(`${name} just rendered`);
    return (
      <li>
        <input 
          type="button" 
          value="Delete" 
          onClick={this.onDeleteClick} 
        />
        {this.props.user.name}
      </li>
    );
  }
}

User.propTypes = {
  user: PropTypes.object.isRequired,
  onClick: PropTypes.func.isRequired
};

export default User;

在 User.js 中,onDeleteClick 调用了在 props 中传递的 onClick 函数,并传递了相应的 user.id。

当你再次点击 delete 按钮时,其他的用户再也不会调用 render 了!

Chrome Performance

在开发模式下, 在支持的浏览器内使用性能工具可以直观的了解组件何时挂载,更新和卸载

  • 打开Chrome开发工具Performance 标签页点击Record
  • 执行你想要分析的动作。不要记录超过20s,不然Chrome可能会挂起。
  • 为什么使用箭头函数和bind容易出现问题,react性能优化。停止记录。
  • React事件将会被归类在 User Timing标签下。
    图片 1

在onDeleteClick={() = this.deleteUser(user.id)}这一行中,我们使用一个箭头函数来传递value到deleteUser 函数中。这就是问题所在了。

原因在于:父组件在 props 中传递了一个箭头函数。箭头函数在每次 render 时都会重新分配(和使用 bind 的方式相同)。所以,尽管我将 User 声明为 PureComponent,User 的父组件中的箭头函数导致 User 组件为所有的用户实例传递了一个新的函数。所以当点击任何删除按钮时,每个用户实例都会重新 render。

Virtual DOM

react引入了一个叫做虚拟DOM的概念,安插在JavaScript逻辑和实际的DOM之间。这一概念提高了Web性能。在UI渲染过程中,React通过在虚拟DOM中的微操作来实对现实际DOM的局部更新。

在Web开发中,我们总需要将变化的数据实时反应到UI上,这时就需要对DOM进行操作。而复杂或频繁的DOM操作通常是性能瓶颈产生的原因,React为此引入了虚拟DOM(Virtual DOM)的机制:在浏览器端用Javascript实现了一套DOM API。基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。而且React能够批处理虚拟DOM的刷新,在一个事件循环(Event Loop)内的两次数据变化会被合并,例如你连续的先将节点内容从A变成B,然后又从B变成A,React会认为UI不发生任何变化,而如果通过手动控制,这种逻辑通常是极其复杂的。尽管每一次都需要构造完整的虚拟DOM树,但是因为虚拟DOM是内存数据,性能是极高的,而对实际DOM进行操作的仅仅是Diff部分,因而能达到提高性能的目的。这样,在保证性能的同时,开发者将不再需要关注某个数据的变化如何更新到一个或多个具体的DOM元素,而只需要关心在任意一个数据状态下,整个界面是如何Render的。

看一看User.js文件。每当我登录的时候控制台都会打印出渲染执行时的console结果。我已经定义User为PureComponent。所以只有当props或者state修改时才会重新渲染User。但是当你点击删除的时候,发现render在所有User实例中触发了。

总结

为了最佳性能:

  1. 避免在 render 中使用箭头函数和绑定。
  2. 怎么做?提取子组件,或者直接传递数据给 HTML 元素
  3. eslint中加入对.bind方法和箭头函数的检测,以及解决之道请参考No .bind() or Arrow Functions in JSX Props (react/jsx-no-bind)

render

react的组件渲染分为初始化渲染和更新渲染。

  • 初始化渲染
    • 在初始化渲染的时候会调用根组件下的所有组件的render方法进行渲染
  • 更新渲染
    • 当我们要更新某个子组件的时候,我们期待的是只变化需要变化的组件,其他组件保持不变。
    • 但是,react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比,如不变则不进行更新。这样的render和虚拟DOM的对比明显是在浪费

来源:

import React from 'react';
import { render } from 'react-dom';
import User from './User';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      users: [
        { id: 1, name: 'Cory' }, 
        { id: 2, name: 'Meg' }, 
        { id: 3, name: 'Bob' }
      ]
    };
  }

  deleteUser = id => {
    this.setState(prevState => {
      return { 
        users: prevState.users.filter( user => user.id !== id)
      }
    })
  }

  render() {
    return (
      <div>
        <h1>Users</h1>
        <ul>
        { 
          this.state.users.map( user => {
            return <User 
              key={user.id} 
              name={user.name} 
              onDeleteClick={() => this.deleteUser(user.id)} />
          })
        }
        </ul>
      </div>
    );
  }
}

export default App;

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

一些概念

怎么会这个样子?因为()=this.deleteUser(user.id)每执行一次就会生成一个新的函数,当然bind也是这样干的,所以在PureComponent的shallowCompare中认为onDeleteClick的值已经被修改,所以触发了重新渲染。看吧,使用箭头函数和bind会造成性能浪费,作为一个节约的程序员应该避免如此。

import React from 'react';

// Note how the debugger below gets hit when *any* delete
// button is clicked. Why? Because the parent component
// uses an arrow function, which means this component
//
class User extends React.PureComponent {
  render() {
    const {name, onDeleteClick } = this.props
    console.log(`${name} just rendered`);
    return (
      <li>             
        <input 
          type="button" 
          value="Delete" 
          onClick={onDeleteClick} 
        /> 
        {name}
      </li>
    );
  }
}

export default User;

突变的数据

大多数情况PureComponent都可以解决,但是之前也说过,他是“浅比较”,如果遇到数据结构比较复杂,他是无法识别的。

class PureCom extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      items: [1, 2, 3],
      title: 'pure',
    }
    this.add = this.add.bind(this);
  }
  add() {
    let { items } = this.state;
    items.push(23);
    this.setState({ items })
  }
  render() {
    console.log('pure render')
    return (
      <div>
        <Title title={this.state.title} />
        <ul>
          {this.state.items.map((e, i) => {
            return <li key={i}>{e}</li>
          })}
        </ul>
        <button onClick={this.add}>add</button>
      </div>
    )
  }
}
  • 点击add,你会发现没有任何反应,为什么呢?因为你setState的items其实是和state里面的items指向相同引用。原理和下面一样。
let a={val:1};
let b=a;
b.val=2;
console.log(a)//{val:2}
console.log(b)//{val:2}
  • 解决办法

    • 1.深拷贝
    add() {
    let items =JSON.parse(JSON.stringify(this.state.items));//黑科技
    //或者let items=deepCopy(this.state.items);
    items.push(23);
    this.setState({ items })
    }
    
    • 2.数组使用concat,对象使用Object.assign()
    add() {
    let { items } = this.state;
    items=items.concat(23)  //此时的items是一个新数组
    this.setState({ items })
    }
    
    • 3.使用不可变数据Immutable.js
    add() {
    let { items } = this.state;
    items = update(items, { $push: [23] });
    this.setState({ items })
    }
    
    • 其中深拷贝如果数据比较复杂消耗会比较大
    • concat,Object.assign用起来很快捷
    • 如果你数据比较复杂,可能Immutable会是最好的选择。官方推荐::seamless-immutable 和immutability-helper。

      redux

      个人感觉redux的渲染机制也是和PureComponent类似的,都是浅比较,因此上面的3种解决办法也适用于redux.

      16.3+ new API

      一些生命周期会被删除,将在17.0:删除componentWillMount,componentWillReceiveProps和componentWillUpdate。

  • 一些变化

    • componentWillMount => componentDidMount
    • componentWillReceiveProps => getDerivedStateFromProps
    • componentWillUpdate => getSnapshotBeforeUpdate
  • static getDerivedStateFromProps
//代替componentWillReceiveProps,因为是静态方法,不能访问到 this,避免了一些可能有副作用的逻辑,比如访问 DOM 等等
//会在第一次挂载和重绘的时候都会调用到,因此你基本不用在constructor里根据传入的props来setState
static getDerivedStateFromProps(nextProps, prevState) {
  console.log(nextProps, prevState)
  if (prevState.music !== nextProps.music) {
    return {
      music: nextProps.music,
      music_file: music_file,
      index:prevState.index+1
    };
   //document.getElementById('PLAYER').load();                   //这里不对,应该放在getSnapshotBeforeUpdate 和 componentDidUpdate
  }
  return null;
}


getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.state.music != prevState.music) {                   //进行aduio的重载
        return true
    }
    return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {       
    if (snapshot !== null) {
        document.getElementById('PLAYER').load();             //重载
    }
}
  • getSnapshotBeforeUpdate
//新的getSnapshotBeforeUpdate生命周期在更新之前被调用(例如,在DOM被更新之前)。此生命周期的返回值将作为第三个参数传递给componentDidUpdate。 (这个生命周期不是经常需要的,但可以用于在恢复期间手动保存滚动位置的情况。)

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {              //snapshot
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}
  • 使用componentDidMount 代替 componentWillMount
//有一个常见的错误观念认为,在componentWillMount中提取可以避免第一个空的渲染。在实践中,这从来都不是真的,因为React总是在componentWillMount之后立即执行渲染。如果数据在componentWillMount触发的时间内不可用,则无论你在哪里提取数据,第一个渲染仍将显示加载状态。
// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._asyncRequest = asyncLoadData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({ externalData });
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

时间: 2018-12-22阅读: 464标签: react

参考

React + Redux 性能优化(一):理论篇

其他

  • props尽量只传需要的数据,避免多余的更新
  • 组件尽量解耦,比如一个input+list组建,可以将list分成一个PureComponent,只在list数据变化是更新
  • 如果组件有复用,key值非常重要。因此key的优化,如果有唯一id,尽量不使用循环得到的index
  • 暂时这些
import React from ‘react‘;import { render } from ‘react-dom‘;import User from ‘./User‘;class App extends React.Component { constructor(props) { super(props); this.state = { users: [ { id: 1, name: ‘Cory‘ }, { id: 2, name: ‘Meg‘ }, { id: 3, name: ‘Bob‘} ], }; } deleteUser = id =gt; { this.setState(prevState =gt; { return { users: prevState.users.filter(user =gt; user.id !== id) }; }); }; renderUser = user =gt; { return lt;User key={user.id} user={user} onClick={this.deleteUser} /gt;; } render() { return ( lt;divgt; lt;h1gt;Userslt;/h1gt; lt;ulgt; {this.state.users.map(this.renderUser)} lt;/ulgt; lt;/divgt; ); }}render(lt;App /gt;, document.getElementById(‘root‘));

每次 render 调用时,控制台上都会打印日志。User 已经被声明为 PureComponent。所以 User 应该只在 props 或者 state 改变时才会重新 render。但是,当你点击 delete 按钮时,对于每一个 User 实例,都会调用 render

优化

上面的例子就没有箭头函数了。这里面使用了闭包的概念,把user传递下去了。

在 35 行中,我使用了一个箭头函数将一个值传递给了 deleteUser 函数,这就是问题的所在。

bind函数

绑定this的方式:一般有下面几种方式

  • constructor中绑定
constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); //构造函数中绑定
}
//然后可以
<p onClick={this.handleClick}>
  • 使用时绑定
<p onClick={this.handleClick.bind(this)}>
  • 箭头函数
<p onClick={() => { this.handleClick() }}>
  • 哪个好呢

    • 答案是第一种方式。
    • 因为第一种,构造函数每一次渲染的时候只会执行 一遍;
    • 而第二种方法,在每次render()的时候都会重新执行一遍函数;
    • 第三种方法的话,每一次render()的时候,都会生成一个新的箭头函数

      shouldComponentUpdate

      shouldComponentUpdate是决定react组件什么时候能够不重新渲染的函数,返回true时更新,false时不更新。默认返回true,即每次重新渲染,因此我们可以重写个函数从而达到"个性化定制更新"的效果。

  • 栗子

class Title extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    console.log('title render')
    return (
      <div>{this.props.title}</div>
    )
  }
}

class PureCom extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: 'pure',
      num: 0
    }
    this.add = this.add.bind(this);
  }
  add() {
    let { num } = this.state;
    num++;
    this.setState({ num })
  }
  render() {
    console.log('pure render')
    return (
      <div>
        <Title title={this.state.title} />
        <p>{this.state.num}</p>
        <button onClick={this.add}>add</button>
      </div>
    )
  }
}
  • 现在每次点击add按钮,父组件state的num都会+1,而title是一直不变的,通过console我们却发现,Title组件也在一直render,这就是因为shouldComponentUpdate默认返回true的,也就是父组件更新之后,子组件也会更新。
  • 然后子组件是没必要更新的,所以我们重写下shouldComponentUpdate方法
class Title extends React.Component {
  constructor(props) {
    super(props)
  }
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.title != this.props.title) {
      return true     //只有title变化时才更新
    } else {
      return false
    }
  }
  render() {
    console.log('title render')
    return (
      <div>{this.props.title}</div>
    )
  }
}
  • 现在就对了,点击父组件的add按钮并没有触发Title组件的更新。

    PureComponent

    类似上面的情况其实我们经常遇到,因此react提供了PureComponent来解决类似的问题,可以让我们少写许多的shouldComponentUpdate。

class Title extends React.PureComponent {
  constructor(props) {
    super(props)
  }
  render() {
    console.log('title render')
    return (
      <div>{this.props.title}</div>
    )
  }
}
  • 用了PureComponent之后作用和之前是相同的。
  • 原理:当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较
if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps)
  || !shallowEqual(inst.state, nextState);
}

请看下面的代码

避免在 render 中使用箭头函数和绑定。否则会打破 shouldComponentUpdate 和 PureComponent 的性能优化。

最后

大家好,这里是「 TaoLand 」,这个博客主要用于记录一个菜鸟程序猿的Growth之路。这也是自己第一次做博客,希望和大家多多交流,一起成长!文章将会在下列地址同步更新……
个人博客:www.yangyuetao.cn
小程序:TaoLand

那我们应该怎样做呢?

在 render 中使用箭头函数或绑定会导致子组件重新渲染,即使 state 并没有改变。作者推荐使用提取子组件或在 HTML 元素中传递数据的方式来避免绑定。
这个例子中,我在 render 中使用一个箭头函数来绑定每个删除按钮对应的用户 ID。

刚开始写react可能只是写出来完成业务就完了,后期审查代码发现可能很多地方其实都可以优化,之前可能有些地方似是而非,在此小结一下。

## index.js import React from ‘react‘;import { render } from ‘react-dom‘;import User from ‘./User‘;class App extends React.Component { constructor(props) { super(props); this.state = { users: [ { id: 1, name: ‘Cory‘ }, { id: 2, name: ‘Meg‘ }, { id: 3, name: ‘Bob‘ } ] }; } deleteUser = id =gt; { this.setState(prevState =gt; { return { users: prevState.users.filter( user =gt; user.id !== id) } }) } render() { return ( lt;divgt; lt;h1gt;Userslt;/h1gt; lt;ulgt; { this.state.users.map( user =gt; { return lt;User key={user.id} name={user.name} onDeleteClick={() =gt; this.deleteUser(user.id)} /gt; }) } lt;/ulgt; lt;/divgt; ); }}export default App;render(lt;App /gt;, document.getElementById(‘root‘));

原文转自https://zhuanlan.zhihu.com/p/29266705

在这个例子中,我们通过使用一个箭头函数(=)来bind用户ID到每个删除按钮中。

点击 CodeSandbox 来查看及演示完整的 demo。(CodeSandbox 很酷,是一个 React 在线编辑器,可以实时编译和展示当前的界面。)

结论:

要查看为什么会这样,先来看看 User.js:

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:为什么使用箭头函数和bind容易出现问题,react性能优化

相关阅读