TS 原理详细解读(6)--语法增量解析
呃....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();
}
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)--语法增量解析的更多相关文章
- TS 原理详细解读(5)语法2-语法解析
在上一节介绍了语法树的结构,本节则介绍如何解析标记组成语法树. 对应的源码位于 src/compiler/parser.ts. 入口函数 要解析一份源码,输入当然是源码内容(字符串),同时还提供路径( ...
- TypeScript 源码详细解读(4)语法1-语法树
在上一节介绍了标记的解析,就相当于识别了一句话里有哪些词语,接下来就是把这些词语组成完整的句子,即拼装标记为语法树. 树(tree) 树是计算机数据结构里的专业术语.就像一个学校有很多年级,每个年级下 ...
- SpringMVC 原理 - 设计原理、启动过程、请求处理详细解读
SpringMVC 原理 - 设计原理.启动过程.请求处理详细解读 目录 一. 设计原理 二. 启动过程 三. 请求处理 一. 设计原理 Servlet 规范 SpringMVC 是基于 Servle ...
- C++多态的实现及原理详细解析
C++多态的实现及原理详细解析 作者: 字体:[增加 减小] 类型:转载 C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型 ...
- live555中ts流详细解析
live555中ts流详细解析 该文档主要是对live555源码下testProgs中testMPEG2TransportStreamer服务器端的详细分析.主要分析ts流实现的总体调用流程.(重新整 ...
- 深入理解NIO(三)—— NIO原理及部分源码的解析
深入理解NIO(三)—— NIO原理及部分源码的解析 欢迎回到淦™的源码看爆系列 在看完前面两个系列之后,相信大家对NIO也有了一定的理解,接下来我们就来深入源码去解读它,我这里的是OpenJDK-8 ...
- MemCache超详细解读
MemCache是什么 MemCache是一个自由.源码开放.高性能.分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高 ...
- MemCache超详细解读 图
http://www.cnblogs.com/xrq730/p/4948707.html MemCache是什么 MemCache是一个自由.源码开放.高性能.分布式的分布式内存对象缓存系统,用于 ...
- MemCache详细解读
MemCache是什么 MemCache是一个自由.源码开放.高性能.分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高 ...
- 【公众号系列】超详细SAP HANA JOB全解析
公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[公众号系列]超详细SAP HANA JOB全解 ...
随机推荐
- 配置 ZRAM,实现 Linux 下的内存压缩,零成本低开销获得成倍内存扩增
由于项目需求,笔者最近在一台 Linux 服务器上部署了 ElasticSearch 集群,却发现运行过程中经常出现查询速度突然降低的问题,登录服务器后发现是物理内存不足,导致机器频繁发生页面交换.由 ...
- Linux 提高cache命中率方法
提高缓存命中率是优化系统性能的关键策略之一.以下是一些提高缓存命中率的有效方法: 数据局部性优化: 空间局部性:优化数据访问模式,使得数据访问在空间上连续,比如通过循环展开和数据重排. 时间局部性:确 ...
- Android10.0系统启动之Launcher(桌面)启动流程-[Android取经之路]
Launcher的启动经过了三个阶段: 第一个阶段:SystemServer完成启动Launcher Activity的调用 第二个阶段:Zygote()进行Launcher进程的Fork操作 第三个 ...
- linux内核 快速分片,技术|Linux slabtop命令——显示内核片缓存信息
Linux内核需要为临时对象如任务或者设备结构和节点分配内存,缓存分配器管理着这些类型对象的缓存.现代Linux内核部署了该缓存分配器以持有缓存,称之为片.不同类型的片缓存由片分配器维护.本文集中讨论 ...
- 2023年6月中国数据库排行榜:OceanBase 连续七月踞榜首,华为阿里谋定快动占先机
群雄逐鹿,酣战墨坛. 2023年6月的 墨天轮中国数据库流行度排行 火热出炉,本月共有273个数据库参与排名.本月排行榜前十变动不大,可以用一句话概括为:OTO 组合连续两月开局,传统厂商GBase南 ...
- 妙用编辑器:使用Notepad--宏功能提高维护指令生成生成效率
应用场景 日常维护工作中,需要快速生成一批指令来完成某些操作,比如:快速添加一批节点. 目标指令列表如下: ADD NODE: ID=1, NAME="NODE_1"; ADD N ...
- Curve 分布式存储在 KubeSphere 中的实践
Curve 介绍 Curve 是网易开发的现代存储系统,目前支持文件存储 (CurveFS) 和块存储 (CurveBS).现在它作为一个沙盒项目托管在 CNCF. Curve 是一个高性能.轻量级操 ...
- nginx配置tomcat的负载均衡记录
实现效果 (1)浏览器地址栏输入地址 http://192.168.17.129/edu/a.html,负载均衡效果,平均在 8080和 8081 端口中. 准备工作 (1)准备两台 tomcat 服 ...
- Python如何完成一个上课点名系统!
阅读目录 一.准备工作 二.预览 三.思路 四.源代码 五.总结 一.准备工作 1.Tkinter Tkinter 是 python 内置的 TK GUI 工具集.TK 是 Tcl 语言的原生 GUI ...
- C语言数据类型和变量
目录 1.数据类型介绍 1.1字符型 1.2整形 1.3浮点型 1.4布尔类型 1.5各种数据类型长度 1.5.1sizeof操作符 1.5.2数据类型长度 1.5.3 sizeof中表达式不计算 2 ...