React源码分析(一)-调用ReactDOM.render后发生了什么

本文主要是介绍React源码分析(一)-调用ReactDOM.render后发生了什么,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

所谓知其然还要知其所以然. 本系列文章将分析 React 15-stable的部分源码, 包括组件初始渲染的过程、组件更新的过程等. 这篇文章先介绍组件初始渲染的过程的几个重要概念, 包括大致过程、创建元素、实例化组件、事务、批量更新策略等. 在这之前, 假设读者已经:

  • 对React有一定了解
  • 知道React element、component、class区别
  • 了解生命周期、事务、批量更新、virtual DOM大致概念等

如何分析 React 源码

代码架构预览

首先, 我们找到React在Github上的地址, 把15-stable版本的源码copy下来, 观察它的整体架构, 这里首先阅读关于源码介绍的官方文档, 再接着看.

我们 要分析的源码在 src 目录下:

1
2
3
4
5
6
7
8
9
10
// src 部分目录├── ReactVersion.js    # React版本号
├── addons             # 插件
├── isomorphic		   # 同构代码,作为react-core, 提供顶级API
├── node_modules
├── package.json
├── renderers          # 渲染器, 包括DOM,Native,art,test等
├── shared             # 子目录之间需要共享的代码,提到父级目录shared
├── test			   # 测试代码

分析方法

1、首先看一些网上分析的文章, 对重点部分的源码有个印象, 知道一些关键词意思, 避免在无关的代码上迷惑、耗费时间;

2、准备一个demo, 无任何功能代码, 只安装react,react-dom, Babel转义包, 避免分析无关代码;

3、打debugger; 利用Chrome devtool一步一步走, 打断点, 看调用栈,看函数返回值, 看作用域变量值;

4、利用编辑器查找代码、阅读代码等

正文

我们知道, 对于一般的React 应用, 浏览器会首先执行代码 ReactDOM.render来渲染顶层组件, 在这个过程中递归渲染嵌套的子组件, 最终所有组件被插入到DOM中. 我们来看看

调用ReactDOM.render 发生了什么

大致过程(只展示主要的函数调用):

React 初始渲染

如果看不清这有矢量图

让我们来分析一下具体过程:


1、创建元素

首先, 对于你写的jsx, Babel会把这种语法糖转义成这样:

1
2
3
4
5
6
7
8
9
10
11
// jsx
ReactDOM.render(<C />,document.getElementById('app')
)// 转义后
ReactDOM.render(React.createElement(C, null), document.getElementById('app')
);

没错, 就是调用React.createElement来创建元素. 元素是什么? 元素只是一个对象描述了DOM树, 它像这样:

1
2
3
4
5
6
7
8
9
10
11
{$$typeof: Symbol(react.element)key: nullprops: {}        // props有child属性, 描述子组件, 同样是元素ref: nulltype: class C    // type可以是类(自定义组件)、函数(wrapper)、string(DOM节点)_owner: null_store: {validated: false}_self: null_source: null
}

React.createElement源码在ReactElement.js中, 其他逻辑比较简单, 值得说的是props属性, 这个props属性里面包含的就是我们给组件传的各种属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// jsx
return (<div className='container'>"dscsdcsd"<i onClick={(e) => console.log(e)}>{this.state.val}</i><Children val={this.state.val}/></div>
)// bable 转义后
// createElement(type, props, children)
return React.createElement('div', { className: 'container' }, '"dscsdcsd"',React.createElement('i', { onClick: e => console.log(e) }, this.state.val),React.createElement(Children, { val: this.state.val })
);// 对应的元素树
{$$typeof: Symbol(react.element)key: nullprops: {  // props有children属性, 描述子组件, 同样是元素children: [""dscsdcsd"",// 子元素{$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …},{$$typeof: Symbol(react.element), type: class Children, props: {…}, …}]className: 'container'}  ref: nulltype: 'div'_owner: null_store: {validated: false}_self: null_source: null
}

2、创建对应类型的React组件

创建出来的元素被当作参数和指定的 DOM container 一起传进ReactDOM.render. 接下来会调用一些内部方法, 接着调用了 instantiateReactComponent, 这个函数根据element的类型实例化对应的component. 当element的类型为:

  • string时, 说明是文本, 创建ReactDOMTextComponent;
  • ReactElement时, 说明是react元素, 进一步判断element.type的类型, 当为
    • string时, 为DOM原生节点, 创建ReactDOMComponent;
    • 函数或类时, 为react 组件, 创建ReactCompositeComponent

instantiateReactComponent函数在instantiateReactComponent.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*** Given a ReactNode, create an instance that will actually be mounted.*/
function instantiateReactComponent(node(这里node指element), shouldHaveDebugID) {...// 如果element为空if (node === null || node === false) {// 创建空componentinstance = ReactEmptyComponent.create(instantiateReactComponent);} else if (typeof node === 'object') {  // 如果是对象...     // 这里是类型检查// 如果element.type是字符串if (typeof element.type === 'string') {//实例化 宿主组件, 也就是DOM节点instance = ReactHostComponent.createInternalComponent(element);} else if (isInternalComponentType(element.type)) {// 保留给以后版本使用,此处暂时不会涉及到} else { // 否则就实例化ReactCompositeComponentinstance = new ReactCompositeComponentWrapper(element);}// 如果element是string或number} else if (typeof node === 'string' || typeof node === 'number') {// 实例化ReactDOMTextComponentinstance = ReactHostComponent.createInstanceForText(node);} else {invariant(false, 'Encountered invalid React node of type %s', typeof node);}...return instance;
}

3、开启批量更新以应对可能的setState

在调用instantiateReactComponent拿到组件实例后, React 接着调用了batchingStrategy.batchedUpdates并将组件实例当作参数执行批量更新(首次渲染为批量插入).

批量更新是一种优化策略, 避免重复渲染, 在很多框架都存在这种机制. 其实现要点是要弄清楚何时存储更新, 何时批量更新.

在React中, 批量更新受batchingStrategy控制,而这个策略除了server端都是ReactDefaultBatchingStrategy:

不信你看, 在ReactUpdates.js中 :

1
2
3
4
5
6
7
8
9
var ReactUpdatesInjection = {...// 注入批量策略的函数声明injectBatchingStrategy: function(_batchingStrategy) {... batchingStrategy = _batchingStrategy;},
};

在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy :

1
ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy); // 注入

那么React是如何实现批量更新的? 在ReactDefaultBatchingStrategy.js我们看到, 它的实现依靠了事务.

3.1 我们先介绍一下事务.

在 Transaction.js中, React 介绍了事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* <pre>*                       wrappers (injected at creation time)*                                      +        +*                                      |        |*                    +-----------------|--------|--------------+*                    |                 v        |              |*                    |      +---------------+   |              |*                    |   +--|    wrapper1   |---|----+         |*                    |   |  +---------------+   v    |         |*                    |   |          +-------------+  |         |*                    |   |     +----|   wrapper2  |--------+   |*                    |   |     |    +-------------+  |     |   |*                    |   |     |                     |     |   |*                    |   v     v                     v     v   | wrapper*                    | +---+ +---+   +---------+   +---+ +---+ | invariants* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->*                    | |   | |   |   |         |   |   | |   | |*                    | |   | |   |   |         |   |   | |   | |*                    | |   | |   |   |         |   |   | |   | |*                    | +---+ +---+   +---------+   +---+ +---+ |*                    |  initialize                    close    |*                    +-----------------------------------------+* </pre>

React 把要调用的函数封装一层wrapper, 这个wrapper一般是一个对象, 里面有initialize方法, 在调用函数前调用;有close方法, 在函数执行后调用. 这样封装的目的是为了, 在要调用的函数执行前后某些不变性约束条件(invariant)仍然成立.

这里的不变性约束条件(invariant), 我把它理解为 “真命题”, 因此前面那句话意思就是, 函数调用前后某些规则仍然成立. 比如, 在调和(reconciliation)前后保留UI组件一些状态.

React 中, 事务就像一个黑盒, 函数在这个黑盒里被执行, 执行前后某些规则仍然成立, 即使函数报错. 事务提供了函数执行的一个安全环境.

继续看Transaction.js对事务的抽象实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 事务的抽象实现, 作为基类
var TransactionImpl = {// 初始化/重置实例属性, 给实例添加/重置几个属性, 实例化事务时会调用reinitializeTransaction: function () {this.transactionWrappers = this.getTransactionWrappers();if (this.wrapperInitData) {this.wrapperInitData.length = 0;} else {this.wrapperInitData = [];}this._isInTransaction = false;},_isInTransaction: false,// 这个函数会交给具体的事务实例化时定义, 初始设为nullgetTransactionWrappers: null,// 判断是否已经在这个事务中, 保证当前的Transaction正在perform的同时不会再次被performisInTransaction: function () {return !!this._isInTransaction;},// 顶级API, 事务的主要实现, 用来在安全的窗口下执行函数perform: function (method, scope, a, b, c, d, e, f) {var ret;var errorThrown;try {this._isInTransaction = true;errorThrown = true;this.initializeAll(0);  // 调用所有wrapper的initialize方法ret = method.call(scope, a, b, c, d, e, f); // 调用要执行的函数errorThrown = false;} finally {// 调用所有wrapper的close方法, 利用errorThrown标志位保证只捕获函数执行时的错误, 对initialize	  // 和close抛出的错误不做处理try {if (errorThrown) {try {this.closeAll(0);} catch (err) {}} else {this.closeAll(0);}} finally {this._isInTransaction = false;}}return ret;},// 调用所有wrapper的initialize方法的函数定义initializeAll: function (startIndex) {var transactionWrappers = this.transactionWrappers; // 得到wrapper// 遍历依次调用for (var i = startIndex; i < transactionWrappers.length; i++) {var wrapper = transactionWrappers[i];try {...this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this):null;} finally {if (this.wrapperInitData[i] === OBSERVED_ERROR) {try {this.initializeAll(i + 1);} catch (err) {}}}}},// 调用所有wrapper的close方法的函数定义closeAll: function (startIndex) {...var transactionWrappers = this.transactionWrappers; // 拿到wrapper// 遍历依次调用for (var i = startIndex; i < transactionWrappers.length; i++) {var wrapper = transactionWrappers[i];var initData = this.wrapperInitData[i];var errorThrown;try {...if (initData !== OBSERVED_ERROR && wrapper.close) {wrapper.close.call(this, initData);}errorThrown = false;} finally {if (errorThrown) {...try {this.closeAll(i + 1);} catch (e) {}}}}this.wrapperInitData.length = 0;}
};

这只是React事务的抽象实现(基类), 还需要实例化事务并对其加强的配合, 才能发挥事务的真正作用. 另外, 在React 中, 一个事务里开启另一个事务很普遍, 这说明事务是有粒度大小的, 就像进程和线程一样.

3.2 批量更新依靠了事务

刚讲到, 在React中, 批量更新受batchingStrategy控制,而这个策略除了server端都是ReactDefaultBatchingStrategy, 而在ReactDefaultBatchingStrategy.js中, 批量更新的实现依靠了事务:

ReactDefaultBatchingStrategy.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
...
var Transaction = require('Transaction');// 引入事务
...var RESET_BATCHED_UPDATES = {   // 重置的 wrapperinitialize: emptyFunction,close: function() {ReactDefaultBatchingStrategy.isBatchingUpdates = false;  // 事务结束即一次batch结束},
};var FLUSH_BATCHED_UPDATES = {  // 批处理的 wrapperinitialize: emptyFunction,close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};// 组合成 ReactDefaultBatchingStrategyTransaction 事务的wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; // 调用 reinitializeTransaction 初始化
function ReactDefaultBatchingStrategyTransaction() {this.reinitializeTransaction();
}// 参数中依赖了事务
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {getTransactionWrappers: function() {return TRANSACTION_WRAPPERS;},
});var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化这类事务// 批处理策略
var ReactDefaultBatchingStrategy = {isBatchingUpdates: false, // 是否处在一次BatchingUpdates标志位// 批量更新策略调用的就是这个方法batchedUpdates: function(callback, a, b, c, d, e) {var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;// 一旦调用批处理, 重置isBatchingUpdates标志位, 表示正处在一次BatchingUpdates中ReactDefaultBatchingStrategy.isBatchingUpdates = true;// 避免重复分配事务if (alreadyBatchingUpdates) {return callback(a, b, c, d, e);} else {return transaction.perform(callback, null, a, b, c, d, e);  // 将callback放进事务里执行}},
};

那么, 为什么批量更新的实现依靠了事务呢? 还记得实现批量更新的两个要点吗?

  • 何时存储更新
  • 何时批处理

对于这两个问题, React 在执行事务时调用wrappers的initialize方法, 建立更新队列, 然后执行函数, 接着 :

  • 何时存储更新—— 在执行函数时遇到更新请求就存到这个队列中
  • 何时批处理—— 函数执行后调用wrappers的close方法, 在close方法中调用批量处理函数

口说无凭, 得有证据. 我们拿ReactDOM.render会调用的事务ReactReconcileTransaction来看看是不是这样:

ReactReconcileTransaction.js 里有个wrapper, 它是这样定义的(英文是官方注释) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ON_DOM_READY_QUEUEING = {/*** Initializes the internal `onDOMReady` queue.*/initialize: function() {this.reactMountReady.reset();},/*** After DOM is flushed, invoke all registered `onDOMReady` callbacks.*/close: function() {this.reactMountReady.notifyAll();},
};

我们再看ReactReconcileTransaction事务会执行的函数mountComponent, 它在

ReactCompositeComponent.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** Initializes the component, renders markup, and registers event listeners.
*/mountComponent: function(transaction,hostParent,hostContainerInfo,context,) {...if (inst.componentDidMount) {if (__DEV__) {transaction.getReactMountReady().enqueue(() => { // 将要调用的callback存起来measureLifeCyclePerf(() => inst.componentDidMount(),this._debugID,'componentDidMount',);});} else {transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);}}...}

而上述wrapper定义的close方法调用的this.reactMountReady.notifyAll()在这

CallbackQueue.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*** Invokes all enqueued callbacks and clears the queue. This is invoked after* the DOM representation of a component has been created or updated.*/notifyAll() {...// 遍历调用存储的callbackfor (var i = 0; i < callbacks.length; i++) {callbacks[i].call(contexts[i], arg);}callbacks.length = 0;contexts.length = 0;}}

即证.

你竟然读到这了

好累(笑哭), 先写到这吧. 我本来还想一篇文章就把组件初始渲染的过程和组件更新的过程讲完, 现在看来要分开讲了… React 细节太多了, 蕴含的信息量也很大…说博大精深一点不夸张…向React的作者们以及社区的人们致敬!

我觉得读源码是一件很费力但是非常值得的事情. 刚开始读的时候一点头绪也没有, 不知道它是什么样的过程, 不知道为什么要这么写, 有时候还会因为断点没打好绕了很多弯路…也是硬着头皮一遍一遍看, 结合网上的文章, 就这样云里雾里的慢慢摸索, 不断更正自己的认知.后来看多了, 就经常会有大彻大悟的感觉, 零碎的认知开始连通起来, 逐渐摸清了来龙去脉.

现在觉得确实很值得, 自己学到了不少. 看源码的过程就感觉是跟作者们交流讨论一样, 思想在碰撞! 强烈推荐前端的同学们阅读React源码, 大神们智慧的结晶!

未完待续…

原文http://realtcg.com/2018/02/11/react-source-code-analysis-1-after-ReactDOM.render/

这篇关于React源码分析(一)-调用ReactDOM.render后发生了什么的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/509354

相关文章

IDEA下"File is read-only"可能原因分析及"找不到或无法加载主类"的问题

《IDEA下Fileisread-only可能原因分析及找不到或无法加载主类的问题》:本文主要介绍IDEA下Fileisread-only可能原因分析及找不到或无法加载主类的问题,具有很好的参... 目录1.File is read-only”可能原因2.“找不到或无法加载主类”问题的解决总结1.File

使用Python实现调用API获取图片存储到本地的方法

《使用Python实现调用API获取图片存储到本地的方法》开发一个自动化工具,用于从JSON数据源中提取图像ID,通过调用指定API获取未经压缩的原始图像文件,并确保下载结果与Postman等工具直接... 目录使用python实现调用API获取图片存储到本地1、项目概述2、核心功能3、环境准备4、代码实现

8种快速易用的Python Matplotlib数据可视化方法汇总(附源码)

《8种快速易用的PythonMatplotlib数据可视化方法汇总(附源码)》你是否曾经面对一堆复杂的数据,却不知道如何让它们变得直观易懂?别慌,Python的Matplotlib库是你数据可视化的... 目录引言1. 折线图(Line Plot)——趋势分析2. 柱状图(Bar Chart)——对比分析3

使用@Cacheable注解Redis时Redis宕机或其他原因连不上继续调用原方法的解决方案

《使用@Cacheable注解Redis时Redis宕机或其他原因连不上继续调用原方法的解决方案》在SpringBoot应用中,我们经常使用​​@Cacheable​​注解来缓存数据,以提高应用的性能... 目录@Cacheable注解Redis时,Redis宕机或其他原因连不上,继续调用原方法的解决方案1

CSS 样式表的四种应用方式及css注释的应用小结

《CSS样式表的四种应用方式及css注释的应用小结》:本文主要介绍了CSS样式表的四种应用方式及css注释的应用小结,本文通过实例代码给大家介绍的非常详细,详细内容请阅读本文,希望能对你有所帮助... 一、外部 css(推荐方式)定义:将 CSS 代码保存为独立的 .css 文件,通过 <link> 标签

Dubbo之SPI机制的实现原理和优势分析

《Dubbo之SPI机制的实现原理和优势分析》:本文主要介绍Dubbo之SPI机制的实现原理和优势,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Dubbo中SPI机制的实现原理和优势JDK 中的 SPI 机制解析Dubbo 中的 SPI 机制解析总结Dubbo中

C#继承之里氏替换原则分析

《C#继承之里氏替换原则分析》:本文主要介绍C#继承之里氏替换原则,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#里氏替换原则一.概念二.语法表现三.类型检查与转换总结C#里氏替换原则一.概念里氏替换原则是面向对象设计的基本原则之一:核心思想:所有引py

使用Vue-ECharts实现数据可视化图表功能

《使用Vue-ECharts实现数据可视化图表功能》在前端开发中,经常会遇到需要展示数据可视化的需求,比如柱状图、折线图、饼图等,这类需求不仅要求我们准确地将数据呈现出来,还需要兼顾美观与交互体验,所... 目录前言为什么选择 vue-ECharts?1. 基于 ECharts,功能强大2. 更符合 Vue

Vue中插槽slot的使用示例详解

《Vue中插槽slot的使用示例详解》:本文主要介绍Vue中插槽slot的使用示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、插槽是什么二、插槽分类2.1 匿名插槽2.2 具名插槽2.3 作用域插槽三、插槽的基本使用3.1 匿名插槽

springboot+vue项目怎么解决跨域问题详解

《springboot+vue项目怎么解决跨域问题详解》:本文主要介绍springboot+vue项目怎么解决跨域问题的相关资料,包括前端代理、后端全局配置CORS、注解配置和Nginx反向代理,... 目录1. 前端代理(开发环境推荐)2. 后端全局配置 CORS(生产环境推荐)3. 后端注解配置(按接口