React Mixin 的前世今生

jopen 9年前發布 | 23K 次閱讀 React


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

使用 Mixin 的緣由

Mixin 的特性一直廣泛存在于各種面向對象語言。尤其在腳本語言中大都有原生支持,比如 Perl、Ruby、Python,甚至連 Sass 也支持。先來看一個在 Ruby 中使用 Mixin 的簡單例子,

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 的技巧,方法不同,但都是為創造一種 類似多重繼承 的效果,事實上說它是 組合 更為貼切。

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

封裝一個 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 上的解釋是把任意多個的源對象所擁有的自身可枚舉屬性拷貝給目標對象,然后返回目標對象。

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

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

React createClass

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

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。

在 Class 上的 Mixin

要在 Class 上封裝 mixin,就要說到 Class 的本質。ES6 沒有改變 JavaScript 基于原型的本質,不過在此之上提供了一些語法糖,Class 就是其中之一,內部還是轉換成相應的 ES5 語法。

對于 Class 具體用法可以參考 MDN 。目前 class 僅是提供一些基本功能,但隨著標準化的進展,相信會有更多的功能。

那對于實現 mixin 方法來說就沒什么不一樣了。但既然剛才講到了語法糖,就來講講另一個語法糖,正巧來實現 mixin 方法。

Decorator

Decorator 在 ES7 中定義的新特性,與 Java 中的 pre-defined Annotations 相似。但與 Java 的 annotations 不同的是 decorators 是被運用在運行時的方法。在 Redux 或其他一些應用層框架中漸漸用 Decorator 實現對 Component 的『修飾』。

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 屬性賦值,但這里用了 getOwnPropertyDescriptordefineProperty 這兩個方法,有什么區別呢?

事實上,這樣實現的好處在于 defineProperty 這個方法,也是定義與賦值的區別,定義則是對已有的定義,賦值則是覆蓋已有的定義。所以說前者并不會覆蓋已有方法,后者是會的。本質上與官方的 mixin 方法都很不一樣,除了定義方法級別的不能覆蓋之外,還得加上對生命周期方法的繼承,以及對 State 的合并。

再回到 decorator 身上,上述只是作用在類上的方法,還有作用在方法上的,它可以控制方法的自有屬性,也可以作 decorator 工廠方法。在其它語言里,decorator 用途廣泛,具體擴展不在本文討論的范圍。

講到這里,對于 React Classes 我們自然可以用上述方法來做 mixin。還有其它方法么?當然有,那就是 HOCs。

Higher-Order Components(HOCs)

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

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

而 HOCs 就很好理解了,將 Function 替代成 Component 就是所謂的高階組件。比如,

import React, { Component } from 'React';

const PopupContainer = (Wrapper) =>
  class WrapperComponent extends Component {
    render() {
      return <Wrapper {...this.props} />;
    }
  }

上面例子中的 PureRender 方法是一個高階函數,返回一個 Component。注意這里 WrapperComponent 中只能直接返回 Wrapper,為什么呢,是一個思考題。還可以換一個形式來寫。

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

    render() {
      return super.render(this.props);
    }
  }

其實,上述兩種方法是不一樣的。區別在哪,請仔細看 Wrapper 的位置。在第一種方法中,Wrapper 的方法是不能繼承的,也無法在 Component 間 調用,this 是隔離的。第二種方法則要通用得多,它通過繼承原 Component 來做,方法都是可以通過 super 來順序調用。

如果只需要普通方法呢,

const PopupContainer = (Wrapper) => {
    Wrapper.prototype.addClass = () => {};

    return Wrapper;
  }

這種方法是不是很像上一個篇章講的 mixin 的實現。然后,我們再來看看怎么用。

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

細心的你是否已經看出了它們的玄妙。

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

  • HOC 可以與組件完全無關,也可以用繼承,這樣可以方便做生命周期方法的順序執行,但與官方 mixin 同樣有所區別;

總結

未來的 React 中 mixin 方案 已經有偽代碼現實,還是利用繼承特性來做。

而繼承并不是 "React Way",Sebastian Markb?ge 認為實現更方便地 Compsition(組合)比做一個抽象的 mixin 更重要。而且聚焦在更容易的組合上,我們才可以擺脫掉 "mixin"。

對于『重用』,可以從語言層面上去說,都是為了可以更好的實現抽象,實現的靈活性與寫法也存在一個平衡。在 React 未來的發展中,期待有更好的方案出現,同樣期待 ES 未來的草案中有增加 Mixin 的方案。就今天來說,怎么去實現一個不復雜又好用的 mixin 是我們思考的內容。

資源

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!