在 React component 构建过程中,常常有这样的场景,有一类功能要被不同的 Component 公用,然后看得到文档经常提到 Mixin(混入) 这个术语。此文就从 Mixin 的来源、含义、在 React 中的使用说起。
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 方法来感受下。
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 最主流构建 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 中使用。
这是 react mixin 特别也是重要的功能,它能够合并生命周期方法。如果有很多 mixin 来定义 componentDidMount 这个周期,那 React 会非常智能的将它们都合并起来执行。同样地,mixins 也可以作用在 getInitialState 的结果上,作 state 的合并,同时 props 也是这样合并。
当 ECMAScript 发展到今天,这已经是一个百家争鸣的时代,各种优异的语言特性都出现在 ES6 和 ES7 的草案中。
React 在发展过程中一直崇尚拥抱标准,尽管它自己看上去是一个异类。当 React 0.13 释出的时候,React 增加并推荐使用 ES6 Classes 来构建 Component。但非常不幸,ES6 Classes 并不原生支持 mixin。尽管 React 文档中也未能给出解决方法,但如此重要的特性没有解决方案,也是一件十分困扰的事。
为了可以用这个强大的功能,还得想想其它方法,来寻找可能的方法来实现重用模块的目的。先回归 ES6 Classes,我们来想想怎么封装 Mixin。
要在 Class 上封装 mixin,就要说到 Class 的本质。ES6 没有改变 JavaScript 基于原型的本质,不过在此之上提供了一些语法糖,Class 就是其中之一,内部还是转换成相应的 ES5 语法。
对于 Class 具体用法可以参考 MDN 。目前 class 仅是提供一些基本功能,但随着标准化的进展,相信会有更多的功能。
那对于实现 mixin 方法来说就没什么不一样了。但既然刚才讲到了语法糖,就来讲讲另一个语法糖,正巧来实现 mixin 方法。
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 属性赋值,但这里用了 getOwnPropertyDescriptor
和 defineProperty
这两个方法,有什么区别呢?
事实上,这样实现的好处在于 defineProperty
这个方法,也是定义与赋值的区别,定义则是对已有的定义,赋值则是覆盖已有的定义。所以说前者并不会覆盖已有方法,后者是会的。本质上与官方的 mixin 方法都很不一样,除了定义方法级别的不能覆盖之外,还得加上对生命周期方法的继承,以及对 State 的合并。
再回到 decorator 身上,上述只是作用在类上的方法,还有作用在方法上的,它可以控制方法的自有属性,也可以作 decorator 工厂方法。在其它语言里,decorator 用途广泛,具体扩展不在本文讨论的范围。
讲到这里,对于 React Classes 我们自然可以用上述方法来做 mixin。还有其它方法么?当然有,那就是 HOCs。
Higher-Order Components(HOCs)最早由 Sebastian Markbåge(React 核心开发成员)在 gist 提出的一段代码。
Higher-Order 这个单词相信都很熟悉,Higher-Order function(高阶函数)在函数式编程是一个基本概念,它描述的是这样一种函数,接受函数作为输入,或是输出一个函数。比如常用的工具方法 map
、 reduce
、 sort
都是高阶函数。
而 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 是我们思考的内容。