React学习之相关代码库(三十六)

本章将讲述React代码库的组织,约定,和它的实现方式。

如果你想更加关注React,或者说作为开发贡献者,对React进行一些修改,这篇博客或许可以帮到你。

当然,我们没必要去过度的关注React应用的约定,因为其中有很多是历史遗留问题,后续版本可能会被pass掉。

1.自定义模板系统

在Facebook,他们内部人员使用了一个叫做Haste的自定义模板系统,这个系统非常类似CommonJS规范,也使用require(),但是又有些不同从而让部分开发者很尴尬,迷糊着。

在CommonJS中,你使用相对路径来导入一个模板

1// 同一目录下 2var setInnerHTML = require('./setInnerHTML'); 3 4// 不同目录下 5var setInnerHTML = require('../utils/setInnerHTML'); 6 7// 多层 8var setInnerHTML = require('../client/utils/setInnerHTML'); 9

上述的setInnerHTML可以在多个文件夹中存在,但是,在React代码库中,您可以导入任何模板与其他模板的名称,Haste要求名称必须是全局唯一的。

1var setInnerHTML = require('setInnerHTML'); 2

Haste最开始被设计出来就是为了开发大型项目的,比如Facebook,你可以将你需要的文件放在不同的文件下面,导入时也不用使用什么相对地址,通过一个全局唯一的名字,它可以帮你定位到文件,这也是Haste的特点,文件名必须唯一,在同一个项目中不能同时出现两个相同的文件名。

React他自身就是从Facebook这个大型项目的代码库分离出来的,所以它会保留一些Haste的一些特性,在以后的版本,React可能会使用CommonJS或者ES6的模板导入来代替它,当然,在Facebook内部的话可能会很难发生改变,毕竟项目这么大,基本架构都已成熟。

这是一些Haste的规则:

  • 在React源码库中文件名必须唯一,$[$这也是它的一些代码会比较冗长的原因$]$

  • 当你导入一个新的文件时,你的文件必须包含一个许可头,你可以从已经存在的代码库中得到这个许可头,这个许可头类似于@providesModule setInnerHTML,github地址,在新文件中,你只要对应着修改就可以了。

  • 在导入时别使用相对路径,不写require('./setInnerHTML'),而写require('setInnerHTML')。

当我们使用npm来处理React时,一个脚本会复制所有的模块到一个叫做lib文件夹中,这种方式就是用require加绝对或者相对地址来处理,在Nodejs,browerify,webpack和其它一些工具都是这样来处理React模块的,但是这和Haste并没有什么关系。

2.外部依赖关系

React一般都没有什么外部依赖,但是比如fbjs,Relay等等,虽然不是React的公共API,但是都是Facebook内部分离出来的,不能算是一种外部依赖。

3.顶级文件夹

通过查看React的库,我们可以看到如下的文件结构:

  • src是React的源代码文件夹,如果你要修改相关代码,src你可能需要花大部分时间去看看,学习学习

  • docs是React文档网页夹,如果你改变了API的话,就有必要更新相关文档文件。

  • examples包含React demo的一部分事例

  • packages包含一些在React代码库中的代码分布结构(比如package.json)。

  • build是React build命令的输出。

还有其他的文件夹,这里就不多说了。

4.单元测试

一般都没有为了单元测试而存在的顶级目录,相反,我们会将这些测试文件放在一个相对他们每一个需要测试的源代码文件叫做_tests__的目录中。

例如,你测试setInnerHTML.js的时候,你需要在相同目录下建立一个__tests__目录来放它的单元测试文件,tests/setInnerHTML-test.js

5.分享代码

尽管Haste允许我们可以在代码库的任何地方导入模块,但是我们为了避免循环包容,按照约定,一个文件只能导入同目录下或者是子目录下的模块。
例如一些文件在src/renderers/dom/stack/client下面可能会导入在其他文件下的文件。

按照约定他是不能导入src/renderers/dom/stack/server 下的文件的,因为它不是src/renderers/dom/stack/client的子目录。

如果我们要同时使用两个模块的功能的话,那么可以在他们的最近公共祖先文件中建立一个shared目录来处理他们。

比如src/renderers/dom/stack/client和src/renderers/dom/stack/server有功能我要一起用,我就可以放在这个文件里实现src/renderers/dom/shared

又比如src/renderers/dom/stack/client和src/renderers/native就可以放在src/renderers/shared中。

6.警告和不变量

React代码库使用了warning模块去警告。

1var warning = require('warning'); 2 3warning( 4 2 + 2 === 4, 5 'Math is not working today.' 6); 7

当第一个条件为false时,第二条信息就会展现。

有一个问题注意的事,这个warn只是用警告非异常错误,因为异常错误,比如说语法等等,console会帮我们处理,我们不要去交叉他们的功能。

1var warning = require('warning'); 2 3var didWarnAboutMath = false; 4if (!didWarnAboutMath) { 5 warning( 6 2 + 2 === 4, 7 'Math is not working today.' 8 ); 9 didWarnAboutMath = true; 10} 11

当然一般警告只是在开发过程中,而实际的产品中这一部分一般都会被剔除掉,你可以通过改为调用invariant 来代替。

1var invariant = require('invariant'); 2 3invariant( 4 2 + 2 === 4, 5 'You shall not pass!' 6); 7

你可以认为这种方式是一种断言。
我们要尽可能保持开发和最后的应用代码基本一致,invariant在产品中会自动将打印出错误的信息。

7.开发和产品

你可以使用__DEV__伪全局变量来控制一段代码块。
在编译器中会转换为process.env.NODE_ENV!=='production'去区分产品还是开发。
通过if判断来确定是处于开发还是产品阶段

1if (__DEV__) { 2 // This code will only run in development. 3} 4

8.JSDoc

JSDoc是一个根据javascript文件中注释信息,生成JavaScript应用程序或库、模块的API文档 的工具。

JSDoc本质是代码注释,所以使用起来非常方便,但是它有一定的格式和规则,只有先了解这些,才能进行接下的工作那么比如生产文档,生成智能提示都可以通过工具来完成。

JSDoc注释一般应该放置在方法或函数声明之前,它必须以/ **开始,以便由JSDoc解析器识别。其他任何以/*,/***或者超过$3$个星号的注释,都将被JSDoc解析器忽略。

1/** 2 * Updates this component by updating the text content. 3 * 4 * @param {ReactText} nextText The next text content 5 * @param {ReactReconcileTransaction} transaction 6 * @internal 7 */ 8receiveComponent: function(nextText, transaction) { 9 // ... 10}, 11

其中@开头的是一个特殊的注释标签,至于什么意思建议大家去官方看看,这里就不提太多,而Facebook好像没有使用这种方式,而是使用Flow类型检测工具来进行处理。

8.Flow

这里提一下Facebook的一个类型检测的牛逼东西,Flow–javascript类型检测,当你在你的许可头中增加了@flow这样的注释标签后,这个文件就会被自动进行类型检测。

咦,类型检测到底是什么鬼,这玩意都快自成一个体系,我也说不了多少,就简简单单的解释一下。

Flow是Facebook公开的一个开源javascript静态类型检测器,旨在发现javascript程序中的类型错误,以此提高程序员开发程序的效率和代码质量,非常快速也方便,可以很精确的判断当前的函数调用或者其他的数据类型是否正确,提供官方地址:自己学去吧,涉及到类型注解啊,Flow类型系统的工作原理啊,怎么配置安装啊,等等等。https://flow.org/en/docs/

9.类和Mixin的区分

React最开始是用ES5来写的,后面自从Babel出来后,就开始支持ES6了,然而,现在大部分社区成员依旧用ES5来写,原因大家也懂,兼容性,可想而知,低版本的浏览器是有多不支持ES6,只能在此一笑。

一般来说,你可能会看到如下的一些代码

1// Constructor 2function ReactDOMComponent(element) { 3 this._currentElement = element; 4} 5 6// Methods 7ReactDOMComponent.Mixin = { 8 mountComponent: function() { 9 // ... 10 } 11}; 12 13// Put methods on the prototype 14Object.assign( 15 ReactDOMComponent.prototype, 16 ReactDOMComponent.Mixin 17); 18 19module.exports = ReactDOMComponent; 20

上述的Mixin和我们React提到的Mixins多继承并没有直接的联系,他只是一个种打包函数的方式,让这些函数之后可以用在其他类上,即便是我们要尽量避免他,但是在某些地方使用这种模式确实是不错的。

而写成ES6就是如下:

1class ReactDOMComponent { 2 constructor(element) { 3 this._currentElement = element; 4 } 5 6 mountComponent() { 7 // ... 8 } 9} 10 11module.exports = ReactDOMComponent; 12

有时候我们会将非ES6代码转换为ES6代码,然而,这个并没有什么必要性,虽然官方推荐使用ES6语法,但是一些方法在ES6上并没有得到很好的实现比如说这里的Mixins多继承方式,在ES6总没有很好的实现,我们前面说的Mixins多继承也是在React.createClass的ES5风格代码下使用的就即将出来的ES7中或许有解决方法比如说修饰器。

10.动态注入

React在某些模块上使用动态注入,虽然他的目的非常明确,但是非常不幸的是,他阻碍了对代码的理解,最开始的React只是单单为DOM元素服务的,由于React Native开始作为React项目的分支,才导致React开发者不得不增加动态注入模块以支持React Native的使用。

如果你看过一些模块,你会发现他们的动态注入方式如同下面:

1// Dynamically injected 2var textComponentClass = null; 3 4// Relies on dynamically injected value 5function createInstanceForText(text) { 6 return new textComponentClass(text); 7} 8 9var ReactHostComponent = { 10 createInstanceForText, 11 12 // 提供一个动态注入 13 injection: { 14 injectTextComponentClass: function(componentClass) { 15 textComponentClass = componentClass; 16 }, 17 }, 18}; 19 20module.exports = ReactHostComponent; 21

在React DOM中,ReactDefaultInjection注入了一个DOM实例

1ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent); 2

在React Native中,ReactNativeDefaultInjection注入了他自己的一个实例

1ReactHostComponent.injection.injectTextComponentClass(ReactNativeTextComponent); 2

以后这种机制可能会被取代掉。

11.多重包

React是一个monorepo模型(该模式特点自行百度),它的代码库包含多个分离的包以至于他们变化时可以相互协调,可以分开编译打包测试。

npm的元数据比如说package.json被放在一个顶级文件夹packages中,但是这个文件夹中除了这玩意基本没有什么代码。
比如packages/react/react.js,真实接口其实在 src/isomorphic/React.js。

虽然代码被分离,但是npm包和brower包是不同的,所以要注意。

12.React核心

React的核心就是所有顶级API,举几个例子

1React.createElement() 2React.createClass() 3React.Component 4React.Children 5React.PropTypes 6

React核心仅包含一些定义组件要用的API,他不包括调解器算法或者其他为了解决平台跨浏览器的代码。这个核心被React DOM和React Native组件所使用的。

核心代码在src/isomorphic中,如果大家要看请到github中查看,在npm中作为一个react包下载,而在浏览器环境下则是react.js来处理,形成一个全局变量React。

注意

如今很多核心代码都被分离,已经不能算是核心代码。

13.渲染器(Renderers)

即便是React接受了移动平台React Native,有一点不会变,那就是React最开始是为DOM而生的,这一小部分将介绍一个React内部的”渲染器”。

渲染器管理一个React树是怎么转换为底层调用的。

渲染器源代码在src/renders中。

  • React DOM渲染器渲染React组件到DOM中,它实现了一个ReactDOM来处理,在npm中我们会导入react-dom,而浏览器端则用react-dom.js来形成一个ReactDOM全局操作接口。

  • React Native渲染器将React组件渲染到移动视图中,它一般是调用React Native的react-native-renderer来处理,这一部分在将来可能会被嵌入React Native的代码库中,来满足React的更新。

  • React Test渲染器渲染React组件到JSON树中,这个东西一般都已依赖于测试工具Jest或者其他,在npm包管理中,直接下载react-test-renderer即可。

其他官方支持的渲染器就只有react-art了,是一个图形化渲染器。

14.解调器(Reconcilers)

在之前说了渲染器的几种,不然,即便是这几种渲染器截然不同,但是他们都需要使用共享功能或者是会共享底层实现模块逻辑。

尤其是,各个渲染器中的解调器算法应该基本一致,这样才可以让声明式渲染,自定义组件,状态,生命周期,refs实例,在跨平台上显示的效果一致。

为了解决这个问题,不同的渲染器的底层代码需要一致,功能需要一致,在React称这一部分功能为调解器,当一个更新(setState())被激活时,我们的调解器就会调用组件的render()去更新树,或是装载,卸载,等行为。

在React中的调解器暂时无法被单独拿出来,因为它暂时还没有公共API可以供上层应用直接调用,但是,渲染器(React DOM 和React Native)处理得很好,可以非常合理的使用它。

15.堆栈调解器

堆栈调解器是现在所有React产品的重要组成部分,核心中的核心,它在src/renderers/shared/stack/reconciler中,同时被React DOM和React Native使用。

它是使用面对对象的方法实现的,作用是用来维护所有React组件的内部实例所构造出的单独的树。这些内部实例包括用户自定义的组件,也包括平台元素即DOM标签,这些内部实例无法直接给用户使用,他们都是透明的。

当一个组件装载,更新,卸载时,堆栈调解器就会调用在这些实例上的一些方法来处理他们,这些方法:mountComponent(element), receiveComponent(nextElement), 和unmountComponent(element).

Host Components(DOM组件)

平台特殊组件又名Host后面基本就用这个名字吧,Host,如同<div>这一类型的组件。他们在处理时会运行专门为他们准备的平台特殊的代码。例如,React DOM会指示堆栈调解器去使用ReactDOMComponent去处理装载,更新,卸载DOM组件。

先不管平台问题,DOM标签会使用类似的方法来处理他们的孩子,为了方便,堆栈调解器提供了一个叫做ReactMultiChild的帮手来帮助在DOM和移动平台上渲染使用。

Composite Components(组合组件)

用户自定义的组件叫做组合组件,这种组件在所有的渲染器中都应该表现出相同的效果,这也是为什么堆栈调解器在ReactCompositeComponent类中提供了一个render的共享实现。

组合组件也实现了装载,更新,卸载,但是不像host组件,ReactCompositeComponent 怎么表现或者渲染成什么样子都取决于用户的代码,这就是为什么他可以在用户自定义的组件调用一些方法,如同render和componentDidMount等。

在更新过程中,ReactCompositeComponent会检查render出来的数据是否跟前一个状态的type,key有不同,如果相同则会抛弃这次孩子节点的更新,然后递归到子内部实例中,否则就会卸载旧的实例,装载新的,这部分在之前的调解器或者是更新博客已经讲到。

(这里有一点需要区分的是,我们进行更新操作的目标永远都是子节点DOM,为什么呢,因为我们的组件是不会加入到浏览器中的,加入浏览器的都是真实的DOM节点)

Recursion(递归)

在更新的时候,堆栈调解器会“向下探索”穿过组合组件,调用他们的render方法,然后绝对是否是更新还是替换掉他们的单一的子孩子实例,然后它会通过host组件去执行平台特殊代码,host组件可能有多层孩子那么就会递归去处理他们。

去理解堆栈调解器在一次流程中如何同步的处理组件树是有必要的,然而个别树分子可能会脱离调解器可以处理的范围,而堆栈调解器又不会阻止,所在在CPU资源被限制的条件下我们需要确保我们递归更新时的子最优或者说是尽可能不更新,防止更新带来过多的资源消耗。

在下一篇博客中我将讲述更多堆栈调解器的一些细节。

16.纤维调解器

这个纤维调解器的存在是为了解决在堆栈调解器中长期存在的一些问题。
这是一个完全重写的调解器,不同的思路,不同的方法,如今这个调解器牛逼人士们正在积极的研究当中,相信不久就能够出来了,期待啊。

它的几个主要的目的:

在大型项目中可以分离出可中断的工作

在项目开发中能更好的优化序列,重定位基准,功能可重用性

可以在父亲和孩子组件之间可以来回传递。

用render()不需要限制一定只能有一个父元素

更准确的识别出错误

你如果想更多的了解纤维调解器,github地址:src/renderers/shared/fiber

17.事件系统

React实现了合成事件,这部分属于共享代码,React DOM和React Native中都有。
src/renderers/shared/shared/event.

18.add-ons

这一部分额外工具在src/addons目录,有兴趣的可以去看看源代码

下一篇将讲React中调解器的实现细节了

代码交流 2021