呃....4年前开了一个坑,准备写一套完整介绍TS 原理的文章。坑很大,要慢慢填,今天就来填一个把。

本节主要介绍语法增量解析。

什么是增量解析

增量解析的意思是,如果我们直接从源码解析成语法树,叫做全量解析。

语法树是由很多个节点对象组成的,比较吃内存。

当用户修改源码后(无论修改哪里,包括插入一个空格),我们都需要重新解析文件,生成新的语法树。

如果每次都全量解析,那意味着释放之前的所有节点,然后重新创建所有节点。

但实际上,用户每次只会修改一部分内容,而整个语法树的大部分节点都不会发生变化。

如果解析时,发现节点没有变化,就可以复用之前的节点对象,只重新创建变化的节点,这个过程叫增量解析。

增量解析是一种性能优化,它不是必须的。

实现增量解析的基本原理

假如我们修改了函数中某行代码的内容,从原则上说,这个函数之前的节点都是不变的。

函数之后的节点大概率不变,但也有小概率会变,比如我们插入了“}”,导致函数的范围发生变化,或者插入“`”,导致后面的内容都变成字符串的一部分了。

看上去好像很复杂,但 TS 采用了一个折中的做法,大幅降低了复杂度。

TS 是以语句为单位进行复用的,即每条语句或者完全复用,或者完全不复用,即使单条语句里面存在可复用的部分子节点。(这种说法其实并不准确,但为了方便理解,可以先这么认为)

核心逻辑为:

1. 当在 pos 位置解析一条语句前,TS 先检测该位置是否存在可复用的旧节点,如果不存在当然就无法增量解析,就转成常规解析。

2. 如果存在旧节点,则检查该旧节点所在区域是否发生变化,如果发生变化,则放弃复用,转为常规解析。

3. 如果没有发生变化,那这条语句就直接解析完毕,然后从这行语句的 end 位置继续解析下一条语句,重复前面的步骤。

SyntaxCursor

代码位于 parser.ts

  1. export interface SyntaxCursor {
  2. currentNode(position: number): Node;
  3. }
  1. SyntaxCursor 用于从原始的语法树中查找指定位置对应的可复用的旧节点。
  1. export function createSyntaxCursor(sourceFile: SourceFile): SyntaxCursor {
  2. let currentArray: NodeArray<Node> = sourceFile.statements;
  3. let currentArrayIndex = 0;
  4.  
  5. Debug.assert(currentArrayIndex < currentArray.length);
  6. let current = currentArray[currentArrayIndex];
  7. let lastQueriedPosition = InvalidPosition.Value;
  8.  
  9. return {
  10. currentNode(position: number) {
  11. // 函数内基于一个事实做了一个性能优化
              // 就是解析时,position 会逐步变大,因此查找的时候,不需要每次都重头查找,而是记住上一次查找的位置
              // 下次查找就从上次的位置继续查找,这样找起来更快

  12. if (position !== lastQueriedPosition) {
  13. if (current && current.end === position && currentArrayIndex < (currentArray.length - 1)) {
  14. currentArrayIndex++;
  15. current = currentArray[currentArrayIndex];
  16. }
  17.  
  18. // 如果上次的位置和要查找的位置不匹配,就重头查找。
  19. if (!current || current.pos !== position) {
  20. findHighestListElementThatStartsAtPosition(position);
  21. }
  22. }
  23.  
  24. // 记住本次查找的位置,加速下次查找
  25. lastQueriedPosition = position;
  26.  
  27. Debug.assert(!current || current.pos === position);
  28. return current;
  29. },
  30. };
  31.  
  32. // 标准的深度优先搜索算法,找到就近的节点
  33. function findHighestListElementThatStartsAtPosition(position: number) {
  34. currentArray = undefined!;
  35. currentArrayIndex = InvalidPosition.Value;
  36. current = undefined!;
  37.  
  38. forEachChild(sourceFile, visitNode, visitArray);
  39. return;
  40.  
  41. function visitNode(node: Node) {
  42. if (position >= node.pos && position < node.end) {
  43. forEachChild(node, visitNode, visitArray);
  44.  
  45. return true;
  46. }
  47.  
  48. // position wasn't in this node, have to keep searching.
  49. return false;
  50. }
  51.  
  52. function visitArray(array: NodeArray<Node>) {
  53. if (position >= array.pos && position < array.end) {
  54. for (let i = 0; i < array.length; i++) {
  55. const child = array[i];
  56. if (child) {
  57. if (child.pos === position) {
  58. currentArray = array;
  59. currentArrayIndex = i;
  60. current = child;
  61. return true;
  62. }
  63. else {
  64. if (child.pos < position && position < child.end) {
  65. forEachChild(child, visitNode, visitArray);
  66. return true;
  67. }
  68. }
  69. }
  70. }
  71. }
  72.  
  73. return false;
  74. }
  75. }
  76. }

解析列表

每个列表(包括语句块的语句列表)都是使用 parseList 解析的。每个元素都是通过 parseListElement 解析。

  1. function parseList<T extends Node>(kind: ParsingContext, parseElement: () => T): NodeArray<T> {
  2. const saveParsingContext = parsingContext;
  3. parsingContext |= 1 << kind;
  4. const list = [];
  5. const listPos = getNodePos();
  6.  
  7. while (!isListTerminator(kind)) {
  8. if (isListElement(kind, /*inErrorRecovery*/ false)) {
  9. list.push(parseListElement(kind, parseElement));
  10.  
  11. continue;
  12. }
  13.  
  14. if (abortParsingListOrMoveToNextToken(kind)) {
  15. break;
  16. }
  17. }
  18.  
  19. parsingContext = saveParsingContext;
  20. return createNodeArray(list, listPos);
  21. }

parseListElement 中会先检测可复用的节点,如果存在,就复用并解析下一个元素,否则正常解析当前元素。

  1. function parseListElement<T extends Node | undefined>(parsingContext: ParsingContext, parseElement: () => T): T {
  2. const node = currentNode(parsingContext);
  3. if (node) {
  4. return consumeNode(node) as T;
  5. }
  6.  
  7. return parseElement();
  8. }
currentNode 负责返回可复用的节点,除了基于 syntaxCursor 查找,还加了一些额外的限制,防止某些特殊情况会复用。
  1. function currentNode(parsingContext: ParsingContext, pos?: number): Node | undefined {
  2.  
  3. if (!syntaxCursor || !isReusableParsingContext(parsingContext) || parseErrorBeforeNextFinishedNode) {
  4. return undefined;
  5. }
  6.  
  7. const node = syntaxCursor.currentNode(pos ?? scanner.getTokenFullStart());
  8.  
  9. // 存在语法错误的节点不能复用,因为我们需要重新解析,重新报错。
  10. if (nodeIsMissing(node) || intersectsIncrementalChange(node) || containsParseError(node)) {
  11. return undefined;
  12. }
  13.  
  14. const nodeContextFlags = node.flags & NodeFlags.ContextFlags;
  15. if (nodeContextFlags !== contextFlags) {
  16. return undefined;
  17. }
  18.  
  19. // 有些节点不能复用,因为存在一定场景导致复用出错
  20. if (!canReuseNode(node, parsingContext)) {
  21. return undefined;
  22. }
  23.  
  24. if (canHaveJSDoc(node) && node.jsDoc?.jsDocCache) {
  25.  
  26. node.jsDoc.jsDocCache = undefined;
  27. }
  28.  
  29. return node;
  30. }

当节点被复用后,使用 consumeNode 设置下次扫描的位置。

  1. function consumeNode(node: Node) {
  2. scanner.resetTokenState(node.end);
  3. nextToken();
  4. return node;
  5. }

不能复用的场景

有些场景复用是有问题的,(很多场景都是社区通过 Issue 给 TS 报的 BUG,然后修复的)。

比如泛型:

  1. var a = b < c, d, e

从复用角度,这是一个列表,列表项分别为:

    1. a = b < c
  • d
  • e

理论在 e 后面插入任何字符,都不影响前面的节点,但存在一个特例,就是">"

  1. var a = b<c,d,e>

当 <> 成对,它变成了泛型。这会导致需要重新解析整个语句。

TS 的做法并不是检测是否插入了“>”,而是因为存在整个特例,就完全不复用变量列表的任何节点,即使多数情况复用的安全的。

毕竟增量解析只是一种性能优化,没有也不是不能用。

完整的检测特殊情况的逻辑在 canReuseNode,因为比较琐碎,且逻辑都比较简单,这里就不贴了。

结论

经过增量解析后,部分节点会被重新使用。

从算法中可以得出,如果子节点被修改了,那父节点一定也会被修改。而源文件本身在每次增量解析时,都会被重新创建。

  1.  

TS 原理详细解读(6)--语法增量解析的更多相关文章

  1. TS 原理详细解读(5)语法2-语法解析

    在上一节介绍了语法树的结构,本节则介绍如何解析标记组成语法树. 对应的源码位于 src/compiler/parser.ts. 入口函数 要解析一份源码,输入当然是源码内容(字符串),同时还提供路径( ...

  2. TypeScript 源码详细解读(4)语法1-语法树

    在上一节介绍了标记的解析,就相当于识别了一句话里有哪些词语,接下来就是把这些词语组成完整的句子,即拼装标记为语法树. 树(tree) 树是计算机数据结构里的专业术语.就像一个学校有很多年级,每个年级下 ...

  3. SpringMVC 原理 - 设计原理、启动过程、请求处理详细解读

    SpringMVC 原理 - 设计原理.启动过程.请求处理详细解读 目录 一. 设计原理 二. 启动过程 三. 请求处理 一. 设计原理 Servlet 规范 SpringMVC 是基于 Servle ...

  4. C++多态的实现及原理详细解析

    C++多态的实现及原理详细解析 作者: 字体:[增加 减小] 类型:转载   C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型 ...

  5. live555中ts流详细解析

    live555中ts流详细解析 该文档主要是对live555源码下testProgs中testMPEG2TransportStreamer服务器端的详细分析.主要分析ts流实现的总体调用流程.(重新整 ...

  6. 深入理解NIO(三)—— NIO原理及部分源码的解析

    深入理解NIO(三)—— NIO原理及部分源码的解析 欢迎回到淦™的源码看爆系列 在看完前面两个系列之后,相信大家对NIO也有了一定的理解,接下来我们就来深入源码去解读它,我这里的是OpenJDK-8 ...

  7. MemCache超详细解读

    MemCache是什么 MemCache是一个自由.源码开放.高性能.分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高 ...

  8. MemCache超详细解读 图

    http://www.cnblogs.com/xrq730/p/4948707.html   MemCache是什么 MemCache是一个自由.源码开放.高性能.分布式的分布式内存对象缓存系统,用于 ...

  9. MemCache详细解读

    MemCache是什么 MemCache是一个自由.源码开放.高性能.分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高 ...

  10. 【公众号系列】超详细SAP HANA JOB全解析

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[公众号系列]超详细SAP HANA JOB全解 ...

随机推荐

  1. ORM的设计思想

    1 以面向对象的思想来完成对于数据库的操作! 2 万物皆对象

  2. 痞子衡嵌入式:瑞萨RA系列FSP固件库分析之外设驱动

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是瑞萨RA系列FSP固件库里的外设驱动. 上一篇文章 <瑞萨RA8系列高性能MCU开发初体验>,痞子衡带大家快速体验了一下瑞萨 ...

  3. 电脑端 itunes 备份保存路径修改方法

    默认在c盘,重做系统就会丢失. 1.先删除C:\Users\你的用户名\AppData\Roaming\Apple Computer里的 MobileSync文件夹(首次安装iTunes没有,要先运行 ...

  4. Nuxt.js 应用中的 components:extend 事件钩子详解

    title: Nuxt.js 应用中的 components:extend 事件钩子详解 date: 2024/11/1 updated: 2024/11/1 author: cmdragon exc ...

  5. Redis学习笔记整理

    一.Redis概述 1.redis简介 Redis(REmote DIctionary Server 远程字典服务器)是一款开源的,用ANSI C编写.支持网络.基于内存.亦可持久化的日志型.Key- ...

  6. LeetCode128 最长连续序列

    最长连续序列 题目链接:LeetCode128 描述 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度. 请你设计并实现时间复杂度为 O(n) 的算法 ...

  7. 基于Java+SpringBoot+Mysql实现的快递柜寄取快递系统功能实现五

    一.前言介绍: 1.1 项目摘要 随着电子商务的迅猛发展和城市化进程的加快,快递业务量呈现出爆炸式增长的趋势.传统的快递寄取方式,如人工配送和定点领取,已经无法满足现代社会的快速.便捷需求.这些问题不 ...

  8. PTA题目集4~6的总结性Blog

    · 前言 本次的三个作业,由答题判题程序- 4.家居强电电路模拟程序- 1.家居强电电路模拟程序 -2组成. 答题判题程序-4是对前三次判题程序的最后升级,设计多个子类继承于基础题类来实现对每种题型的 ...

  9. 腾讯AICR : 智能化代码评审技术探索与应用实践(下)

  10. nodejs版本管理工具之n

    转载: https://juejin.cn/post/7065534944101007391 Node.js 对于现在的前端开发人员来说是不可或缺的需要掌握的技能,但我们在使用时避免不了会需要切换不同 ...