React Mixin 的前世今生

React Mixin 的前世今生

流形流形

在 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 做了两件事:

  • 1. 工具方法
    • 这是 mixin 的基本功能,如果你想共享一些工具类方法,就可以定义它们,直接在各个 Component 中使用。
  • 2. 生命周期继承,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(高阶函数)在函数式编程是一个基本概念,它描述的是这样一种函数,接受函数作为输入,或是输出一个函数。比如常用的工具方法 map、reduce、sort 都是高阶函数。

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

资源

文章被以下专栏收录
11 条评论
推荐阅读