NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

React的思考(三)- 总结下shouldComponentUpdate

| Comments

Google来,百度去,原来网上已经有一大堆讲解shouldComponentUpdate的文章,差点就打算放弃了,为了学习精神,那我就集百家之长,小总结一下。

它的作用

首先,简单说一下shouldComponentUpdate的作用(如果你已经知道,请不要跳过,帮助我审查下有没有描述错误)

1
2
3
shouldComponentUpdate(nextProps, nextState) {
  return true;
}

extends React.Component和写Functional Stateless Component(它不能复写shouldComponentUpdate),shouldComponentUpdate默认都是返回true。

这意味着,当props或者state更新时,该组件一定会调用render方法。

也就意味着,React一定会去对比该组件节点上的VisualDOM,但是,不一定会去更新真实DOM,因为reconciliation的结果可能是相等的。(一致性对比,对比新返回的元素是否和之前的元素是否一样)

如果,你将shouldComponentUpdate复写,返回false,那么componentWillUpdate(),render()和componentDidUpdate()都不会被调用,那么该组件不会更新。

当shouldComponentUpdate返回true,这个过程是向叶子节点传递的,比如:父节点返回true,它有两个叶子节点A和B,那么A和B会被要求执行mount或者update的生命周期,如果A的shouldComponentUpdate返回false,B返回true,那么只有B更新。

React官方的ShouldComponentUpdate In Action讲解的很清楚。

逃生出口

听起来貌似很有道理,谁不希望减少无谓的计算,提高性能。

然而,我又看到这样一句话:React team called shouldComponentUpdate an “escape hatch”(逃生出口)instead of “turbo button”(涡轮增压按钮)。在 github issue Stateless functional components and shouldComponentUpdate上也有人这么说。 听上去总结起来,有两个原因:

1.维护自定义的shouldComponentUpdate成本太高,有可能加了一个新的prop,但是忘记更新shouldComponentUpdate的代码,导致bug
2.也许shouldComponentUpdate的比较计算逻辑比起直接重新render更加浪费性能

尴尬了

那这就尴尬了,这到底是写还是不写呢?当被问到这个问题的时候,永远都有一个正确但不受人欢迎的答案:“Well, it depends”(好吧,看情况)。

React.Component,PureComponent和Function

与其思考这个没有人能够给出准确答案的问题,不如我们思考怎么样结合对shouldComponentUpdate的理解,合理的写组件,设计合理的状态结构树。

大家对React.Component和Functional Stateless Component比较熟,一个就是extends React.Component,一个就是函数,前面也说了,它们的shouldComponentUpdate永远返回true。

React里还有一个顶级的组件API:PureComponent。 这个PureComponent,对shouldComponentUpdate有一个默认实现,官方称为shallow的prop和state对比。啥意思呢?就是帮你对比this.props和this.state上的第一层叶子节点的引用。

如果,某一个叶子节点里面深层的一个元素改变了,而该叶子节点本身的引用没变,shouldComponentUpdate是检查不出来的。

什么类型的组件比较适合写成PureComponent呢?比如:基础组件Button,它本身的属性就相对简单,完全可以和普通HTML的button元素相似,这样就可以将组件的属性拍平一层展现,用PureComponent正好满足shallow对比。

总结

一切不分析性能瓶颈而做的性能优化,都是无用功,shouldComponentUpdate不一定是你的性能瓶颈,但是,我们在这里讨论shouldComponentUpdate,为React组件的更新的问题开了一个头,后面介绍Redux和Object.assign还会在提到组件(不)更新的问题。

周五了,祝大家周末愉快。

React的思考(二)- 逃不开的生命周期函数之构造函数

| Comments

这一定是一个老生常谈的话题,你们就别多想了,跟我一起回顾一遍,看我说的有没有道理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }

  render() {
    return (
      <div>
        Seconds: {this.state.seconds}
      </div>
    );
  }
}

ReactDOM.render(<Timer />, mountNode);

Constructor

构造函数(或者构造器),这个概念对于熟悉基于类的面向对象语言的朋友们肯定烂熟于心,但是对于JavaScript而言,这个概念往往容易让人困惑。

在JavaScript的世界里,构造函数和普通函数没有什么区别,你一样的可以像普通函数一样调用它,但如果通过new关键字来调用函数,该函数就成为了构造函数,this指针就会指向新创建的对象。

比如:

1
2
3
4
5
6
7
8
9
function Person(){
  this.name = "CodePlayer";
  console.log(this)
}
Person()
$ Window {external: Object, chrome: Object, document: document, WPCOMSharing: Object, RecaptchaTemplates: Object}

var b = new Person()
$ Person {name: "CodePlayer"}

更多具体的解释请参考我以前的一篇博客:JavaScript渐入佳境 - 构造函数、new、原型

ES6的Class、extends、super

上面是一个ES5的例子,然而,当我们写React代码时,我们会用到ES6语法,会用到class,constructor以及super关键字,他们的作用是什么?

我们先看下面一个跟React无关的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//ES6
class Rectangle {
    constructor (width, height) {
        this.width  = width;
        this.height = height;
    }
}

//翻译成ES5
var Rectangle = function (width, height) {
    this.width = width;
    this.height = height;
};

//ES6
class Shape {

}
class Rectangle extends Shape {
    constructor (id, x, y, width, height) {
        super(id, x, y);
        this.width  = width;
        this.height = height;
    }
}

//概念性翻译成ES5
var Share = function(){

}
var Rectangle = function (id, x, y, width, height) {
    Shape.call(this, id, x, y);
    this.width  = width;
    this.height = height;
};
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.__proto__ = Shape;

简单来说,class的作用就是定义Shape和Rectangle两个function(这就是被人用烂的词,语法糖),extends的作用是定义函数Rectangle的prototype和proto属性来实现原型链的继承,super的作用是在Rectangle函数中执行Share函数,并绑定this指针。

建议查看Babel的编译结果,它是更准确的ES5转义:babel链接

如果以后有人问我,JavaScript和Java有什么关系,我不会说它们没关系,我会告诉那个人,ES6抄袭Java的语法范式。

React中的constructor

我们再来回到React上,看ES6和Babel编译后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//ES6
class Rectangle extends React.Component{

}

//不完全Babel编译代码
function Rectangle() {
  _classCallCheck(this, Rectangle);

  return _possibleConstructorReturn(
    this,
    (Rectangle.__proto__ || Object.getPrototypeOf(Rectangle))
      .apply(this, arguments) // 看这里
}

//ES6
class Rectangle extends React.Component{
    constructor (props) {
      super(props);
    }
}

//不完全Babel编译代码
function Rectangle(props) {
   _classCallCheck(this, Rectangle);

   return _possibleConstructorReturn(
     this,
     (Rectangle.__proto__ || Object.getPrototypeOf(Rectangle))
       .call(this, props) // 看这里
}

当我们创建一个组件时,如果不需要在constructor里面做任何初始化的操作时,我们是不需要复写constructor的,因为Babel编译后,会将整个arguments都绑定上this指针后传递给被Rectangle的原型(React.Component),并执行,它替我们将constructor中super()的操作做了,如上所示。

如果有需要在constructor中做初始化的操作时,那么必须带上super(props)并放在最前面,因为它的作用是调用Rectangle的原型(React.Component),并绑定this指针。

那么,哪些初始化的操作可以在constructor里面做呢?原则上只有一个,那就是初始化state。

1
2
3
4
5
6
class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }
}

有三个问题:

1.为什么不用this.setState()来执行?

原因:1.this.state够直接了,你还要怎样?2.this.setState()是一个异步执行的函数,执行完之后,组件的响应式重新渲染(render),你这第一次渲染都还没开始呢。3.在这里this.setState()是一个空指令,这么写,不会任何起作用,不信你可以试试。

2.那么能不能在constructor里面执行网络请求来初始化数据?

我问过许多来面试的候选人,你的网络请求会放置在什么时候执行,我印象中确实有人回答我说在constructor中。听起来,在constructor中获取数据来初始化挺合理的。而且确实有人问:Stack Overflow

然而,我们在官方文档上看到这样一句话:避免在构造函数中引入任何有副作用的代码(比如data fetching或者DOM manipulation)或执行订阅的操作,如果有,请在componentDidMount里执行。

关于这个位置的理解,我常常这样解释,就像你用jQuery写代码一样,你一定会等到document ready之后,才开始操作DOM或执行网络请求(也是为了操作DOM),否则,很有可能遇到undefined的情况。虽然,React这里也许和jQuery不一样,但我认为它的理由是相似的。(如果不对,请纠正我)。

3.那么可不可以传递props到state呢?非常像Java的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
constructor(props) {
  super(props);
  this.state = {
    color: props.initialColor
  };
}

//Java
public Shape {
  Shape(String color){
    this.color = color;
  }
}

这么写,没有说完全错误,但是你需要注意的是,要同时实现getDerivedStateFromProps()在React 16中,或者16之前的componentWillReceiveProps,来保证当父组件更新时,props能有传递到state,因为constructor只会执行一次。

如果出现这种使用场景,我们需要思考一下,能否将state的控制向上提升,将Shape组件仅仅作为Presentional Component,这样减少在不同的两个位置(父组件和它自己)来控制组件的状态。

this.handleClick = this.handleClick.bind(this);

官方文档说,在constructor里面只做两件事情,初始化state,和绑定event的handler函数的this指针到组件对象本身。既然是this指针这么困惑的话题,我再啰嗦一句这里做了什么:

当函数(这里指这个组件)就成为了构造函数,该函数中this指针就会指向新创建的对象,也就是constructor里面的this就是指向的它自己(该组件的实例),那么this.handleClick = this.handleClick.bind(this);就能保证在handleClick函数里面的this指针,无论handleClick被传递到了哪里,可能被基础DOM元素button使用,也可能被子组件传递到别的位置,handleClick里面的this指针都能指向该组件的(如果是下面的例子就是Toggle),这样里面的this.setState才能起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // This binding is necessary to make `this` work in the callback
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

总结

别小看一个constructor函数,这里面的知识点可多了。总结一下,就是只干这些事情,其他的别干:

1
2
3
4
5
constructor(props) {
  super(props);
  this.state = {isToggleOn: true};
  this.handleClick = this.handleClick.bind(this);
}