最近刚接触Vue,一个Vue实例总是调用一个未定义的方法,仔细一看是引用了Mixin,不看不知道,一看吓一跳。代码里好多Mixin啊。一个Mixin里可能又引用了其他Mixin,最后你没办法知道Mixin里的方法都被哪些组件引用了。难搞哦…
之前写React的时候都会封装成组件,来实现代码复用,React倒是没有Mixins这个概念。React官网倒是看到一篇文章 - Mixins Considered Harmful。网上找到了它的中文版,在这里留个记录。
(后记:后来觉得Mixin真香!一些简单页面,简单固定逻辑,用Mixin会很方便)

Mixins或许不太好

“如何在不同的组件之间进行代码复用?”是人们在学习 React 时首先要问的问题之一,我们的答案始终是使用组件组合来重用代码。你可以定义一个组件并在其他几个组件中使用它。

某种确定的模式通过用组合的方式进行解决并不总是显而易见的。React 受函数式编程的影响,但进入了以面向对象库为主的领域。Facebook 内部和外部的工程师很难放弃他们习惯的模式。

为了简化最初的采用和学习,我们在 React 中加入了一些“逃生”功能。mixin 系统就是其中一种逃生方法,它的目标是当你不知道如何通过组合解决问题时,给你另外一种在组件之间重用代码的方法。

React 已经发布三年了,前端领域的技术在这三年中也发生了翻天覆地的变化。现在多个用于构建用户界面的前端框架都采用了类似于 React 的组件模型。使用基于继承的组合声明式的构建用户界面,不再是新鲜事物。我们对 React 组件模型也更加自信,我们在内部和社区都看到了它的许多创造性的用途。

在这篇文章中,我们将思考通常是由 mixin 引起的问题。然后,我们将为相同的用例提出几种替代模式。我们发现随着项目代码复杂度的增加,这些替代的模式的可扩展性比使用 mixin 更好。

React为什么不用Mixin

在 Facebook,React 的使用量已经从几个组件增长到了成千上万个。这给了我们一个去思考人们该如何更好的使用 React 的窗口。由于声明性渲染和自上而下的数据流,许多团队在采用 React 去实现一些新功能时,能够解决了很多之前难以去解决的 bug。

然而,使用 React 的一些代码不可避免地变得难以理解。有时候,React 团队会看到开发者不敢去碰某些项目中的组件。这些组件维护起来很容易出 bug,对新开发人员造成负担,最终组件会变得让创建这个组件的人都难以去维护。这种巨大的开发成本大部分是由 mixin 引起的。当时,我并没有在Facebook工作,但是在写下了我可怕的 mixin 之后,我得出了相同的结论—Mixins已经,组合永生。

这并不意味着 mixin 本身就是坏的。开发者们成功地在不同的语言和范例中使用 mixin,包括在一些函数式编程语言中。在 Facebook,我们广泛使用 Hack 中与 mixin 非常相似的特性。但是,我们依旧认为 mixin 在 React 代码库中是不必要的和有问题的。下面的内容是我们这样认为的原因。

Mixin 引入了隐式依赖关系

有时一个组件依赖于mixin中定义的某个方法,比如getClassName()。有时候是相反的,mixin在组件上调用renderHeader()方法。 JavaScript是一种动态语言,所以很难执行或记录这些依赖关系。

Mixin打破了常见且通常安全的假设,即可以通过在组件文件中搜索其出现来重命名状态键或方法。你可能会写一个有状态的组件,然后你的同事可能会添加一个读取这个状态的mixin。在几个月内,您可能需要将该状态移至父组件,以便与兄弟组件共享。你会记得更新mixin来读取道具吗?如果现在其他组件也使用这个mixin呢?

这些隐含的依赖性使新团队成员难以贡献代码库。一个组件的render()方法可能会引用一些未在该类上定义的方法。移除安全吗?也许它是在一个mixin中定义的。但是其中哪一个呢?您需要向上滚动到mixin列表,打开每个文件,然后查找此方法。更糟的是,mixin可以指定他们自己的mixin,所以搜索可以很深入。

mixin经常依赖于其他的mixin,而删除其中的一个会打破另一个。在这些情况下,告诉数据如何进出mixin是非常棘手的,以及它们的依赖关系图是什么样的。与组件不同,mixin不构成层次结构:它们被夷为平地并在相同的名称空间中运行。

Mixins导致名称冲突

不能保证两个特定的mixin可以一起使用。例如,如果FluxListenerMixin定义了handleChange()和WindowSizeMixin定义了handleChange(),则不能一起使用它们。你也不能在你自己的组件上定义一个带有这个名字的方法。

如果你控制混入代码,这不是什么大不了的事情。如果发生冲突,可以在其中一个mixin上重命名该方法。然而,这很棘手,因为一些组件或其他mixin可能已经直接调用这个方法,你也需要找到并修复这些调用。

如果你的名字与第三方包中的mixin有冲突,你不能只重命名一个方法。相反,您必须在您的组件上使用尴尬的方法名称以避免冲突。

mixin作者的情况并不好。即使向mixin添加一个新的方法总是一个潜在的重大改变,因为一个名称相同的方法可能已经存在于一些使用它的组件,直接或通过另一个mixin。一旦写入,mixin很难删除或更改。不好的想法不会被重构,因为重构风险太大。

Mixin导致复杂的滚雪球

即使mixin开始简单,随着时间的推移,它们往往会变得复杂。下面的例子是基于我在代码库中看到的真实场景。

组件需要一些状态来跟踪鼠标悬停。为了保持这个逻辑可重用,你可以将handleMouseEnter(),handleMouseLeave()和isHovering()提取到一个HoverMixin中。接下来,有人需要实施一个工具提示。他们不想复制HoverMixin中的逻辑,以便创建使用HoverMixin的TooltipMixin。 TooltipMixin读取HoverMixin在其componentDidUpdate()中提供的isHovering(),并显示或隐藏工具提示。

几个月后,有人想让工具提示方向可配置。为了避免代码重复,他们添加了一个名为getTooltipOptions()的新的可选方法到TooltipMixin。到目前为止,显示popovers的组件也使用HoverMixin。然而,popovers需要不同的悬停延迟。为了解决这个问题,有人增加了对可选的getHoverOptions()方法的支持,并在TooltipMixin中实现它。那些混合现在是紧密耦合的。

没有新的要求,这很好。但是这个解决方案不能很好地扩展。如果你想支持在单个组件中显示多个工具提示呢?你不能在一个组件中定义两次相同的mixin。如果工具提示需要在导游中自动显示,而不是悬停,怎么办?祝你好运解耦TooltipMixin从HoverMixin。如果您需要支持悬停区域和工具提示锚点位于不同组件的情况,该怎么办?你不能轻易地把混入到父组件中的状态提升起来。与组件不同,mixin不会自然地适应这种变化。

每一个新的要求都会让混音变得更难理解。使用相同mixin的组件越来越与时间耦合。任何新的能力被添加到使用该mixin的所有组件。如果没有复制代码或在mixin之间引入更多的依赖性和间接性,就没有办法拆分mixin的“更简单”的部分。逐渐地,封装边界逐渐消失,由于很难改变或移除现有的混合,他们不断变得抽象,直到没人理解它们是如何工作的。

这些与我们在React之前构建应用程序的问题是一样的。我们发现它们是通过声明性渲染,自顶向下的数据流和封装组件来解决的。在Facebook上,我们一直在迁移我们的代码以使用替代模式来混合,我们对结果普遍感到满意。你可以阅读下面的模式。

参考资料