资讯专栏INFORMATION COLUMN

窥探React - 源码分析

instein / 1537人阅读

摘要:所谓知其然还要知其所以然本文将分析的部分源码包括组件初始渲染的过程和组件更新的过程在这之前假设读者已经对有一定了解知道区别了解生命周期事务批量更新大致概念等如何分析源码代码架构预览首先我们找到在上的地址把版本的源码下来观察它的整体架构这

所谓知其然还要知其所以然. 本文将分析 React 15-stable的部分源码, 包括组件初始渲染的过程和组件更新的过程.在这之前, 假设读者已经:

对React有一定了解

知道React element、component、class区别

了解生命周期、事务、批量更新、virtual DOM大致概念等

如何分析 React 源码

代码架构预览

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

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

</>复制代码

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

分析方法

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

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

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

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

正文

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

调用ReactDOM.render 发生了什么 大致过程(只展示主要的函数调用):

如果看不清这有矢量图

让我们来分析一下具体过程:
1、创建元素

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

</>复制代码

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

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

</>复制代码

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

React.createElement源码在ReactElement.js中, 逻辑比较简单, 不做分析.

2、实例化组件

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

string时, 说明是文本, 实例化ReactDOMTextComponent;

ReactElement时, 说明是react元素, 进一步判断element.type的类型, 当为

string时, 为DOM原生节点, 实例化ReactDOMComponent;

函数或类时, 为react 组件, 实例化ReactCompositeComponent

instantiateReactComponent函数在instantiateReactComponent.js :

</>复制代码

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

在调用instantiateReactComponent拿到组件实例后, React 接着调用了batchingStrategy.batchedUpdates并将组件实例当作参数执行批量更新.

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

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

不信你看, 在ReactUpdates.js中 :

</>复制代码

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

在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy :

</>复制代码

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

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

3.1 我们先介绍一下事务.

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

</>复制代码

  1. *

    </>复制代码

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

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

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

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

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

</>复制代码

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

好的, 相信你已经对事务如何实现有了大致了解, 但这只是React事务的抽象实现, 还需要实例化事务并对其加强的配合, 才能发挥事务的真正作用.

3.2 批量更新依靠了事务

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

ReactDefaultBatchingStrategy.js :

</>复制代码

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

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

何时存储更新

何时批处理

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

何时存储更新—— 在执行函数时遇到更新请求就存到这个队列中

何时批处理—— 函数执行后调用wrappers的close方法, 在close方法中调用批量处理函数

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

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

</>复制代码

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

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

ReactCompositeComponent.js :

</>复制代码

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

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

CallbackQueue.js :

</>复制代码

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

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

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

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

未完待续...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107443.html

相关文章

  • 窥探 Script 标签(步入现代 Web 开发的魔法世界)

    摘要:而且默认带有执行的顺序是,,即便是内联的,依然具有属性。模块脚本只会执行一次必须符合同源策略模块脚本在跨域的时候默认是不带的。通常被用作脚本被禁用的回退方案。最后标签真的令人感到兴奋。 窥探 Script 标签 0x01 什么是 script 标签? script 标签允许你包含一些动态脚本或数据块到文档中,script 标签是非闭合的,你也可以将动态脚本或数据块当做 script 的...

    Terry_Tai 评论0 收藏0
  • 窥探 Script 标签(步入现代 Web 开发的魔法世界)

    摘要:而且默认带有执行的顺序是,,即便是内联的,依然具有属性。模块脚本只会执行一次必须符合同源策略模块脚本在跨域的时候默认是不带的。通常被用作脚本被禁用的回退方案。最后标签真的令人感到兴奋。 窥探 Script 标签 0x01 什么是 script 标签? script 标签允许你包含一些动态脚本或数据块到文档中,script 标签是非闭合的,你也可以将动态脚本或数据块当做 script 的...

    gaosboy 评论0 收藏0
  • 窥探Underscore源码系列-开篇

    摘要:他指示了一个对象的属性,返回的将用来获得该属性对应的值在上面的分析中,我们知道,当传入的是一个函数时,还要经过一个叫的内置函数才能获得最终的所以此处的必然是优化回调的作用了。 开篇说明 对的,让你所见,又开始造轮子了。哈哈,造轮子我们是认真的~ 源码阅读是必须的,Underscore是因为刚刚学习整理了一波函数式编程,加上自己曾经没有太多阅读源码的经验,先拿Underscore练练手,...

    zorpan 评论0 收藏0
  • 窥探react事件

    摘要:解决问题为了解决上述问题,先来了解下的事件,事件是合成事件,为原生事件的一个子集,仅仅是进行了一个跨浏览器的封装。参考本文部分参考自事件初探 写在前面 本文源于本人在学习react过程中遇到的一个问题;本文内容为本人的一些的理解,如有不对的地方,还请大家指出来。本文是讲react的事件,不是介绍其api,而是猜想一下react合成事件的实现方式 遇到的问题 class EventTes...

    刘厚水 评论0 收藏0
  • 割裂的前端工程师--- 2017年前端生态窥探

    摘要:主要兼容的微信的浏览器,因为要在朋友圈来营销,总体来说,会偏设计以及动画些。 有一天,我们组内的一个小伙伴突然问我,你知道有一个叫重构工程师的岗位?这是干什么的?重构工程师 这个问题引发了我对前端领域发展的思考,所以我来梳理下前端领域的发展过程,顺便小小的预测下2017年的趋势。不想看回忆的,可以直接跳到后面看展望。 神说,要有光,就有了光 自1991年蒂姆·伯纳斯-李公开提及HTML...

    duan199226 评论0 收藏0

发表评论

0条评论

instein

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<