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 是咱们思考的内容。

资源

相关文章
相关标签/搜索