React Mixin 的前世此生

在 React component 構建過程當中,經常有這樣的場景,有一類功能要被不一樣的 Component 公用,而後看獲得文檔常常提到 Mixin(混入) 這個術語。此文就從 Mixin 的來源、含義、在 React 中的使用提及。html

使用 Mixin 的原因

Mixin 的特性一直普遍存在於各類面嚮對象語言。尤爲在腳本語言中大都有原生支持,好比 Perl、Ruby、Python,甚至連 Sass 也支持。先來看一個在 Ruby 中使用 Mixin 的簡單例子,react

module D
  def initialize(name)
    @name = name
  end
  def to_s
    @name
  end
end

module Debug
  include D
  def who_am_i?
    "#{self.class.name} (\##{self.object_id}): #{self.to_s}"
  end
end

class Phonograph
  include Debug
  # ...
end

class EightTrack
  include Debug
  # ...
end

ph = Phonograph.new("West End Blues")
et = EightTrack.new("Real Pillow")
puts ph.who_am_i?  # Phonograph (#-72640448): West End Blues
puts et.who_am_i?  # EightTrack (#-72640468): Real Pillow

在 ruby 中 include 關鍵詞便是 mixin,是將一個模塊混入到一個另外一個模塊中,或是一個類中。爲何編程語言要引入這樣一種特性呢?事實上,包括 C++ 等一些年齡較大的 OOP 語言,有一個強大但危險的多重繼承特性。現代語言爲了權衡利弊,大都捨棄了多重繼承,只採用單繼承。但單繼承在實現抽象時有着諸多不便之處,爲了彌補缺失,如 Java 就引入 interface,其它一些語言引入了像 Mixin 的技巧,方法不一樣,但都是爲創造一種 相似多重繼承 的效果,事實上說它是 組合 更爲貼切。git

在 ES 歷史中,並無嚴格的類實現,早期 YUI、MooTools 這些類庫中都有本身封裝類實現,並引入 Mixin 混用模塊的方法。到今天 ES6 引入 class 語法,各類類庫也在向標準化靠攏。github

封裝一個 Mixin 方法

看到這裏,咱們既然知道了廣義的 mixin 方法的做用,那不妨試試本身封裝一個 mixin 方法來感覺下。編程

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 BigMixin = {
  fly: () => {
    console.log('I can fly');
  }
};

const Big = function() {
  console.log('new big');
};

const FlyBig = mixin(Big, BigMixin);

const flyBig = new FlyBig(); // 'new big'
flyBig.fly(); // 'I can fly'

對於廣義的 mixin 方法,就是用賦值的方式將 mixins 對象裏的方法都掛載到原對象上,就實現了對對象的混入。數組

是否看到上述實現會聯想到 underscore 中的 extend 或 lodash 中的 assign 方法,或者說在 ES6 中一個方法 Object.assign()。它的做用是什麼呢,MDN 上的解釋是把任意多個的源對象所擁有的自身可枚舉屬性拷貝給目標對象,而後返回目標對象。ruby

由於 JS 這門語言的特別,在沒有提到 ES6 Classes 以前沒有真正的類,僅是用方法去模擬對象,new 方法即爲建立一個實例。正由於這樣地弱,它也那樣的靈活,上述 mixin 的過程就像對象拷貝同樣。app

那問題是 React component 中的 mixin 也是這樣的嗎?框架

React createClass

React 最主流構建 Component 的方法是利用 createClass 建立。顧名思義,就是創造一個包含 React 方法 Class 類。這種實現,官方提供了很是有用的 mixin 屬性。咱們就先來看看它來作 mixin 的方式是怎樣的。less

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
  mixins: [PureRenderMixin],

  render() {
    return <div>foo</div>;
  }
});

以官方封裝的 PureRenderMixin 來舉例,在 createClass 對象參數中傳入一個 mixins 的數組,裏面封裝了咱們所須要的模塊。mixins 也能夠增長多個重用模塊,使用多個模塊,方法之間的有重合會對普通方法和生命週期方法有所區分。

在不一樣的 mixin 裏實現兩個名字同樣的普通方法,在常規實現中,後面的方法應該會覆蓋前面的方法。那在 React 中是否同樣會覆蓋呢。事實上,它並不會覆蓋,而是在控制檯裏報了一個在 ReactClassInterface 裏的 Error,說你在嘗試定義一個某方法在 component 中多於一次,這會形成衝突。所以,在 React 中是不容許出現重名普通方法的 Mixin。

若是是 React 生命週期定義的方法呢,是會將各個模塊的生命週期方法疊加在一塊兒,順序執行。

由於,咱們看到 createClass 實現的 mixin 爲 Component 作了兩件事:

  • 工具方法

    • 這是 mixin 的基本功能,若是你想共享一些工具類方法,就能夠定義它們,直接在各個 Component 中使用。

  • 生命週期繼承,props 與 state 合併

    • 這是 react mixin 特別也是重要的功能,它可以合併生命週期方法。若是有不少 mixin 來定義 componentDidMount 這個週期,那 React 會很是智能的將它們都合併起來執行。

    • 一樣地,mixins 也能夠做用在 getInitialState 的結果上,做 state 的合併,同時 props 也是這樣合併。

將來的 React Classes

當 ECMAScript 發展到今天,這已是一個百家爭鳴的時代,各類優異的語言特性都出如今 ES6 和 ES7 的草案中。

React 在發展過程當中一直崇尚擁抱標準,儘管它本身看上去是一個異類。當 React 0.13 釋出的時候,React 增長並推薦使用 ES6 Classes 來構建 Component。但很是不幸,ES6 Classes 並不原生支持 mixin。儘管 React 文檔中也未能給出解決方法,但如此重要的特性沒有解決方案,也是一件十分困擾的事。

爲了能夠用這個強大的功能,還得想一想其它方法,來尋找可能的方法來實現重用模塊的目的。先回歸 ES6 Classes,咱們來想一想怎麼封裝 mixin。

讓 ES6 Class 與 Decorator 跳舞

要在 Class 上封裝 mixin,就要說到 Class 的本質。ES6 沒有改變 JavaScript 面向對象方法基於原型的本質,不過在此之上提供了一些語法糖,Class 就是其中之一,換湯不換藥。

對於 Class 具體用法能夠參考 MDN。目前 Class 僅是提供一些基本寫法與功能,隨着標準化的進展,相信會有更多的功能加入。

那對於實現 mixin 方法來講就沒什麼不同了。但既然講到了語法糖,就來說講另外一個語法糖 Decorator,正巧能夠來實現 Class 上的 mixin。

Decorator 在 ES7 中定義的新特性,與 Java 中的 pre-defined Annotations 類似。但與 Java 的 annotations 不一樣的是 decorators 是被運用在運行時的方法。在 Redux 或其餘一些應用層框架中漸漸用 decorator 實現對 component 的『修飾』。如今,咱們來用 decorator 來現實 mixin。

core-decorators.js 爲開發者提供了一些實用的 decorator,其中實現了咱們正想要的 @minxin。咱們來解讀一下核心實現。

import { getOwnPropertyDescriptors } from './private/utils';

const { defineProperty } = Object;

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++) {
       // 獲取 mixins 的 attributes 對象
    const descs = getOwnPropertyDescriptors(mixins[i]);

     // 批量定義 mixin 的 attributes 對象
    for (const key in descs) {
      if (!(key in 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 對象的方法都疊加到 target 對象的原型上以達到 mixin 的目的。這樣,就能夠用 @mixin 來作多個重用模塊的疊加了。

import React, { Component } from 'React';
import { mixin } from 'core-decorators';

const PureRender = {
  shouldComponentUpdate() {}
};

const Theme = {
  setTheme() {}
};

@mixin(PureRender, Theme)
class MyComponent extends Component {
  render() {}
}

細心的讀者有沒有發現這個 mixin 與 createClass 上的 mixin 有區別。上述實現 mixin 的邏輯和最先實現的簡單邏輯是很類似的,以前直接給對象的 prototype 屬性賦值,但這裏用了 getOwnPropertyDescriptor defineProperty 這兩個方法,有什麼區別呢?

事實上,這樣實現的好處在於 defineProperty 這個方法,也是定義與賦值的區別,定義則是對已有的定義,賦值則是覆蓋已有的定義。因此說前者並不會覆蓋已有方法,後者是會的。本質上與官方的 mixin 方法都很不同,除了定義方法級別的不能覆蓋以外,還得加上對生命週期方法的繼承,以及對 State 的合併。

再回到 decorator 身上,上述只是做用在類上的方法,還有做用在方法上的,它能夠控制方法的自有屬性,也能夠做 decorator 工廠方法。在其它語言裏,decorator 用途普遍,具體擴展不在本文討論的範圍。

講到這裏,對於 React 來講咱們天然能夠用上述方法來作 mixin。但 React 開發社區提出了『全新』的方式來取代 mixin,那就是 Higher-Order Components。

Higher-Order Components(HOCs)

Higher-Order Components(HOCs)最先由 Sebastian Markbåge(React 核心開發成員)在 gist 提出的一段代碼。

Higher-Order 這個單詞相信都很熟悉,Higher-Order function(高階函數)在函數式編程是一個基本概念,它描述的是這樣一種函數,接受函數做爲輸入,或是輸出一個函數。好比經常使用的工具方法 mapreducesort 都是高階函數。

而 HOCs 就很好理解了,將 Function 替代成 Component 就是所謂的高階組件。若是說 mixin 是面向 OOP 的組合,那 HOCs 就是面向 FP 的組合。先看一個 HOC 的例子,

import React, { Component } from 'React';

const PopupContainer = (Wrapper) =>
  class WrapperComponent extends Component {
    componentDidMount() {
      console.log('HOC did mount')
    }

    componentWillUnmount() {
      console.log('HOC will unmount')
    }

    render() {
      return <Wrapper {...this.props} />;
    }
  }

上面例子中的 PopupContainer 方法就是一個 HOC,返回一個 React Component。值得注意的是 HOC 返回的老是新的 React Component。要使用上述的 HOC,那能夠這麼寫。

import React, { Component } from 'React';

class MyComponent extends Component {
  render() {}
}

export default PopupContainer(MyStatelessComponent);

封裝的 HOC 就能夠一層層地嵌套,這個組件就有了嵌套方法的功能。對,就這麼簡單,保持了封裝性的同時也保留了易用性。咱們剛纔講到了 decorator,也能夠用它轉換。

import React, { Component } from 'React';

@PopupContainer
class MyComponent extends Component {
  render() {}
}

export default MyComponent;

簡單地替換成做用在類上的 decorator,理解起來就是接收須要裝飾的類爲參數,返回一個新的內部類。恰與 HOCs 的定義徹底一致。因此,能夠認爲做用在類上的 decorator 語法糖簡化了高階組件的調用。

若是有不少個 HOC 呢,形如 f(g(h(x)))。要不不少嵌套,要不寫成 decorator 疊羅漢。再看一下它,有沒有想到 FP 裏的方法?

import React, { Component } from 'React';

// 來自 https://gist.github.com/jmurzy/f5b339d6d4b694dc36dd
let as = T => (...traits) => traits.reverse().reduce((T, M) => M(T), T);

class MyComponent extends as(Component)(Mixin1, Mixin2, Mixin3(param)) { }

絕妙的方法!或用更好理解的 compose 來作

import React, { Component } from 'React';
import R from 'ramda';

const mixins = R.compose(Mixin3(param), Mixin2, Mixin1);

class MyComponent extends mixins(Component) {}

講完了用法,這種 HOC 有什麼特殊之處呢,

  1. 從侵入 class 到與 class 解耦,React 一直推崇的聲明式編程優於命令式編程,而 HOCs 恰是。

  2. 調用順序不一樣於 React Mixin,上述執行生命週期的過程相似於 堆棧調用didmount -> HOC didmount -> (HOCs didmount) -> (HOCs will unmount) -> HOC will unmount -> unmount

  3. HOCs 對於主 Component 來講是 隔離 的,this 變量不能傳遞,以致於不能傳遞方法,包括 ref。但能夠用 context 來傳遞全局參數,通常不推薦這麼作,極可能會形成開發上的困擾。

固然,HOCs 不只是上述這一種方法,咱們還能夠利用 Class 繼承 來寫,再來一個例子,

const PopupContainer = (Wrapper) =>
  class WrapperComponent extends Wrapper {
    static propTypes = Object.assign({}, Component.propTypes, {
      foo: React.PropTypes.string,
    });

    componentDidMount() {
      super.componentDidMount && super.componentDidMount();
      console.log('HOC did mount')
    }

    componentWillUnmount() {
      super.componentWillUnmount && super.componentWillUnmount();
      console.log('HOC will unmount')
    }
  }

其實,這種方法與第一種構造是徹底不同的。區別在哪,仔細看 Wrapper 的位置處在了繼承的位置。這種方法則要通用得多,它經過繼承原 Component 來作,方法都是能夠經過 super 來順序調用。由於依賴於繼承的機制,HOC 的調用順序和 隊列 是同樣的。

didmount -> HOC didmount -> (HOCs didmount) -> will unmount -> HOC will unmount -> (HOC will unmount)

細心的你是否已經看出 HOCs 與 React Mixin 的順序是反向的,很簡單,將 super 執行放在後面就能夠達到正向的目的,儘管看上去很怪。這種不一樣極可能會致使問題的產生。儘管它是將來可能的選項,但如今看還有很多問題。

總結

將來的 React 中 mixin 方案 已經有僞代碼現實,仍是利用繼承特性來作。

而繼承並非 "React Way",Sebastian Markbåge 認爲實現更方便地 Compsition(組合)比作一個抽象的 mixin 更重要。並且聚焦在更容易的組合上,咱們才能夠擺脫掉 "mixin"。

對於『重用』,能夠從語言層面上去說,都是爲了能夠更好的實現抽象,實現的靈活性與寫法也存在一個平衡。在 React 將來的發展中,期待有更好的方案出現,一樣期待 ES 將來的草案中有增長 Mixin 的方案。就今天來講,怎麼去實現一個不復雜又好用的 mixin 是咱們思考的內容。

資源

相關文章
相關標籤/搜索