本章将讲述React 代码库的组织,约定,和它的实现方式。
如果你想更加关注Reac t,或者说作为开发贡献者,对React 进行一些修改,这篇博客或许可以帮到你。
当然,我们没必要去过度的关注React 应用的约定,因为其中有很多是历史遗留问题,后续版本可能会被pass 掉。
1.自定义模板系统
在Facebook ,他们内部人员使用了一个叫做Haste 的自定义模板系统,这个系统非常类似CommonJS 规范,也使用require() ,但是又有些不同从而让部分开发者很尴尬,迷糊着。
在CommonJS 中,你使用相对路径来导入一个模板
var setInnerHTML = require('./setInnerHTML');
var setInnerHTML = require('../utils/setInnerHTML');
var setInnerHTML = require('../client/utils/setInnerHTML');
上述的setInnerHTML 可以在多个文件夹中存在,但是,在React 代码库中,您可以导入任何模板与其他模板的名称,Haste 要求名称必须是全局唯一的。
var setInnerHTML = require('setInnerHTML');
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 模块去警告。
var warning = require('warning');
warning(
2 + 2 === 4,'Math is not working today.'
);
当第一个条件为false 时,第二条信息就会展现。
有一个问题注意的事,这个warn 只是用警告非异常错误,因为异常错误,比如说语法等等,console 会帮我们处理,我们不要去交叉他们的功能。
var warning = require('warning');
var didWarnAboutMath = false;
if (!didWarnAboutMath) {
warning(
2 + 2 === 4,'Math is not working today.'
);
didWarnAboutMath = true;
}
当然一般警告只是在开发过程中,而实际的产品中这一部分一般都会被剔除掉,你可以通过改为调用invariant 来代替。
var invariant = require('invariant');
invariant(
2 + 2 === 4,'You shall not pass!'
);
你可以认为这种方式是一种断言。 我们要尽可能保持开发和最后的应用代码基本一致,invariant 在产品中会自动将打印出错误的信息。
7.开发和产品
你可以使用__DEV__ 伪全局变量来控制一段代码块。 在编译器中会转换为process.env.NODE_ENV!=='production' 去区分产品还是开发。 通过if 判断来确定是处于开发还是产品阶段
if (__DEV__) {
// This code will only run in development.
}
8.JSDoc
JSDoc 是一个根据javascript 文件中注释信息,生成JavaScript 应用程序或库、模块的API 文档 的工具。
JSDoc 本质是代码注释,所以使用起来非常方便,但是它有一定的格式和规则,只有先了解这些,才能进行接下的工作那么比如生产文档,生成智能提示都可以通过工具来完成。
JSDoc 注释一般应该放置在方法或函数声明之前,它必须以/ ** 开始,以便由JSDoc 解析器识别。其他任何以/* ,/*** 或者超过
3
个星号的注释,都将被JSDo c解析器忽略。
/** * Updates this component by updating the text content. * * @param {ReactText} nextText The next text content * @param {ReactReconcileTransaction} transaction * @internal */
receiveComponent: function(nextText,transaction) {
},
其中@ 开头的是一个特殊的注释标签,至于什么意思建议大家去官方看看,这里就不提太多,而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 ,只能在此一笑。
一般来说,你可能会看到如下的一些代码
function ReactDOMComponent(element) {
this._currentElement = element;
}
ReactDOMComponent.Mixin = {
mountComponent: function() {
}
};
Object.assign(
ReactDOMComponent.prototype,ReactDOMComponent.Mixin
);
module.exports = ReactDOMComponent;
上述的Mixin 和我们React 提到的Mixins 多继承并没有直接的联系,他只是一个种打包函数的方式,让这些函数之后可以用在其他类上,即便是我们要尽量避免他,但是在某些地方使用这种模式确实是不错的。
而写成ES6 就是如下:
class ReactDOMComponent {
constructor(element) {
this._currentElement = element;
}
mountComponent() {
// ...
}
}
module.exports = ReactDOMComponent;
有时候我们会将非ES6 代码转换为ES6 代码,然而,这个并没有什么必要性,虽然官方推荐使用ES6 语法,但是一些方法在ES6 上并没有得到很好的实现比如说这里的Mixins 多继承方式,在ES6 总没有很好的实现,我们前面说的Mixins 多继承也是在React.createClass 的ES5 风格代码下使用的就即将出来的ES7 中或许有解决方法比如说修饰器。
10.动态注入
React 在某些模块上使用动态注入,虽然他的目的非常明确,但是非常不幸的是,他阻碍了对代码的理解,最开始的React 只是单单为DOM 元素服务的,由于React Native 开始作为React 项目的分支,才导致React 开发者不得不增加动态注入模块以支持React Native 的使用。
如果你看过一些模块,你会发现他们的动态注入方式如同下面:
var textComponentClass = null;
function createInstanceForText(text) {
return new textComponentClass(text);
}
var ReactHostComponent = {
createInstanceForText,
injection: {
injectTextComponentClass: function(componentClass) {
textComponentClass = componentClass;
},},};
module.exports = ReactHostComponent;
在React DOM 中,ReactDefaultInjection 注入了一个DOM 实例
ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent)
在React Native 中,ReactNativeDefaultInjection 注入了他自己的一个实例
ReactHostComponent.injection.injectTextComponentClass(ReactNativeTextComponent)
以后这种机制可能会被取代掉。
11.多重包
React 是一个monorepo 模型(该模式特点自行百度),它的代码库包含多个分离的包以至于他们变化时可以相互协调,可以分开编译打包测试。
npm 的元数据比如说package.json 被放在一个顶级文件夹packages中,但是这个文件夹中除了这玩意基本没有什么代码。 比如packages/react/react.js ,真实接口其实在 src/isomorphic/React.js 。
虽然代码被分离,但是npm 包和brower 包是不同的,所以要注意。
12.React 核心
React 的核心就是所有顶级API ,举几个例子
React.createElement()
React.createClass()
React.Component
React.Children
React.PropTypes
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 中调解器的实现细节了
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|