使用 ES2015 重构 React 组件

React 组件 ES2015 Class 改写细节

createClass 工厂函数转换为 ES2015 Class

ES5:

var MyComponent = React.createClass({  
  // ...
});

ES2015:

class MyComponent extends React.Component {  
  // ...
}

提取 propTypesgetDefaultTypes

createClass 对象字面量不同,ES2015 极简 Class 只允许定义方法,不能定义属性(有可能在 ES7 中扩展),诸如 propTypes 等属性都要提取为 Class 本身的属性(静态属性)。

静态属性指的是 Class 本身的属性,即 Class.propname,而不是定义在实例对象(this)上的属性。

ES5:

var MyComponent = React.createClass({  
  propTypes: {
    someProp: React.PropTypes.string
  },

  getDefaultProps: function() {
    return {
      someProp: ''
    };
  }
});

ES2015:

class MyComponent extends React.Component {  
}

MyComponent.propTypes = {  
  someProp: React.PropTypes.string,
}

MyComponent.defaultProps = {  
  someProp: '',
}

ES2015 Class 静态属性语法:

class MyComponent extends React.Component {  
  static propTypes = {
    someProp: React.PropTypes.string,
  };

  static defaultProps = {
    someProp: '',
  };
}

显然,使用 static 更加方便代码块的组织,后面涉及的 contextTypes 等属性也可以按此语法编写。

注意:

使用 static 语法需要在 Babel 配置中添加 stage-0

javascript { "presets": ["stage-0"] }

转移 getInitialState

ES5:

var MyComponent = React.createClass({  
  getInitialState: function() {
    return {
      someState: this.props.someProp
    };
  }
});

ES2015:

class MyComponent extends React.Component {  
  constructor(props) {
    super(props);
    // 初始状态设置转移到 constructor 里了
    this.state = {
      someState: props.someProp,
    };
  }
}

注意:

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用super 方法),然后再用子类的构造函数修改 this

如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法。 javascript constructor(...args) { super(...args); } 另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。 (via

constructor 必须先调用 super 方法,再进行初始状态设置、this 绑定等操作

提取 Context 相关属性(方法)

ES5:

var Foo = React.createClass({  
  contextTypes: {
    tag: React.PropTypes.string,
    className: React.PropTypes.string
  },

  getInitialState: function() {
    return {
      tag: this.context.tag,
      className: this.context.className
    };
  },

  render: function() {
    var Tag = this.state.tag;

    return <Tag className={this.state.className} />;
  }
});

var Outer = React.createClass({  
  childContextTypes: {
    tag: React.PropTypes.string,
    className: React.PropTypes.string
  },

  getChildContext: function() {
    return {
      tag: 'span',
      className: 'foo'
    };
  },

  render: function() {
    return <Foo />;
  }
});

ES2015:

class Foo extends React.Component {  
  constructor(props, context) {
    super(props, context);
    this.state = {
      tag: context.tag,
      className: this.context.className,
    };
  }

  render() {
    var Tag = this.state.tag;
    return <Tag className={this.state.className} />;
  }
}

Foo.contextTypes = {  
  tag: React.PropTypes.string,
  className: React.PropTypes.string,
};

class Outer extends React.Component {  
  getChildContext() {
    return {
      tag: 'span',
      className: 'foo',
    };
  }

  render() {
    return <Foo />;
  }
}

Outer.childContextTypes = {  
  tag: React.PropTypes.string,
  className: React.PropTypes.string,
};

以上代码来自 React 官方测试用例

绑定 this

转换为 ES2015 Class 以后,实例方法不会自动绑定 this,需要手动绑定或者使用箭头函数。

ES5:

var MyComponent = React.createClass({  
  handleClick: function() {
    console.log(this);
  },

  render: function() {
    return (
      <div
        onClick={this.handleClick}
      >
        Hello world.
      </div>
    );
  }
});

ES2015:

class MyComponent extends React.Component {  
  handleClick() {
    console.log(this);
  }

  render() {
    // 使用 bind 绑定
    return (
      <div onClick={this.handleClick.bind(this)}>
        Hello world.
      </div>
    );

    // 或者使用箭头函数 <div onClick={() => this.handleClick()}>
  }
}

官方推荐在 constructor 中绑定,方便在不同的地方调用:

class MyComponent extends React.Component {  
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this);
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        Hello world.
      </div>
    );
  }
}

如果方法很多,还可以抽象出一个专门一个基类。

每个方法手动绑定:

class MyComponent extends React.Component {  
  constructor() {
    super();
    this.handleClick = this.handleClick.bind(this);
    this.handleFoo = this.handleFoo.bind(this);
  }
  // ...
}

使用基类绑定:

class BaseComponent extends React.Component {  
  _bind(...methods) {
    methods.forEach((method) => {
      this[method] = this[method].bind(this);
    });
  }
}

class MyComponent extends BaseComponent {  
  constructor() {
    super();
    this._bind('handleClick', 'handleFoo');
  }

  // ...
}

Mixin 问题

由于 ES2015 的原因,使用 ES2015 Class 创建的 React 组件不支持 mixin。这应该是已有项目使用 ES2015 Class 重构的最大障碍。编写 Amaze UI Touch 时,虽然使用了部分 ES2015 语法,但基于使用 mixin 考虑,仍然使用 createClass 定义组件。在一些项目中,也能看到 Class 语法和工厂方法混用,原因皆在于 mixin。

修饰符可能是将来的解决方案,但仍需等待,目前可先参考以下内容:

参考链接