Svelte从入门到精通——库
发表于 阅读时长15分钟
目录
在正式解读源码之前,笔者会和读者朋友们一起了解一些npm库的功能作用,每一个都和我们接下来的源码解读息息相关。在阅读完本章后,相信读者们在接下来的源码讲解中,不至于遇到一个功能库而一头雾水。本章中,我们将了解以下功能库:
AST
在认识这些工具库之前,我们需要先了解一个概念:抽象语法树(Abstract Syntax Tree,AST)。这个概念非常重要,它是实现一个编译器的核心,后面介绍的各种库都是围绕这个核心概念服务的。
抽象语法树是以树形结构数据来表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。其拥有一套约定的规范。
AST运用非常广泛:代码格式化、代码高亮、代码错误校验、代码转换等等,可以说,谁掌握了AST的使用,谁就掌握了前端代码编译的天下。
AST的生成通常涉及到两个步骤:词法分析、语法分析。
词法分析
词法分析(Lexical analyzer)是由词法分析器来扫描源代码,将扫描出来的字符与Javascript关键字进行比较,生成一个个不可再分割的最小单元,这些单元被称为Token。
比如我们有一段代码:
let str = 'svelte';
那么经过词法分析后,得到的是类似如下的一个数组:
[
{ type: 'Keyword', value: 'let' },
{ type: 'Identifier', value: 'str' },
{ type: 'Punctuator', value: '=' },
{ type: 'String', value: '"svelte"' }
]
语法分析
语法分析(Syntax analyzer)就是将词法分析阶段生成的Token转换为抽象语法树。
在astexplorer中,我们能够试验各种ast工具对源码的抽象。
acorn
如果读者此前没有听说过acorn,那有一个鼎鼎大名的库相信读者一定了解,那就是webpack。webpack底层将代码转换成抽象语法树时使用的便是acorn。使用acorn转换得到的返回值,是符合The ESTree Spec规范的对象。
除了webpack之外,Rollup和Babel的@babel/parser等工具都使用到了acorn。
市面上除了acorn外,还流行其他js解析器,如:Esprima、UglifyJS、Shift等。
parse
acorn.parse(str, options)
str就是要解析的字符串内容,options是一个对象,其中只有ecmaVersion是必填的,用于指定指定要解析的 ECMAScript 版本。
export interface Options {
ecmaVersion: ecmaVersion
sourceType?: "script" | "module"
onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
allowReserved?: boolean | "never"
allowReturnOutsideFunction?: boolean
allowImportExportEverywhere?: boolean
allowAwaitOutsideFunction?: boolean
allowSuperOutsideMethod?: boolean
allowHashBang?: boolean
checkPrivateFields?: boolean
locations?: boolean
onToken?: ((token: Token) => void) | Token[]
onComment?: ((
isBlock: boolean, text: string, start: number, end: number, startLoc?: Position,
endLoc?: Position
) => void) | Comment[]
ranges?: boolean
program?: Node
sourceFile?: string
directSourceFile?: string
preserveParens?: boolean
}
目前我们只需大概了解参数即可,感兴趣的读者可阅读acorn了解其他参数的功能。
import * as acorn from "acorn";
let str = `let a = 0;
let b = 1;
let c = a + b;
a++;
window.e = 2;
f = 3;
`
const ast = acorn.parse(str, {
ecmaVersion: 2023
});
console.log(ast);

我们在AST Explorer中,得到的是同样的结果:

使用acorn解析之后得到的数据便是ast抽象语法树,树形结构的数据必然存在一个或多个叶子节点,在每个节点上,都有一个参数type来标记这个节点属于什么类型。我们了解一下在一个AST对象中的各种type的意义。
type
Literal
Literal是字面量的意思,比如 let str = 'svelte'中'svelte'就是一个字符串字面量


Identifier
Identifier指标识符,变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。
const str = 'world';
function greet(val) {
console.log(val);
}
const foo = {
bar: 'svelte'
}
就拿这一小段代码来说,它的Identifier类型节点就将近十个:

Statement
语句,一些常见的执行代码用此类型,比如:
{}是BlockStatement
do {} while()是DoWhileStatement
debugger是DebuggerStatement

$:xxx是LabeledStatement。Svelte中的反应性的监听,巧妙地运用了这个语句。
不再逐一展示。以下是常见语句:
{} // BlockStatement
do {} while() // DoWhileStatement
while() {} // WhileStatement
for(let i = 0; i < n; i++){} // ForStatement
for(let i in obj){} // ForInStatement
for(let i of arr){} // ForOfStatement
debugger; // DebuggerStatement
throw Error(); // ThrowStatement
label: xxx // LabeledStatement
break; // BreakStatement
continue; // ContinueStatement
return; // ReturnStatement
if(true) {} // IfStatement
switch(true) {} // SwitchStatement
try {} catch(e) {} // TryStatement
...等等
Declaration
相比于Statement语句用来控制执行逻辑,Declaration声明语句将非空标识符绑定到常量、Class、变量、函数或导入导出当中。


let a = ''; // VariableDeclaration
function a() {} // FunctionDeclaration
class A {} // ClassDeclaration
import a from 'a'; // ImportDeclaration
export default a = 1; // ExportDefaultDeclaration
...等等
Expression
Expression表达式,通过调用运算符或者函数来得到一个计算后的返回值。
[1,2,3] // ArrayExpression 数组表达式
({a: ''}) // ObjectExpression 对象表达式
this; // ThisExpression this表达式
1 + 2 // BinaryExpression 二元表达式
true ? '' : '' // ConditionalExpression 条件表达式
a++; // UpdateExpression 更新表达式
-1 // UnaryExpression 一元表达式
a = 1; // AssignmentExpression 赋值表达式
new a(); // NewExpression New表达式
a = function() {} // FunctionExpression 函数表达式
() => {} // ArrowFunctionExpression 箭头函数表达式
...等等
Program
正常情况下,最顶级的节点的type即是Program
还有Class、Modules、Directive、File、Comment等type不一一演示了。
code-red
code-red是一个基于acorn二次封装的工具库。 我们看一下基本使用:
import { x, b, print } from 'code-red';
const expression = x`i + j`;
const body = b`i + j`;
console.log('expression', expression);
console.log('body', body);
const hello = x`i`;
const world = x`j`;
const expression2 = x`${hello}+${world}`;
console.log('hello', hello);
console.log('world', world);
hello.name = 'abc';
world.name = 'efg';
console.log('code', print(expression2).code);

x方法内部是基于acorn.parseExpressionAt()的封装:
export function x(strings, ...values) {
const str = join(strings);
/** @type {CommentWithLocation[]} */
const comments = [];
try {
let expression =
/** @type {Expression & { start: Number, end: number }} */ (
acorn.parseExpressionAt(str, 0, acorn_opts(comments, str))
);
const match = /\S+/.exec(str.slice(expression.end));
if (match) {
throw new Error(`Unexpected token '${match[0]}'`);
}
expression = /** @type {Expression & { start: Number, end: number }} */ (
inject(str, expression, values, comments)
);
return expression;
} catch (err) {
handle_error(str, err);
}
}
b方法是基于acorn.parse()的封装:
export function b(strings, ...values) {
const str = join(strings);
/** @type {CommentWithLocation[]} */
const comments = [];
try {
let ast = /** @type {any} */ (acorn.parse(str, acorn_opts(comments, str)));
ast = inject(str, ast, values, comments);
return ast.body;
} catch (err) {
handle_error(str, err);
}
}
periscopic
从ast对象中分析出变量作用域。
import { analyze } from 'periscopic';
const ast = acorn.parse(str, options);
const { map, globals, scope } = analyze(ast);
analyze
periscopic导出一个方法analyze,调用此方法得到一个对象,该对象有以下属性:
map:是一个WeakMap类型的对象(WeakMap<Node, Scope>), 对象的keys是创建出scope的ast节点。globals:是一个Map类型的对象(Map<string, Node>),收集所有被引用的但没有被声明的Identifier,Identifier我们在acorn中已详细讲解。scope:程序中的顶级作用域,Scope类型
import * as acorn from 'acorn';
import { analyze } from 'periscopic';
let a = `let a = 0;
window.b = 1;
function c() {
let d = 2;
}
e = 3;
`;
const ast = acorn.parse(a, {
ecmaVersion: 2023,
});
const { map, globals, scope } = analyze(ast);
console.log('map', map);
console.log('globals', globals);
console.log('scope', scope);
我们首先看下map的打印数据:
periscopic分析出两个创建了scope的节点。

globals中我们可以得知,e和window这两个变量我们没有进行声明而直接用了。

scope,top-level级别的作用域

Scope
Scope类型的对象有以下参数:
scope.block:如果scope是BlockStatement创建的,则是true,否则是falsescope.parent:父级的Scope对象scope.declarations:一个Map类型对象(Map<string, Node>),在当前scope中声明的所有变量,Node节点值是Declaration类型节点scope.initialised_declarations:一个Set类型对象(Set<string>),在当前scope中声明且初始化了的对象scope.references:一个Set类型对象(Set<string>),在当前scope中的所有变量名
Scope类型对象有两个方法:
scope.has(name):如果name在当前作用域或祖先作用域中声明过,则返回truescope.find_owner(name):返回声明name的Scope对象
我们以上述例子const { map, globals, scope } = analyze(ast)中返回的scope为例:
block:不是BlockStatement创建的,所以false
parent:没有父级作用域,所以null
declarations: 变量a和c是在当前作用域声明的
initialised_declarations: 变量a在当前作用域声明并且初始化
references: 当前作用域中的变量有a、window、c、d、e
extract
periscopic还导出了两个方法extract_identifiers和extract_names用来提取特定值。
import * as acorn from 'acorn';
import { extract_identifiers, extract_names } from 'periscopic';
let a = `let a = 0;
window.b = 1;
function c() {
let d = 2;
}
e = 3;
`;
const ast = acorn.parse(a, {
ecmaVersion: 2023,
});
const data = ast.body[2].id;
const identifiers = extract_identifiers(data);
const names = extract_names(data);
console.log('identifiers', identifiers);
console.log('names', names);

在控制台中输出:

estree-walker
用来遍历ast对象。
import { walk } from "estree-walker";
const ast = acorn.parse(str, options);
walk(ast, {
enter(node, parent, prop, index) {
// some code happens
},
leave(node, parent, prop, index) {
// some code happens
}
});
enter / leave
enter方法:进入节点时调用 leave方法:离开遍历的节点时调用
- 在
enter方法中,调用this.skip()可以阻止当前节点的子节点被遍历,或者阻止leave方法被调用 - 在
enter或leave方法中调用this.replace(new_node)可以替换节点 - 在
enter或leave方法中调用this.remove可以删除当前节点
具有相同作用的包还有:estraverse、acorn-walk等。
import * as acorn from "acorn";
import { walk } from "estree-walker";
let a = `let a = 0;`;
let ast = acorn.parse(a, { ecmaVersion: 2023 });
walk(ast, {
enter(node) {
console.log('enter', node);
},
leave(node) {
console.log('leave', node);
}
})

我们对照着解析出来的ast对象来过一遍流程:
- 进入Program节点
enter Program - 进入VariableDeclaration节点
enter VariableDeclaration - 进入VariableDeclarator节点
enter VariableDeclarator - 进入Identifier节点
enter Identifier,没有子节点了,回退到上一级父节点VariableDeclaratorleave Identifier - 进入Literal节点
enter Literal,没有子节点了,回退到上一级父节点VariableDeclaratorleave Literal - VariableDeclarator的子节点已遍历完了,回退到上一级VariableDeclaration
leave VariableDeclarator - VariableDeclaration的子节点也遍历完了,继续回退到上一级
leave VariableDeclaration - 结束遍历
leave Program
skip
this.skip()功能演示:
let a = `let a = 0;`;
let ast = acorn.parse(a, { ecmaVersion: 2023 });
walk(ast, {
enter(node) {
console.log('enter', node);
if (node.type == 'VariableDeclarator') {
this.skip();
}
},
leave(node) {
console.log('leave', node);
}
})

replace
this.replace()方法演示:
比如我们想把let a = 0;改成let a = 1;,0对应的是Literal的节点

import * as acorn from "acorn";
import { walk } from "estree-walker";
import { generate } from "escodegen";
let a = `let a = 0;`;
let ast = acorn.parse(a, { ecmaVersion: 2023 });
walk(ast, {
enter(node) {
console.log('enter', node);
if (node.type === 'Literal') {
this.replace({
...node,
raw: '1',
value: 1
})
}
},
leave(node) {
console.log('leave', node);
}
})
const str = generate(ast);
console.log('new str', str);
我们把原来节点的raw和value属性都改成1,然后调用this.replace()来替换。

escodegen
用于把满足 Estree 标准的 AST 转换为 ESMAScript 代码
generate
escodegen.generate(AST[, options]);
options的详细参数见escodegen options
在estree-walk的最后一节演示中,我们已经使用了escodegen来展示它的功能。
小结
本章我们学习了:
- AST的基本概念
- acorn的使用,ast节点的不同type,基于acorn封装的code-red的使用
- periscopic的使用
- estree-walker的使用
- escodegen的使用
通过介绍这几个库,相信笔者已经发现,这些其实就是实现一个编译器所需要的几个功能:解析代码文件 -> 分析ast,ast转换 -> 生成新代码。