React Mixins入門指南

請叫我王磊同學發表於2017-03-24

  對於很多初級的前端工程師對mixins的概念並不是很瞭解,也沒有在React中嘗試使用過Mixins,這邊文章基本會按照Mixins的作用、用途、原理等多個方面介紹React中Mixins的使用。
  首先解釋一下什麼是Mixins,在一些大型專案中經常會存在多個元件需要使用相同的功能的情況,如果在每個元件中都重複性的加入相同的程式碼,那麼程式碼的維護性將會變的非常差,Mixins的出現就是為了解決這個問題。可以將通用共享的方法包裝成Mixins方法,然後注入各個元件實現,我們首先給出一個Mixins簡單的例子:

const mixin = function(obj, mixins) {
  const newObj = obj;
  newObj.prototype = Object.create(obj.prototype);
  for (let prop in mixins) {
    if (mixins.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixins[prop];
    }
  }
  return newObj;
}
const manMixins = {
  speak: function (){
    console.log("I'm "+this.name);
  }
};
const Man = function() {
  this.name = 'wang';
};
const manCanSpeak = mixin(Man,manMixins);
const man = new manCanSpeak(); 
man.speak(); //'I'm wang'複製程式碼

  上述程式碼就實現了一個簡單的mixin函式,其實質就是將mixins中的方法遍歷賦值給newObj.prototype,從而實現mixin返回的函式建立的物件都有mixins中的方法。在我們大致明白了mixin作用後,讓我們來看看如何在React使用mixin

React.createClass

  假設我們所有的React元件的props中都含有一個預設的displayName,在使用React.createClass時,我們必須給每個元件中都加入

getDefaultProps: function () {
    return {displayName: "component"};
}複製程式碼

  當然我們,我們通過實現一個mixin函式,就可以實現這個功能,並且在createClass方法使用mixin非常簡單:

var mixinDefaultProps = {
    getDefaultProps: function(){
        return {displayName: 'component'}
    }
}

var ExampleComponent = React.createClass({
    mixins: [mixinDefaultProps],
    render: function(){
        return <div>{this.props.displayName}</div>
    }
});複製程式碼

  這樣我們就實現了一個最簡單的mixin函式,通過給每一個元件配置mixin,我們就實現了不同元件之間共享相同的方法。需要注意的是:

mixin中有相同的函式

  1. 元件中含有多個mixin,不同的mixin中含有相同名字的非生命週期函式,React會丟擲異常(不是後面的函式覆蓋前面的函式)。
  2. 元件中含有多個mixin,不同的mixin中含有相同名字的生命週期函式,不會丟擲異常,mixin中的相同的生命週期函式(除render方法)會按照createClass中傳入的mixins陣列順序依次呼叫,全部呼叫結束後再呼叫元件內部的相同的宣告周期函式。

mixin中設定props或state

  1. 元件中含有多個mixin,不同的mixin中的預設props或初始state中不存在相同的key值時,則預設props和初始state都會被合併。
  2. 元件中含有多個mixin,不同的mixin中預設props或初始state中存在相同的key值時,React會丟擲異常。

JSX

  目前幾乎很少有人會使用React.createClass的方式使用React,JSX + ES6成了標配,但是JavaScript在ES6之前是原生不支援的mixin的,ES7引入了decorator,首先介紹一下decorator到底是什麼?

Decorator

  ES7的Decorator語法類似於Python中的Decorator,在ES7中也僅僅只是一個語法糖,@decorator主要有兩種,一種是面向於類(class)的@decorator,另一種是面向於方法(function)的@decorator。並且@decorator實質是利用了ES5中的Object.defineProperty

Object.defineProperty  

  關於Object.defineProperty不是很瞭解的同學其實非常推薦看一下《JavaScript高階程式設計》的第六章第一節,大概總結一下:在ES5中物件的屬性其實分為兩種: 資料屬性訪問器屬性

資料屬性

資料屬性有四個特性:

  1. configurable: 屬性是否可刪除、重新定義
  2. enumerable: 屬性是否可列舉
  3. writable: 屬性值是否可修改
  4. value: 屬性值
    訪問器屬性
  5. configurable: 屬性是否可刪除、重新定義
  6. enumerable: 屬性是否可列舉
  7. get: 讀取屬性呼叫
  8. set: 設定屬性呼叫

  Object.defineProperty(obj, prop, descriptor)的三個引數是定義屬性的物件、屬性名和描述符,描述符本身也是Object,其中的屬性就是資料屬性或者訪問器屬性規定的引數,舉個栗子:

var person = {};
Object.defineProperty(person,'name',{
    configurable: true,
    enumerable: true,
    writable: true,
    value: 'wang'
});
console.log(person.name);//wang複製程式碼

瞭解了Object.defineProperty,我們分別看下面向於類(class)的@decorator和麵向於方法(function)的@decorator。

面向方法的@decorator

  class語法其實僅僅只是ES6的一個語法糖而已,class其實質是function。並且class中的內部方法會通過Object.defineProperty定義到function.prototype,例如:

class Person {
  speak () {
    console.log('I am Person!') 
  }
}複製程式碼

會被Babel轉成:

function Person(){}

Object.defineProperty(Person.prototype,'speak',{
  value: function () { 'I am Person!' },
  enumerable: false,
  configurable: true,
  writable: true
})複製程式碼

  Decorator函式接受的引數與Object.defineProperty類似,與對類(class)的方法使用@decorator,接受到的方法分別是類的prototype,內部方法名和描述符,@decorator會在呼叫Object.defineProperty前劫持,先呼叫Decorator函式,將返回的descriptor定義到類的prototype上。
  例如:

function readonly(target, key, descriptor) {
  //可以通過修改descriptor引數實現各種功能
  descriptor.writable = false
  return descriptor
}

class Person {
  @readonly
  speak () {
    return 'I am Person!'
  }
}

const person = new Person();
person.speak = ()=>{
    console.log('I am human')
}複製程式碼

面向類的@decorator

  當我們對一個class使用@decorator時,接受到的引數target是類本身。例如:   

function name (target) {
  target.name = 'wang'
}

@name
class Person {}

console.log(Dog.name)
//'wang'複製程式碼

  講完了@decorator,現在讓我們回到JSX中,react-mixin core-decorators兩個庫都提供了mixin函式可用。大致讓我們看一下core-decorators庫中mixin的大致程式碼:

function handleClass(target, mixins) {
  if (!mixins.length) {
    throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
  }

  for (let i = 0, l = mixins.length; i < l; i++) {
    const descs = getOwnPropertyDescriptors(mixins[i]);
    const keys = getOwnKeys(descs);

    for (let j = 0, k = keys.length; j < k; j++) {
      const key = keys[j];

      if (!(hasProperty(key, target.prototype))) {
        defineProperty(target.prototype, key, descs[key]);
      }
    }
  }
}

export default function mixin(...mixins) {
  if (typeof mixins[0] === 'function') {
    return handleClass(mixins[0], []);
  } else {
    return target => {
      return handleClass(target, mixins);
    };
  }
}複製程式碼

@mixin使用如下:

import { mixin } from 'core-decorators';

const SingerMixin = {
  sing(sound) {
    alert(sound);
  }
};

const FlyMixin = {
  fly() {},
  land() {}
};

@mixin(SingerMixin, FlyMixin)
class Bird {
  singMatingCall() {
    this.sing('tweet tweet');
  }
}

var bird = new Bird();
bird.singMatingCall();複製程式碼

  我們可以看到mixin函式相當於採用Currying的方式接受mixins陣列,返回

return target => {
      return handleClass(target, mixins);
};複製程式碼

handleClass函式的大致作用就是採用defineProperty將mixins陣列中的函式定義在target.prototype上,這樣就實現了mixin的要求。

Mixin在React中的作用

  講了這麼多Mixin的東西,那麼Mixin在React中有什麼作用呢?Mixin的作用無非就是在多個元件中共享相同的方法,實現複用,React中的Mixin也是相同的。比如你的元件中可能有共同的工具方法,為了避免在每個元件中都有相同的定義,你就可以採用Mixin。下面依舊舉一個現實的例子。
  React的效能優化一個非常常見的方法就是減少元件不必要的render,一般我們可以在生命週期shouldComponentUpdate(nextProps, nextState)中進行判斷,通過判斷nextPropsnextStatethis.prosthis.state是否完全相同(淺比較),如果相同則返回false,表示不重新渲染,如果不相同,則返回true,使得元件重新渲染(當然你也可以不使用mixin,而使用React.PureComponent也可以達到相同的效果)。並且現在有非常多的現成的庫提供如上的功能,例如react-addons-pure-render-mixin中提供了PureRenderMixin方法,首先我們可以在專案下執行:

npm install --save react-addons-pure-render-mixin;複製程式碼

然後在程式碼中可以如下使用

import PureRenderMixin from 'react-addons-pure-render-mixin';
import {decorate as mixin} from 'react-mixin'
@mixin(PureRenderMixin)
class FooComponent extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <div className={this.props.className}>foo</div>;
  }
}複製程式碼

當然你也可以這樣寫:

var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div className={this.props.className}>foo</div>;
  }
});複製程式碼

甚至這樣寫:

import PureRenderMixin from 'react-addons-pure-render-mixin';
class FooComponent extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  render() {
    return <div className={this.props.className}>foo</div>;
  }
}複製程式碼

  因為@decorator是ES7的用法,所以必須使用Babel才能使用,所以我們需要在.babelrc檔案中設定:

{
  "presets": ["es2015", "stage-1"],
  "plugins": [
    "babel-plugin-transform-decorators-legacy"
  ]
}複製程式碼

並安裝外掛:

npm i babel-cli babel-preset-es2015 babel-preset-stage-1 babel-plugin-transform-decorators複製程式碼

  之後我們就可以盡情體驗ES7的decorator了!