我们写这个文档是为了让你更好地了解我们是如何决定React该做什么和不该做什么,以及我们的开发理念。 虽然我们很高兴看到社区做出的贡献,但我们不太可能选择一条违反原则的路。
注意: 这篇文章假设你已经对React有深刻的理解。它描述了React本身的设计原理,而不是React组件或者应用。 有关React的介绍,请参考 Thinking in React
React的关键特性就是组件的组合。不同人员写的组件放一起也能很好地运作。对我们来说很重要的是,为一个组件增加方法不需要在代码层面做出一系列的变化。
例如,组件可以引入一些局部变量,而不改变任何使用它的组件。相似的,也可以在必要时对组件增加初始化和代码拆分。
在组件内使用状态以及生命钩子没有任何“坏处”。就像任何强大的功能都应该被适度使用,而我们也不会打算删掉它们。相反,我们认为它们是让React有用的部件。我们将来可能会启用更多的函数模式,但是本地状态和生命周期钩子都将成为该模型的一部分。
组件通常被描述为“仅仅是函数”,但在我们看来,它们需要变得更有用途。在React中,组件描述任何可组合的行为,它包括渲染,生命周期以及状态。 一些外部库(如Relay)增加了具有其他职责的组件,例如描述数据的依赖关系。这些想法也可能会以某种形式放到“React”中。
通常情况下,我们反对添加本来应该是在用户自己能实现范围内的功能,不想让你们的应用因为无用的库代码而变得庞大。但这也有一些例外。例如,如果React不提供本地状态和生命周期钩子的支持,人们会对状态和生命周期钩子做自定义抽象。当有多个抽象组件同时竞争时,React不能强制人们使用这些的属性。它只能使用它们最小的公共特性工作。
这就是为什么我们有时候给React本身添加功能。如果我们发现很多组件实现了不兼容或者效率低的功能,我们会优先把它放入React中。我们并不会轻率地做这件事。当我们做了,是因为我们有信心能够提高抽象层,使整个生态系统受益。状态,生命周期钩子,标准化跨浏览器事件都是很好的例子。
React是十分务实的,它靠facebook的产品需求驱动。虽然它受到一些还没成为主流的范式影响,比如函数式编程,但它有个明确的目标,就是让越来越多不同技能和经验水平的开发者接触到这个框架。
如果我们想要弃用我们不喜欢的模式,我们需要很负责任地考虑到所有存在的用例以及在弃用前需要告诉社区它的替代品。如果某种模式已经对构建应用非常有用,但很难以声明的方式来表述,那么我们会提供一个命名式的API。如果在很多应用里我们发现一些必要的东西,却没法提供完美的API,那我们会提供一个临时的不标准运作API,只要之后我们能够处理它并且为未来的改进留一条后路。
我们非常重视 API 的稳定性。在Facebook,我们拥有超过2万个使用React的组件,很多其他公司,包括Twitter以及Airbnb,也是React的重度用户。这也是为什么我们不愿意去改公共API或者行为。
然而,我们认为稳定并不意味着没有变化。因为它很快就会变成停滞。相反,我们更倾向于“在生产环境中被大量使用,当某些东西发生变化了,会有一条清晰的(最好是自动的)迁移之路”。
当我们弃用某一种模式,我们研究它在Facebook的内部使用,以及添加弃用警告。他们会让我们评估这次变化的影响。有时候如果我们发现这样的改变太早了,会回滚回去,而且我们需要更加策略性地考虑,代码是否为这次改动做好了准备。
如果我们有信心这次改变不会太具破坏性,而且迁移策略是对所有用户都使用,我们会把所有弃用警告发布到开源社区。我们会和Facebook以外的React用户保持紧密联系,而且会监视一些流行的开源项目,指导它们修复这次弃用造成的问题。
鉴于Facebook React庞大的代码库,成功的内部迁移会起很好地标识作用,告诉其他公司迁移时也不会碰到问题。尽管如此,有时候人们会指出会有我们没有考虑到的用例,我们会专门为它们添加后门,或者重新考虑我们的解决方案。
我们不会因为任何不够好的理由而弃用。我们认识到,弃用警告有时候会导致挫败感,但是我们添加它们是因为弃用那些会为了改善和增加新功能铺路,我们以及社区里很多人都认为这是有价值的。
例如,我们在React 15.2.0里增加了一个关于未知DOM属性的警告。很多项目都会被其影响。然后修复这个警告是很重要的,以至于我们在React里引入了自定义属性的支持。每当我们增加一个弃用,后面都会加上一个像这样的原因。
当我们添加一个弃用警告,我们会在当前剩下的主版本里保留它,在下一个主版本里改变行为。如果有很多重复性的工作,我们会发布一个codemod脚本来自动化大部分改动。 codemod能够让我们向前移动,不会在大量的代码里停滞不前,而且我们也鼓励你使用它们。
你会在react-codemod里找到我们发布过的codemod。
我们将现有系统的互通性放在很高的位置,并且逐步使用。Facebook有非常庞大的非React代码块。它的网站混合了一种称之为XHP的服务端组件系统,以及在React出现之前公司内部的UI库,还有React。对我们来说重要的是,任何产品团队能够为小功能而开始使用React而不是重写他们的代码。
这就是为什么React为了让可变模块一起运作提供了紧急出口,并且尝试与其他的UI库一同运作。你可以把一个已经存在的命令式UI包裹进声明式组件,反之亦然。这也是逐步使用的关键。
即便你的组件被描述为函数,你不会在使用React时直接调用它们。每一个组件返回一个需要被渲染成什么样的描述,并且这个描述会包含用户编写的组件例如和比如
这是一个微小但强大的功能。既然你不会调用那个组件方法而让React去做,这就意味着React有权在必要时延迟调用它。当前的实现,React递归遍历了并更新整个树,在单个tick时间内调用渲染方法。然而未来可能会延迟某些更新来避免掉帧
这是React设计的一个共同主题。一些流行的库会实现“推(push)”方法,当新的数据可用时就进行计算。然而,React坚持使用“拉(pull)”方法,计算可以被推迟到必要时。
React不是一般的数据处理库。它是一个构建用户界面的库。我们认为它的独特定位在于,感知哪些计算是和组件相关的,而哪些不是相关的。
如果某些内容不在可视屏幕范围内,我们可以推迟它的相关逻辑计算。如果数据比帧率更快,我们可以合并并且批量更新。我们可以优化来自用户交互的工作(例如一次按钮点击的动画)而不是不重要的后台工作(例如从网络中加载新内容),以避免掉帧。
很明显,我们当前还没好好利用这一点。然而,能如此自由的控制调度,也是更喜欢它的原因,以及为什么setState()
是异步的。从概念上来讲,我们认为这是一个调度更新。
如果我们让用户直接以“推(push)”的范式在一些函数式响应编程里组合视图,控制调度对我们来说会变得更加困难。我们想要自己拥有这个“胶水”代码。
大量用户代码在React前执行,这是React的一个关键目标。这保证了React有能力调度以及根据它所知的UI分块执行任务。
团队内部有一个玩笑,React应该被称之为“调度”,因为它不想变得完全“响应”。
对我们来说,提供一个良好的开发体验很重要。
例如,我们维护React DevTools,它让你能够在Chrome以及Firefox里检查React组件树。我们听说它给facebook工程师以及社区带来了巨大的生产力提升。
我们也尝试更多的努力提供更有帮助的开发警告。例如,在开发过程中如果以某种浏览器无法识别的方式嵌套标签或者在API里发生拼写错误,React都会给你警告。给开发者警告以及做相关的检查都是React开发版本比生产版本速度慢的主要原因。
我们在facebook内部遇见的使用方式帮助我们知道了哪些是常见错误,以及怎么提前预防它们。这样,当我们增加新特性时,我们会尝试预测常见错误,以及发出警告。
我们经常寻找提高开发者体验的方式。我们非常高兴听到你们的意见以及接受你们的贡献,使其更加强大。
当代码运行不正确时,通过面包屑来追踪源代码中的错误变得十分重要。在React里,属性和状态就是那些面包屑。
如果你看到屏幕上有什么错误,你可以打开React开发者工具,找到负责渲染的组件,然后看一下属性和状态是否正确,如果它们正确,那你就知道问题出在组件的render()
方法里,或者一些被render()
调用的方法。这样问题就被分离出来了。
如果这个状态是错的,你就知道问题是由于该文件某次调用setState()
导致的。这也是相对简单的定位以及修复,因为通常情况下,在一个文件中只有很少几次的setState()
调用。
如果属性是错误的,那你可以在检测器中向上遍历这个树,查找向下传递错误属性的组件是哪个。
这种追踪任何UI到当前属性和状态形式下产生数据的能力对于React来说很重要。这是一个明确的设计目标,状态不是被困在闭包或者组合器中,而是直接对React有用。
虽然UI是动态的,但我们相信,属性和状态的同步render()
函数,把调试从基于猜测变的虽然枯燥但能看到结果。我们想要保留React的这个限制,即使它使一些用例更困难,比如复杂的动画。
我们发现全局运行时的配置选项是有问题的。
例如,它偶尔要求我们实现像React.configure(options)
或者React.register(component)
一样的方法。然而,这带来好几个问题,我们并没有发现好的解决方法。
如果有人从一个第三方的组件库调用这样的函数,那会怎么样?如果一个React应用嵌入另一个React应用程序,并且它们要求的配置是不兼容的,那该怎么办?一个第三方的组件如何指出它所需要的特定配置?我们认为全局配置使得组合不能很好地运作。由于组合是React的核心,所以我们不在代码里提供全局配置。
然而,我们在构建层提供一些全局的配置。例如,我们提供独立的开发和生产构建。未来我们也可以增加一个 __PROFILE__构建,而且我们也考虑其他的构建标志。
我们看到React的价值,它允许我们写组件时出现更少的bug,而且能很好地组合起来。DOM是React的最初渲染目标,但是React Native对Facebook和社区有着同样重要的意义。
兼容渲染器(renderer-agnostic)是React相当重要的设计约束。它在内部处理时增加了开销。另一方面,任何对于核心内容的改进都是跨平台的。
我们尝试在任何地方都提供优雅的API,我们很少会担心是否需要优雅的实现。真实世界远远达不到完美,在某种程度上,如果用户不需要写它,我们更倾向于把丑陋的代码放入库里。当我们评估新代码时,我们会寻找一种正确的实现,它能更加高效并且能够提供一个很好的开发体验。优雅是次要的。
相对于聪明的代码,我们更喜欢枯燥的代码。代码是一次性的而且会经常变。所以重要的是,它不会引入新的内部抽象,除非绝对必要。冗余的代码容易移动,改变和删除,而优雅的代码过早的抽象化,以至于难以改变。
拥有一个单一的编程模型可以让我们围绕产品而不是平台来组建工程团队。到目前为止,这项交易对我们来说是值得的。
一些常用的API通常有冗余的名称。例如,我们使用componentDidMount()
而不是didMount()
或者onMount()
。这是故意的。这样做是为了与库的交互点显而易见。
在一个像Facebook那么庞大的代码库里,能够为特定的API使用搜索是十分重要的。我们会重视那些不同的冗余的名称,特别是应该谨慎使用的特性。例如,dangerouslySetInnerHTML
在代码审查时很难被漏掉。
对于搜索的优化也是十分重要的,因为我们依赖codemods做出的重大改变。我们希望它能够自动地对代码库进行改动,并且保证简单安全,唯一冗余的名称帮助我们实现了这一点。相似的,独特的名称使得写自定义的React lint rules也变得简单,不必担心潜在的误报。
JSX扮演了同样的角色。虽然它不需要React, 但我们在facebook中广泛使用它,由于它好看以及实用。
在我们的代码库里,JSX为处理React元素树工具提供了一个不太�明显的提示。这也使得构建时间优化如常量提升,安全lint以及codemod内部组件使用,以及把JSX源定位放入警告中。
我们尽力解决社区提出的问题。然而我们有可能优先考虑的是facebook内部人员同样经历的问题。也许反直觉,我们认为这是社区这么关注React的重要原因。
内部的大量使用给我们信心,React不会在明天消失,它在facebook为了解决问题而内部创建的。它为公司带来了有形的商业价值以及被很多它的产品使用。众测意味着我们可以保持敏锐的视角,朝着一个方向集中前进。
这不意味着我们会忽视社区的问题。例如,我们为React添加了网页组件以及SVG支持,尽管我们内部不依赖于它们中的任何一个。我们正在积极聆听你们的痛点,并且用我们最大的能力解决。社区就是让React更加特别,而我们很荣幸回馈社区。
在Facebook发布了许多开源项目后,我们已经了解到,尝试让每一个人在开心的同时产生了一些专注度低,成长不好的项目。相反,我们发现选择一个小众的,并且专注于让他们高兴地带来积极的影响。这就是我们使用React所做的。而且到目前为止,解决Facebook产品团队遇到的问题已经很好的反馈到了开源社区。
这种方式的缺点在于,有时候我们不能足够地关注Facebook团队没有处理的事情,就像“开始”体验。我们敏锐地意识到这点,我们正在考虑如何以一种对社区中所有人有利的方式来改善,而不会产生我们之前在开源项目中遇到的问题。
作者简介
周昊 滴滴上海前端团队高级前端工程师,曰天,曾就职于SAP,深耕React、对mobx、redux等有深入的实践经验,王者荣耀王者段位。