NO END FOR LEARNING

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

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);
}

React的思考(一)- 官网首页的信息量就挺大

| Comments

没错,这个标题有些大的,也挺抽象,给自己画了一个大饼,就看能不能给圆上。

从官网的首页开始

先看小标题

我们就从React官方网站的首页开始我们的思考。先看它的小标题:

A JavaScript library for building user interfaces。

React从一开始就将自己到底是一个什么样的存在,定义的非常的清楚。看清楚,我不是一个框架,我就是一个JavaScript的库,那我是干什么用的呢?构建用户的界面(UI),其他的乱七八糟的事情我不管。

有人可能会问,其他乱七八糟事情的是什么?比如:页面的路由,网络请求,逻辑控制器(Controller),服务等等,是不是听起来挺像前几年某A打头的框架做的事情,这里我就不点名了,大家心里都清楚,没有谁对谁错,此一时彼一时的。

为什么React是这样的一个定义呢?关于这一点,我们可以在Pete Hunt在2013年5月份写的一篇博客Why did we build React?中看到一些insight。比如: 简单摘录一段:

React isn’t an MVC framework.

React is a library for building composable user interfaces. It encourages the creation of reusable UI components which present data that changes over time.

鄙人简单理解和翻译:我不是MVC框架。React是一个用于构建可组合用户界面的库。它鼓励创建可重用的UI组件,以呈现随时间变化的数据。Pete Hunt将React的目的说的很透彻。

React官网也通过这样一句话,给自己了一个清晰的定位,并且在这个清晰的定位下,给出下面三个基本特性:Declarative,Component-Based和Learn Once, Write Anywhere。我们一个个来看:

Declarative

Declarative,声明式的,嗯呐嗯呐,这是什么意思?相信大家对“声明”这个词比较了解,比如:声明一个变量,声明一个函数。

要理解它,首先要引入另外一个东西,叫做Imperative Programming(命令式编程)。声明式编程和命令式编程,都是一种编程范式,那么他们的区别是什么?简单来说就是what和how的区别。

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式编程:告诉“机器”你想要的是什么(what),让“机器”想出如何去做(how)。

命令式编程应该大家都比较好理解,比如:操作几个变量,最后计算出你想要的结果,这里的重点在于你通过指令操作它们得到结果。那么声明式呢?举个例子,比如,SQL语句:

1
Select * from JSLibrary where name = 'React'

你没告诉SQL该怎么去搜索,只是告诉它要找到名字是React的库,对吧?

React就是采用声明式的编程范式思想,你只需要设计在不同状态下,组件应该是长什么样子,React自己会帮助完成组件的更新。它的最直接明白的对比(反面教材),就是通过jQuery操作DOM来更新UI。

React的这种开发模式和有限状态机的思想是一致的,在预知所有状态的条件下,去规划你的代码,也因此衍生了Redux, MobX这样的状态管理库。

Component-Based

基于组件的,这个相对比较好理解,组件是什么?对数据和方法的简单封装。它应该具备具有独立性,封装性,可重用性,职责单一,有自己的状态等等。React组件就封装了自己的状态来构建复杂的UI的组件。

Since component logic is written in JavaScript instead of templates, you can easily pass rich data through your app and keep state out of the DOM.

官方网站上说:组件的逻辑是用JavaScript编写,而不是模板,所以你轻松的传递数据到应用,并且让状态不和DOM打交道。

如果你十分好奇,这里的“而不是模板”是什么意思?Pete Hunt的那篇文章其实说的很清楚,传统的Web应用是通过HTML或者模板引擎(比如后端模板引擎:JSP,HAML等,前端模板引擎:handlebar,ejs等)来构建UI的,而React使用有完整功能编程语言来渲染视图。

其实,我是不太认同的,JSX不算模板,VueTemplate不算模板?不过JSX允许你用JavaScript的方式做一些逻辑的处理,而不像JSP需要些JSTL和ctag的的逻辑标签,如果我的理解有误,请务必纠正我。

Learn Once, Write Anywhere

这个,看看就行,官方网站这里特指的是React Native可以开发移动端的应用,不过大家也不要太天真,你理解成React和React-Native的思想和语法是融会贯通的就行了,不要真的以为可以很轻松的将组件在React和RN之间移植,否则你会被鄙视的。

另外提一点,在ElectronJS的帮助下,可以通过React开发桌面应用,这个倒是真可行。

总结

你看,首页的信息量其实挺大吧,认证阅读和思考,其实收获不少,总结下来就是:我是一个用来实现基于状态的UI组件的JavaScript库(妈呀,有点绕)。