从Svelte官网的示例中可以看到有一个tab是JS output,里面可以看到将svelte文件编译为js文件之后的产物。我们通过产物来看Svelte是如何实现各种功能的。
编译
-
解析(Parsing): Svelte 编译器首先会解析
.svelte
文件,将其转换成抽象语法树(AST)。在这个阶段,编译器会理解组件文件中的结构,包括<script>
、<style>
和<svelte>
部分。 -
转换(Transformation): 在 AST 的转换阶段,Svelte 编译器会对抽象语法树进行一系列的转换操作,将 Svelte 特有的语法和概念转化为标准的 JavaScript。这包括:
- 响应式声明转换: 将组件中使用
let
声明的变量转换为内部的状态管理逻辑。例如,将let count = 0;
转换为内部的状态变量和更新逻辑。 - 事件处理器转换: 将组件中的事件处理器转换为适当的 JavaScript 代码。例如,将
on:click={handleClick}
转换为原生的事件监听器。 - 样式转换: 处理
<style>
部分,将其转换为适当的 CSS 格式。Svelte 还支持局部作用域的样式,编译器会相应地处理这一特性。 - 模块化输出: 编译器会为组件生成模块化的输出,以便在应用中引入和使用组件。
- 响应式声明转换: 将组件中使用
-
生成(Generation): 在生成阶段,编译器会根据转换后的 AST 生成最终的 JavaScript 代码。这些代码包括组件的状态管理、事件处理逻辑、样式等。生成的代码通常是一个自包含的 JavaScript 模块。
-
运行时(Runtime): 生成的 JavaScript 代码包括 Svelte 的运行时库,它提供了组件的核心功能,如状态管理、事件处理等。这个运行时库负责在运行时处理组件的各种逻辑。
-
精简输出: Svelte 的编译器会尽量产生小而精简的输出。这是因为 Svelte 在编译时生成的代码是针对具体组件的,而不是通用的框架代码。这使得最终生成的 JavaScript 文件更小,加载更快。
parse 解析
- 接收模板字符串和解析选项。
- 创建
Parser
实例,执行解析。 - 对解析得到的 CSS 和 JavaScript 进行额外的检查,例如是否有重复的
<style>
标签,是否有多个脚本等。 - 返回解析得到的抽象语法树(AST),包括 HTML、CSS、实例 JavaScript、模块 JavaScript。
Parser
类
-
实例属性:
template
: 存储模板字符串。filename
: 存储模板所在的文件名。customElement
: 存储是否是自定义元素。css_mode
: 存储 CSS 模式('injected' | 'external' | 'none' | boolean)。index
: 当前解析位置的索引。stack
: 一个数组,用于跟踪当前解析的 HTML 结构。html
: 存储 HTML 结构的抽象语法树。css
: 存储 CSS 结构的抽象语法树。js
: 存储 JavaScript 结构的抽象语法树。meta_tags
: 存储 meta 标签的信息。last_auto_closed_tag
: 存储上一个自动关闭的标签的信息。
-
构造函数:
- 接收模板字符串和解析选项。
- 对模板进行初始化和预处理。
- 调用
fragment
函数,执行解析过程。
-
方法:
current()
: 获取当前正在解析的节点。acorn_error(err)
: 处理 Acorn(JavaScript 解析器)的错误。error({ code, message }, index)
: 抛出解析错误。eat(str, required, error)
: 检查模板是否以指定字符串开始,如果是则移动索引。match(str)
: 检查模板当前位置是否匹配指定字符串。match_regex(pattern)
: 在当前位置匹配指定正则表达式。allow_whitespace()
: 移动索引直到不是空白字符为止。read(pattern)
: 从当前位置读取匹配指定正则表达式的内容。read_identifier(allow_reserved)
: 读取标识符,可选地允许保留字。read_until(pattern, error_message)
: 从当前位置读取直到匹配指定正则表达式的内容。require_whitespace()
: 检查并移动索引,确保当前位置是空白字符。
Component类
- 初始化: 在构造函数中,接受组件的抽象语法树(AST)、源代码、组件名称、编译选项、统计信息和警告信息等。初始化阶段会将这些信息存储在类的实例属性中,并为后续的处理做准备。
- 处理模块 JS 代码:
walk_module_js
方法用于处理组件的模块级别的 JavaScript 代码。它会创建作用域,处理变量声明、全局变量、导入语句、导出语句等,并对 AST 进行相应的变更。 - 处理实例 JS 代码(在模板处理前):
walk_instance_js_pre_template
方法用于在模板处理之前处理组件实例级别的 JavaScript 代码。它会处理变量声明,注入与响应式声明相关的变量,并进行一些额外的处理。 - 处理模板: 在构造函数中,通过
new Fragment(this, ast.html)
创建了组件的Fragment
对象,表示组件的模板部分。在模板处理阶段,会进行一些处理,包括处理忽略注释、遍历模板的 JS 代码块等。 - 处理实例 JS 代码(在模板处理后):
walk_instance_js_post_template
方法用于在模板处理之后处理组件实例级别的 JavaScript 代码。它会调用post_template_walk
方法,该方法用于一些额外的步骤,如移除不需要的节点,处理导入和导出等。 - 提取 JavaScript 代码:
extract_javascript
方法用于提取 JavaScript 代码。这个方法会过滤掉不需要处理的节点,如 hoistable 节点、响应式声明节点等。 - 生成结果:
generate
方法用于生成最终的编译结果。它会根据处理后的 AST、样式表等信息,生成 JavaScript 代码和样式表,并返回一个包含相关信息的对象。 - 变量管理: 包括添加变量、添加引用、创建别名、应用样式表等方法,用于管理组件中涉及的变量和样式表。
compile
- 接收 Svelte 组件的源代码和编译选项。
- 使用默认选项补充传入的选项,包括默认的生成目标为 DOM,开发模式关闭,启用源映射,CSS 采用注入模式。
- 创建一个
Stats
实例用于统计编译性能,以及一个空数组用于存储编译过程中的警告。 - 调用
validate_options
函数验证选项的合法性。 - 开始统计解析阶段的性能,调用
parse
函数解析源代码,得到抽象语法树(AST)。 - 停止解析阶段的性能统计,开始统计创建组件阶段的性能。
- 创建
Component
实例,传入 AST、源代码、组件名称、编译选项、性能统计对象和警告数组。 - 停止创建组件阶段的性能统计。
- 如果生成选项为
false
,返回null
;如果为'ssr'
,调用render_ssr
生成 SSR 代码;否则,调用render_dom
生成 DOM 代码。 - 最后,调用组件的
generate
方法,生成最终的编译结果。
编译产物
create_fragment 方法
create_fragment
函数实际上是用来生成组件的 DOM 结构,并包含了对应的事件处理逻辑。这个函数的目的是为了在组件初始化时创建 DOM 结构,以及在组件更新时更新 DOM。
js
function create_fragment(ctx) {
let p;
let t0;
let button;
let dispose;
return {
c() {
p = /*createElementForParagraph*/ create_element('p');
t0 = /*createText*/ create_text('The count is ' + ctx.count);
button = /*createElementForButton*/ create_element('button');
/*addEventListenersForButton*/ add_event_listener(button, 'click', /*handleClick*/ ctx.increment);
},
m(target, anchor) {
/*insertNode*/ insert_node(p, target, anchor);
/*insertNode*/ insert_node(t0, p, null);
/*insertNode*/ insert_node(button, p, null);
/*insertNode*/ insert_node(/*createText*/ create_text('Increment'), button, null);
dispose = [
/*addEventListenersForButton*/ add_event_listener(button, 'click', /*handleClick*/ ctx.increment),
];
},
p(ctx, dirty) {
if (dirty & /*count*/ 1) {
/*setTextNode*/ set_text(t0, 'The count is ' + ctx.count);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
/*removeNode*/ detach_node(p);
}
/*destroyBlock*/ destroy_each(dispose);
},
};
}
create_frament里面返回了一个对象,对象中有以下几个方法:
c()
方法: 这是"create"(创建)的缩写,用于创建虚拟 DOM 的节点。在这个方法中,我们调用了一些函数(例如createElementForParagraph
、createText
、createElementForButton
)来创建节点,并通过addEventListenersForButton
添加事件监听器。m()
方法: 这是"mount"(挂载)的缩写,用于将虚拟 DOM 插入到实际的 DOM 中。在这个方法中,我们调用了一些函数(例如insertNode
)来将创建的节点插入到指定的目标和锚点位置,并设置事件监听器。p()
方法: 这是"update"(更新)的缩写,用于处理组件状态的变化。在这个方法中,我们检查dirty
参数,根据状态的变化更新相应的节点。i()
和o()
方法: 这是"intro"和"outro"的缩写,用于处理节点的进入和退出动画。在这个简化的例子中,它们被设置为noop
,表示没有动画。d()
方法: 这是"destroy"(销毁)的缩写,用于在组件销毁时清理资源。在这个方法中,我们调用一些函数(例如removeNode
)来移除节点,并通过destroy_each
来销毁事件监听器。