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

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

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

如何分析 React 源码

代码架构预览

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

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

分析方法

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

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

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

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

正文

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

调用ReactDOM.render 发生了什么

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

React 初始渲染

如果看不清这有矢量图

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


1、创建元素

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

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

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

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

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

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

instantiateReactComponent函数在instantiateReactComponent.js :

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

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

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

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

不信你看, 在ReactUpdates.js中 :

在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy :

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

3.1 我们先介绍一下事务.

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

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

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

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

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

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

3.2 批量更新依靠了事务

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

ReactDefaultBatchingStrategy.js :

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

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

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

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

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

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

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

ReactCompositeComponent.js :

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

CallbackQueue.js :

即证.

你竟然读到这了

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

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

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

未完待续…

1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部