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

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

什么是增量解析

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

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

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

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

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

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

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

实现增量解析的基本原理

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

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

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

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

核心逻辑为:

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

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

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

SyntaxCursor

代码位于 parser.ts

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

if (position !== lastQueriedPosition) {
if (current && current.end === position && currentArrayIndex < (currentArray.length - 1)) {
currentArrayIndex++;
current = currentArray[currentArrayIndex];
} // 如果上次的位置和要查找的位置不匹配,就重头查找。
if (!current || current.pos !== position) {
findHighestListElementThatStartsAtPosition(position);
}
} // 记住本次查找的位置,加速下次查找
lastQueriedPosition = position; Debug.assert(!current || current.pos === position);
return current;
},
}; // 标准的深度优先搜索算法,找到就近的节点
function findHighestListElementThatStartsAtPosition(position: number) {
currentArray = undefined!;
currentArrayIndex = InvalidPosition.Value;
current = undefined!; forEachChild(sourceFile, visitNode, visitArray);
return; function visitNode(node: Node) {
if (position >= node.pos && position < node.end) {
forEachChild(node, visitNode, visitArray); return true;
} // position wasn't in this node, have to keep searching.
return false;
} function visitArray(array: NodeArray<Node>) {
if (position >= array.pos && position < array.end) {
for (let i = 0; i < array.length; i++) {
const child = array[i];
if (child) {
if (child.pos === position) {
currentArray = array;
currentArrayIndex = i;
current = child;
return true;
}
else {
if (child.pos < position && position < child.end) {
forEachChild(child, visitNode, visitArray);
return true;
}
}
}
}
} return false;
}
}
}

解析列表

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

function parseList<T extends Node>(kind: ParsingContext, parseElement: () => T): NodeArray<T> {
const saveParsingContext = parsingContext;
parsingContext |= 1 << kind;
const list = [];
const listPos = getNodePos(); while (!isListTerminator(kind)) {
if (isListElement(kind, /*inErrorRecovery*/ false)) {
list.push(parseListElement(kind, parseElement)); continue;
} if (abortParsingListOrMoveToNextToken(kind)) {
break;
}
} parsingContext = saveParsingContext;
return createNodeArray(list, listPos);
}

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

    function parseListElement<T extends Node | undefined>(parsingContext: ParsingContext, parseElement: () => T): T {
const node = currentNode(parsingContext);
if (node) {
return consumeNode(node) as T;
} return parseElement();
}
currentNode 负责返回可复用的节点,除了基于 syntaxCursor 查找,还加了一些额外的限制,防止某些特殊情况会复用。
function currentNode(parsingContext: ParsingContext, pos?: number): Node | undefined {

        if (!syntaxCursor || !isReusableParsingContext(parsingContext) || parseErrorBeforeNextFinishedNode) {
return undefined;
} const node = syntaxCursor.currentNode(pos ?? scanner.getTokenFullStart()); // 存在语法错误的节点不能复用,因为我们需要重新解析,重新报错。
if (nodeIsMissing(node) || intersectsIncrementalChange(node) || containsParseError(node)) {
return undefined;
} const nodeContextFlags = node.flags & NodeFlags.ContextFlags;
if (nodeContextFlags !== contextFlags) {
return undefined;
} // 有些节点不能复用,因为存在一定场景导致复用出错
if (!canReuseNode(node, parsingContext)) {
return undefined;
} if (canHaveJSDoc(node) && node.jsDoc?.jsDocCache) { node.jsDoc.jsDocCache = undefined;
} return node;
}

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

function consumeNode(node: Node) {
scanner.resetTokenState(node.end);
nextToken();
return node;
}

不能复用的场景

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

比如泛型:

var a = b < c, d, e

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

  • a = b < c
  • d
  • e

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

var a = b<c,d,e>

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

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

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

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

结论

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

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

												

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. Windows应急响应-Auto病毒

    目录 应急背景 分析样本 开启监控 感染病毒 查看监控 分析病毒行为 autorun.inf分析 2.异常连接 3.进程排查 4.启动项排查 查杀 1.先删掉autorun.inf文件 2.使用xue ...

  2. C++20中对于并发方面的进步

    在当今的编程世界中,并发处理能力变得越来越重要.C++20 在并发方面带来了一些进步,使开发者能够更高效.更安全地编写多线程应用程序.这些进步主要包括: 作用域线程(Scoped Threads) 停 ...

  3. NoC简介

    一.什么是NoC NOC(Network on Chip)是一种用于片上系统(SoC, System on Chip)中的通信架构,它在芯片内为处理器.存储器.I/O 设备和其他硬件模块之间提供数据通 ...

  4. USB Type-C Power Role

    USB Power Role 是指 USB 设备在供电方面所扮演的角色,主要分为供电方(Provider)和受电方(Consumer).在 USB 供电协议中,电源角色的管理尤为重要,尤其是在 USB ...

  5. Android dtbo(3) 编译和验证

    您可以使用设备树编译器 (DTC) 编译设备树源文件.不过,在将叠加层 DT 应用于目标主 DT 之前,您还应该通过模拟 DTO 的行为来验证结果. 1. 通过DTC进行编译 构建主 DT .dts ...

  6. .NET云原生应用实践(一):从搭建项目框架结构开始

    开篇 很早之前就想做一套案例,介绍.NET下如何从零开始搭建一个云原生的应用程序.不过这个话题有点大,会要包含很多内容.我本打算从新建一个ASP.NET Core Web API应用程序开始介绍,但又 ...

  7. 云原生周刊:Gateway API v1.1 发布 | 2024.6.3

    开源项目推荐 Grafana Tanka Tanka 是 Grafana 开发的一款用于 Kubernetes 的灵活.可重用和简洁的配置工具,是使用 YAML 进行 Kubernetes 配置的一种 ...

  8. javascript语法--for in、for of和forEach

    首先看简单for循环效果,功能最基本,但可以实现所有循环功能 for (let i = 0; i < list.length; i++) { } 接下来看for in.for of和forEac ...

  9. Java中“=”克隆理解

    在Java中,对于基本类型可以使用"="来进行克隆,此时两个变量除了相等是没有任何关系的. 而对于引用类型却不能简单地使用"="进行克隆,这与java的内存空间 ...

  10. 矩阵怪 - 2024全新矩阵产品,一键分发抖音,快手,视频号,B站,小红书!

    本方案面向谁,解决了什么问题 本方案主要面向C端客户,特别是那些在各大短视频平台(如小红书.抖音.视频号.快手.B站等)上进行内容创作和分发的个人用户.自由职业者.小型团队或企业.这些用户通常面临着在 ...