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

  • 东西要领

    • 这是 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 是我们思索的内容。

资本

    原文作者:Arcthur
    原文地址: https://segmentfault.com/a/1190000004034031
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞