You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
function addAndLog(x, y) {
var result = x + y;
console.log('result:', result);
return result;
}
function multiplyAndLog(x, y) {
var result = x * y;
console.log('result:', result);
return result;
}
function withLogging(wrappedFunction) {
// Return a function with the same API...
return function(x, y) {
// ... that calls the original function
var result = wrappedFunction(x, y);
// ... but also logs its result!
console.log('result:', result);
return result;
};
}
function add(x, y) {
return x + y;
}
function multiply(x, y) {
return x * y;
}
function withLogging(wrappedFunction) {
return function(x, y) {
var result = wrappedFunction(x, y);
console.log('result:', result);
return result;
};
}
// Equivalent to writing addAndLog by hand:
var addAndLog = withLogging(add);
// Equivalent to writing multiplyAndLog by hand:
var multiplyAndLog = withLogging(multiply);
// This is a child component.
// It only renders the comments it receives as props.
var CommentList = React.createClass({
render: function() {
// Note: now reading from props rather than state.
var comments = this.props.comments;
return (
<div>
{comments.map(function(comment) {
return <Comment comment={comment} key={comment.id} />
})}
</div>
)
}
});
// This is a parent component.
// It subscribes to the data source and renders <CommentList />.
var CommentListWithSubscription = React.createClass({
getInitialState: function() {
return {
comments: DataSource.getComments()
};
},
componentDidMount: function() {
DataSource.addChangeListener(this.handleChange);
},
componentWillUnmount: function() {
DataSource.removeChangeListener(this.handleChange);
},
handleChange: function() {
this.setState({
comments: DataSource.getComments()
});
},
render: function() {
// We pass the current state as props to CommentList.
return <CommentList comments={this.state.comments} />;
}
});
module.exports = CommentListWithSubscription;
var CommentList = React.createClass({
render: function() {
var comments = this.props.comments;
return (
<div>
{comments.map(function(comment) {
return <Comment comment={comment} key={comment.id} />
})}
</div>
)
}
});
// withSubscription() returns a new component that
// is subscribed to the data source and renders
// <CommentList /> with up-to-date data.
var CommentListWithSubscription = withSubscription(CommentList);
// The rest of the app is interested in the subscribed component
// so we export it instead of the original unwrapped CommentList.
module.exports = CommentListWithSubscription;
var RouterMixin = {
contextTypes: {
router: React.PropTypes.object.isRequired
},
// The mixin provides a method so that components
// don't have to use the context API directly.
push: function(path) {
this.context.router.push(path)
}
};
var Link = React.createClass({
mixins: [RouterMixin],
handleClick: function(e) {
e.stopPropagation();
// This method is defined in RouterMixin.
this.push(this.props.to);
},
render: function() {
return (
<a onClick={this.handleClick}>
{this.props.children}
</a>
);
}
});
module.exports = Link;
function withRouter(WrappedComponent) {
return React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
render: function() {
// The wrapper component reads something from the context
// and passes it down as a prop to the wrapped component.
var router = this.context.router;
return <WrappedComponent {...this.props} router={router} />;
}
});
};
var Link = React.createClass({
handleClick: function(e) {
e.stopPropagation();
// The wrapped component uses props instead of context.
this.props.router.push(this.props.to);
},
render: function() {
return (
<a onClick={this.handleClick}>
{this.props.children}
</a>
);
}
});
// Don't forget to wrap the component!
module.exports = withRouter(Link);
原文: https://facebook.github.io/react/blog/2016/07/13/mixins-considered-harmful.html
“如何在不同的组件中分享代码?”是大家学习React的时候常问的问题之一。我们的回答一直是:使用组件组合的方式实现代码复用。你可以定义一个组件然后在其他组件中使用它。
如何通过组合来解决某个固定的模式不总是那么明显。React是受函数式编程的影响,但却进入了一个由面向对象库为主导的领域。这对于无论是Facebook的内部开发者还是外部开发者,要放弃之前常用的模式都会感到困难。
为了缓解入门学习React的压力,我们包含了固定的解决方法(逃生舱)。混入体系就是解决方法之一,其目的就是在不确定如何通过组合解决同样的问题时,给大家一个在组件间复用代码的方式。
从React发布以来已经三年了,前端的格局已经发生了变化。各种各样的视图框架采纳了和React类似的组件模型。通过在继承上使用组合的方式来建立声明式的的用户界面也不再是什么新玩意儿。我们对React组件模型也很有信心,无论是在内部还是在社区我们看到了许多创造性应用。
在这篇文章中,我们将考虑由混入带来的常见问题。然后针对同样的使用案例建议一些可选的替代模式。这些模式相比混入而言,在代码库的复杂度上扩展更好。
混入模式为什么不好
在Facebook里面,React的使用已经从几个组件发展到成千上万个。这给了我们研究人们是如何使用React的一个渠道。由于声明式的渲染以及从上而下的数据流,很多团队在采用React进行迁移项目时就可以修复一堆错误。
然而,一些React代码逐渐变得难以理解也是在所难免。偶尔,React团队会查看一些人们不敢碰的一些不同项目中的组件。这些组件太简单了,以致于一不小心就会被破坏,对新人来说也很让人困惑,而且最终对于最初写它的人来说也很困惑。而这些困惑是由混入模式引起。那时,我还没有在Facebook工作,然而在写了我对于糟糕混入分享的担忧后也得出了相同的结论。
这并不意味着混入模式本身就不好。也有人成功地在不同的语言和规范中使用它,包括一些函数式编程语言。在Facebook,我们广泛使用和混入模式非常相似的兼容特性。不过,我们觉得混入是不必要的,而且在React代码库中也存在问题。原因有以下。
混入模式采用隐式依赖
在混入模式中,有时一个组件依赖于一种特定的方法定义,比如
getClassName()
。有时用另外的方法,在组件上混合调用一个像renderHeader()
这样的方法。JavaScript是一个动态语言,所以很难执行或者记录这些依赖。混入打破了常见的安全前提,你可以重命名一个状态键或者一个方法,通过在组件中搜索它就行。你可能写一个状态组件,然后你的同事可能会添加一个读取这个状态的混入。在接下来几个月内,你可能想把状态移到父组件上便于和兄弟组件分享。你会记得更新混入而不是读取一个prop么?如果现在也有其他组件使用这个混入呢?
这种隐式依赖对团队新成员继续维护更新一个代码库是很困难的。一个组件的
render()
方法可能会引用一些其他没有定义在当前类中的方法。要去掉安全么?或许它就定义在一个混合中。但是哪一个呢?你需要滚动混入的列表,打开每一个文件,再查找这个方法。更糟糕的是,混入可以指定为它们自己,所以查找链就会更深。通常,混入涉及依赖其他混入,去除其中一个会导致另一个失效。在这种情况下,要说清楚混入内外的数据流以及它们的依赖关系就非常棘手。不像组件,混入不会形成一种层次结构:它们是扁平的并且要在同一个命名空间里操作。
混入造成命名冲突
不保证说两个单独的混入可以一起使用。比如:如果
FluxListenerMixin
定义了handleChange()
,WindowSizeMixin
也定义了handleChange()
,你不能一起使用它们。你也不能在你自己的组件中用这个名称定义方法。如果你在掌控这些混入代码这也没什么。当你遇到冲突的时候,你可以在其中一个混入中重新命名那个方法。然而这很棘手,因为一些组件或者其他混入可能已经直接调用此方法,你还是需要找出这些调用并修改。
如果你遇到来自第三方包的混入命名冲突,就不能只是重命名了。相反,你必须在你的组件上使用不太合适的方法命名来避免冲突。
对于混入创造者而言情况并没有好转。甚至添加一个新方法到混入一直是一个潜在的突破性改变,因为在使用混入的组件上可能已经存在一个同名方法,不是直接调用就是通过另一个混入调用。一旦使用了混入,那就很难移除或者改变了。 差劲的理念都懒得去重构了因为重构风险太大。
混入导致滚雪球式的复杂性
尽管混入模式一开始是简单的,但随着时间的推移它慢慢变得复杂。下面的例子就是基于我见过的在一个代码库里面无用的真实场景。
一个组件需要一些跟踪鼠标移动的状态。为了让逻辑可复用,你可能要提取
handleMouseEnter()
,handleMouseLeave()
以及isHovering()
这些方法到HoverMixin
中。接下来,有人需要实现一个提示框。他们不想复制HoverMixin
里的逻辑,因此他们创建了一个使用了HoverMixin
的TooltipMixin
。TooltipMixin
读取由HoverMixin
在它componentDidUpdate()
时提供的isHovering()
方法,然后显示或隐藏这个提示框。几个月后,一些人想让这个提示框的用法可配置。在努力避免代码重复的情况下,他们在
TooltipMixin
新添加了一个支持可选项的getTooltipOptions()
方法。通过这样,显示浮层选框的组件也可以使用HoverMixin
了。然而浮层框需要一个不同的悬停延时。为了解决这个问题,一些人添加一个支持可选项的getHoverOptions()
方法,并在TooltipMixin
中实现。现在这些混入就紧紧耦合在一起了。在没有新需求时这也是可以的。但这种解决方案不能良好的扩展。如果你想支持在一个单独的组件中展示多种提示框呢?你总不能在一个组件中定义一个混入两次吧。如果提示框需要在引导下自动显示而不是悬停显示呢?祝你好运吧,将
TooltipMixin
从HoverMixin
中解耦出来!如果你需要支持在不同组件中悬停区在哪提示框的锚点就位于哪的情况呢?你又不能轻易地把混入中使用的状态提取到父组件中。和组件不一样,混入不是天生就赋予自己那样的变化。每有新需求就会使混入更难理解。使用相同混入的组件随着时间的推移,藕合度也逐渐增加。任何新功能都会被加到使用那些混入的组件中。在不重写代码或者在混入中引入更多的依赖和间接方法的情况下,就没有办法把混入分成一个“更简单”的部分了。慢慢地,可以封装的界限也模糊了。而且因为很难改变或者移动现有的混入,所以代码就越来越抽象,直到没有人能理解它到底是如何工作的。
在React之前,我们在建立应用时也面临同样的问题。发现通过声明式渲染、从上自下的数据流以及封装组件可以解决这些问题。在Facebook,我们一直在把代码迁移到可替换的模式上,对这个结果也大体满意。你可以在下面看到这些模式。
从混入迁移
我们得明确混入并不是严格意义上的弃用。如果你使用
React.createClass()
,你可以继续使用。我们只是说它对于我们来讲工作中并不好用,所以我们不建议在未来还使用它。下面的每一个部分都对应着一个我们在Facebook代码库里发现的混入使用模式。针对每一个模式,我们会描述问题以及给出一个我们认为比混入更好的解决方法。这些例子用ES5实现,但是一旦你不需要混入,如果你愿意,可以切换到ES6类的方式实现。
我们希望你会觉得这个列表有帮助。如果我们没抓到重点请告诉我们,这样我们可以改进这份列表或者证明它是错误的!
性能优化
最常用的一个混入就是
PureRenderMixin
。当props和state和之前的props和state差不多一样时,你可能会在一些阻止不必要的重新渲染的组件中用它。解决方案
为了在不使用混入的情况下达到同样的效果,你可以这样直接使用
shallowCompare
函数:如果你用自定义的混入通过不同的算法实现一个
shouldComponentUpdate
函数,我们建议从一个模块中导出那个单一的函数,在你的组件中直接调用它。我们都知道码更多的代码很烦。对于最常见的情况,我们打算介绍一个新的基类,在下一个小版本中名为
React.PureComponent
。它使用和现在用的PureRenderMixin
类似的浅对照。订阅和副作用
第二个我们碰到的最常见的混入类型是订阅一个React组件到第三方数据源。无论这个数据源是一个Flux Store, 还是一个Rx Observable,或者其他数据源,这个模式非常相似:订阅是在
componentDidMount
中被创建的,在componentWillUnmount
中被销毁,在this.setState()
中进行变化的处理。解决方案
如果只有一个组件订阅到这个数据源,将订阅逻辑嵌入到组件中也不错。可以避免过早的抽象。
如果几个使用了混入的组件订阅到一个数据源,避免重复的一个好方法就是使用名为“高阶组件”的模式。听起来有点吓人,我们将进一步仔细了解下这个模式是如何自然地从组件模型中摆脱出来的。
高阶组件讲解
让我们先暂时忘掉React。想想这两个数相加和数相乘的函数,打出各自对应的结果:
这两个函数没有一点用处,但是可以帮助我们来解释一个模型,这个模型我们随后就会用到组件中。
我们想在不改变这些函数的特征的情况下,抽出打印日志的逻辑。如何做到这点呢?优雅的解决方法就是写一个高阶函数,就是说,一个函数以函数作为参数并且返回一个函数。
确实,这听起来比它实际的样子更吓人:
withLogging
高阶函数让我们可以写出没有打印语句的add
和multiply
函数,然后用和之前完全相同的签名包装它们得到addAndLog
和multiplyAndLog
。高阶组件是非常相似的一种模式,但是是应用于React中的组件。我们将分两步来介绍如何从混入转变过来。
第一步,我们将
CommentList
组件分成两部分,一个子组件和一个父组件。子组件只关注于渲染评论。父组件会建立订阅并通过props将当前的实时数据传给子组件。还有最后一步要做。
还记得我们写的
withLogging()
接收一个函数并返回另一个包装它的函数么?我们可以应用一个类似的模式到React组件上。我们将写一个新函数命名为
withSubscription(WrappedComponent)
。它的参数可以是任何React组件。我们把CommentList
作为WrappedComponent
传进去,我们也能在代码库里任何组件上调用withSubscription()
。这个函数会返回另一个组件。返回的组件将管理订阅以及根据当前的数据渲染
<WrappedComponent />
。我们把这个模式称为“高阶组件”。
这种组合结构发生在React渲染层而不是用一个直接函数调用。这就是为什么无论外层包裹的组件是通过ES6 类或者函数的方式定义
createClass()
都没有关系。如果WrappedComponent
是一个React组件,由withSubscription()
创建的该组件可以渲染它。现在我们可以通过调用以
CommentList
作为参数的withSubscription
来声明CommentListWithSubscription
:重新审视方案
既然我们要更好的理解高阶组件,我们来看另一个不包含混入的完整解决方案。有一些小改动,已经用行内说明注释出来了。
高阶组件是一种很强大的模式。如果你想进一步自定义它们的行为,你可以传额外的参数。毕竟,它甚至不是React的一个特征。它们只是接收组件并返回包装它们的组件的函数而已。
和其他任何解决方案一样,高阶组件有它们自己的陷阱。比如,如果你严重依赖使用 refs,你可能会注意到包装一些东西到高阶组件会改变ref指向这个包装组件。在练习中我们不鼓励使用refs进行组件的通信,所以我们认为这不是一个大问题。在未来,我们可能会考虑在React中添加ref转发特征来解决这种干扰。
渲染逻辑
在我们代码库中发现的下一个混入最常用的例子就是在组件之间共享渲染逻辑。
下面是这种模式一个典型例子:
多数组件可能会共享
RowMixin
来渲染头部,每个组件都会需要定义getHeaderText()
。解决方案
如果你看到在一个混入里面有渲染逻辑,那是时候提取成一个组件了!
我们将定义一个
<Row>
组件而不是RowMixin
。我们也会将定义getHeaderText()
方法这种习惯替换成在React中标准的自上而下的数据流机制:通过props。最后,由于目前这些组件没有一个需要生命周期的挂钩和状态,我们可以用简单的函数声明它们:
使用props可以保持组件依赖清晰,容易替换,并且可以用像Flow和TypeScript这样的工具进行执行。
上下文环境
我们发现,另一组混入类别是提供以及消费React环境的帮助类。上下文环境,经验上说来是一个不稳定的特征,存在确定的问题,在未来很可能会改变它的API。我们不建议使用它,除非你确信没有其他方法来解决你的问题。
不过,如果你现在已经使用了上下文环境,你可能已经用过混入隐藏了它的用法,像这样:
解决方案
在上下文环境API固定之前,我们赞同使用组件中隐藏上下文用法是一个不错的点子。但是,我们还是建议使用高阶组件而不是这样的混入。
让包装组件从上下文中攫取一些信息,然后通过props向下传递到被包装的组件:
如果你正在使用只提供混入的第三方库类,我们支持你提出问题时链接到这篇文章,这样他们可以提供一个高阶组件的方案。同时,你可以以同样的方式自己围绕这个问题创建一个高阶组件。
公用方法
有些时候,混入是唯一用来在组件之间分享公用函数的方法:
解决方案
将工具函数放到规范的JavaScript模块中再导入他们。这样在测试以及在你组件中外部使用它们时也更容易点。
其他使用案例
有些时候,大家使用混入是用来选择性地在一些组件中添加日志记录生命周期的变化联系。以后,我们打算提供一个官方的开发工具API,让大家在不接触组件下实现一些类似的功能。但一直有非常多在进行中的工作。如果你严重依赖于打印混入日志进行调试,你可能会想使用这些混入再长久一点。
如果用一个组件,或者高阶组件,或一个公用模块都无法实现你要的,这可能意味着React应该提供超出它之外的功能。提一个问题告诉我们你对于混入的使用案例,我们会帮你考虑可选方案或者也许帮你实现功能需求。
传统意义说来,混入不是不宜使用的。你可以通过
React.createClass()
一直使用它们,我们不会再进一步改变它了。最后,随着ES6类获得更多的采用以及它们在React中的可用性问题被解决,我们可能会把React.createClass()
分离成一个单独的包,因为大部分人应该不再需要它了。即使是那样的情况,以前的混入也会继续保持工作。我们相信对于绝大多数案例来说上面的替换方案会更好一些,我们也诚邀大家在写React应用时尽量不使用混入。
The text was updated successfully, but these errors were encountered: