每次都信誓旦旦的给自己立下要好好学习react源码的flag,结果都是因为某个地方卡住了,或是其他原因没看多少就放弃了。这次又给自己立个flag-坚持看完react源码。为了敦促自己,特开设这样一个专栏来记录自己的学习历程,这意味着这个专栏的文章质量并不高,你可以拿来参考参考,切莫全信,我不想误人子弟,后面要是学有所成再考虑产出些好点的文章。 要是发现文章中有什么不当之处,欢迎批评交流。我看的源码版本是
16.8.2
。
为了看react源码,我查找了不少资料,这里推荐两个参考资料,个人觉得写得不错。
- 慕课网一个课的电子书,他有个源码解析的视频教程,应该不错,不过我没买。
- 一个知乎专栏,写得很清晰,只不过是
15.6.2
的, 在react16
里面一些方法找不到了。
本篇文章是官方文档里边的一篇文章的翻译,原文地址。
这部分将给你介绍下react代码的基本结构, 代码约定和它的基本实现.
如果你想为react贡献代码的话, 我们希望这篇指南能让你写代码更加舒服.
我们不推荐将这些约定用在react应用中, 因为这些约定大多是基于一些历史原因存在的, 随着时间推移可能会发生变化.
外部依赖
react 几乎没有外部依赖. 通常require()
指向的是react自己代码库的一个文件. 但是也有一些例外.
由于react想要通过库共享一些诸如Relay
的小工具, 所以存在fbjs repository
, 而且我们让他们是同步的. 我们没有依赖任何node生态系统下的小模块, 因为我们希望facebook的工程师的能能再任何必要的时候修改他们. fbjs中的任何工具都不能被认为是公共api, 并且他们只是为Facebook的一些工程使用, 比如react.
一级目录
克隆了react的仓库后你会发现在里边有几个一级目录.
-
packages
目录包括一些元数据(如package.json)和react库提供的所有包的源码(src
的下面), 如果你想修改代码,src
下面就是你要花时间最多的地方. -
fixtures
目录包括了为贡献者准备的一些小的react的测试应用 -
build
是react打包输出的目录. 他不在代码库管理范畴, 但是当你第一次打包后就会生成.
文档是放在和react不同的另一个仓库管理的.
还有一些其他一级目录, 他们大多是工具层面的, 在你贡献代码时可能不会用到他们能.
共同测试(Colocated Tests)
我们没有搞个一级目录来做单元测试. 我们把它放在了被测试文件相邻的被称为__tests__
的目录.
举个例子, 对于setInnerHTML.js
这个文件的测试被放在与他同级的__tests__/setInnerHTML-test.js
这个里边.
这个词不知道怎么翻译
Warnings and Invariants
react中使用warning
模块显示警告信息.
var warning = require('warning');
warning(
2 + 2 === 4,
'Math is not working today.'
);
当警告条件是false的时候会展示警告信息
可以这么理解, 条件应该指示正常的情况, 而不是异常的情况. 就是说第一个参数是true表示的是正常, false是异常.
最好避免使用console取代warnings.
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!'
);
当条件为false时, 这个方法会直接抛出异常.
“Invariant” 就是说这个条件为真, 你可以认为他就是做了个断言.
保持开发环境和生产环境一致是很重要的, 因此invariant
在生产环境和开发环境都可以抛出异常. 生产环境下的错误消息被自动替换成错误码, 以防增加代码体积.
Development and Production
你可以使用__DEV__
这个为全局变量指定仅仅在开发环境才执行的代码块.
他是在编译过程中工作的, 他是在commonjs编译的时候检查process.env.NODE_ENV !== 'production'
这个值.
单独编译的时候, 他在未压缩版是true, 在压缩版直接被去掉了.
if (__DEV__) {
// 这里边的代码只会带开发环境执行
}
Flow
我们最近开始引入flow
做静态类型检查, 在文件头的注释里标注了@flow
的使用了类型检查.
我们接受在现有代码加入flow类型检查的pull request (不错哎, 可以试着提个pull request哦). Flow的签名类似下面这样.
ReactRef.detachRefs = function(
instance: ReactInstance,
element: ReactElement | string | number | null | false,
): void {
// ...
}
时机成熟的时候, 新代码要用Flow 签名, 你可以在本地运行yarn flow
用Flow检查你的代码.
动态植入
react在一些模块使用了动态植入. 但是这个东西不太好, 因为他让代码比较难理解了. 他存在的理由是react一开始只把支持dom作为目标的. 但是后来杀出了个React Native, 他是基于react的, 我们不得不加入动态植入好让react native 重载一些行为.
你可能会看到模块像下面这样声明它的动态依赖
// Dynamically injected
var textComponentClass = null;
// Relies on dynamically injected value
function createInstanceForText(text) {
return new textComponentClass(text);
}
var ReactHostComponent = {
createInstanceForText,
// Provides an opportunity for dynamic injection
injection: {
injectTextComponentClass: function(componentClass) {
textComponentClass = componentClass;
},
},
};
module.exports = ReactHostComponent;
注入的部分没有以任何方式特殊处理. 但是规定, 它的意思是这个模块想在运行时有一些依赖(可能是平台特定的)被注入进去.
代码里边有几个注入的入口. 未来, 我们将废弃掉这种动态植入的机制, 方案是在编译时以静态方式处理他们.
多包
react是个monorepo
, 他的仓库包含了多个独立的包, 因此他们的修改可以合在一起, 而且issues也可以放在一个地方.
React核心
react的核心是所有顶级api, 包括:
- React.createElement()
- React.Component
- React.Children
react核心只包括定义组件必要的api, 并不包括reconciliation
算法和平台特定代码. React DOM和React Native都使用了他们.
react核心的相关代码在packages/react
里边. npm使用时在react这个包里边, 浏览器版的是react.js, 他挂载一个被称为React的全局变量.
Renderers
react起初是为DOM创造的, 但是后台通过RN被用来支持原生环境了. 这里介绍加react内部的“renderers”的理念.
“renderers”管理了react树如何变成平台可调用的东西.
Renderers也在packages
里边
-
React DOM Renderer
把react 组件渲染进 DOM. 他实现了顶级的ReactDOM APIs
, 在react-dom
这个npm包里被暴露出来. 浏览器版叫react-dom.js, 通过ReactDOM这个全局变量暴露出来. -
React Native Renderer
把react组件渲染到原生视图层里. 他被RN内部使用. -
React Test Renderer
把react组件渲染成JSON树, 他被Jest
的一个特性Snapshot Testing
使用, 在react-test-renderer
这个npm包里可用.
另一个官方唯一支持的渲染器是react-art
, 他曾经是个独立的库, 现在被移进来了.
注意
技术上
react-native-renderer
是很薄的一层, 只是用来和RN的实现相互配合, 真正的平台相关代码是RN库里一些native view.
Reconcilers(协调器)
相当多的渲染器, 如Reat DOM, React Native 需要共享一套逻辑. 尤其reconciliation算法需要足够的相似, 以便让rendering, 自定义组件, 状态, 生命周期函数和refs能跨平台工作.
为了解决这个问题, 不同的渲染器共用一些代码. 我们把React 中的这个部分叫做"reconciler". 当一个更新比如setState要执行了,Reconcilers就去在组件上调用render(), 然后mounts, updates, 或者unmounts他们.
Reconcilers没有独立成包, 因为他现在还没有公共API. 相反, 他仅仅是在渲染器被使用, 比如React DOM , React Native.
Stack Reconciler
Stack Reconciler 是在react15之前实现使用的, 现在已经不用了, 但是下一部分的文档还会有详细的介绍.
Fiber Reconciler
"Fiber"是为了解决stack reconciler固有问题和修复长期存在的bug所做的努力, 他从react16开始成为默认的Reconciler.
他的主要目标是:
- 在chunks里分离可中断的工作
- 在过程中重建, 重用work或者改变他的优先级(瞎翻译的)的能力
- 在父子组件前进或回退以只是react中的布局的能力
- 在render方法里返回多个元素的能力
- 更好的支持错误边际
你可在这里和这里阅读更多关于Fiber架构的相关信息. 但是React16对他做了封装, 默认不支持异步特性了.
他的源码在packages/react-reconciler
里边.
事件系统
react实现了一个对renders透明的事件系统, 这个系统被用于react dom 和react native. 源码在packages/events
;