无虚拟DOM的框架-Svelte(编译)

从Svelte官网的示例中可以看到有一个tab是JS output,里面可以看到将svelte文件编译为js文件之后的产物。我们通过产物来看Svelte是如何实现各种功能的。

编译

  1. 解析(Parsing): Svelte 编译器首先会解析 .svelte 文件,将其转换成抽象语法树(AST)。在这个阶段,编译器会理解组件文件中的结构,包括 <script><style><svelte> 部分。

  2. 转换(Transformation): 在 AST 的转换阶段,Svelte 编译器会对抽象语法树进行一系列的转换操作,将 Svelte 特有的语法和概念转化为标准的 JavaScript。这包括:

    • 响应式声明转换: 将组件中使用 let 声明的变量转换为内部的状态管理逻辑。例如,将 let count = 0; 转换为内部的状态变量和更新逻辑。
    • 事件处理器转换: 将组件中的事件处理器转换为适当的 JavaScript 代码。例如,将 on:click={handleClick} 转换为原生的事件监听器。
    • 样式转换: 处理 <style> 部分,将其转换为适当的 CSS 格式。Svelte 还支持局部作用域的样式,编译器会相应地处理这一特性。
    • 模块化输出: 编译器会为组件生成模块化的输出,以便在应用中引入和使用组件。
  3. 生成(Generation): 在生成阶段,编译器会根据转换后的 AST 生成最终的 JavaScript 代码。这些代码包括组件的状态管理、事件处理逻辑、样式等。生成的代码通常是一个自包含的 JavaScript 模块。

  4. 运行时(Runtime): 生成的 JavaScript 代码包括 Svelte 的运行时库,它提供了组件的核心功能,如状态管理、事件处理等。这个运行时库负责在运行时处理组件的各种逻辑。

  5. 精简输出: 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类

  1. 初始化: 在构造函数中,接受组件的抽象语法树(AST)、源代码、组件名称、编译选项、统计信息和警告信息等。初始化阶段会将这些信息存储在类的实例属性中,并为后续的处理做准备。
  2. 处理模块 JS 代码: walk_module_js 方法用于处理组件的模块级别的 JavaScript 代码。它会创建作用域,处理变量声明、全局变量、导入语句、导出语句等,并对 AST 进行相应的变更。
  3. 处理实例 JS 代码(在模板处理前): walk_instance_js_pre_template 方法用于在模板处理之前处理组件实例级别的 JavaScript 代码。它会处理变量声明,注入与响应式声明相关的变量,并进行一些额外的处理。
  4. 处理模板: 在构造函数中,通过 new Fragment(this, ast.html) 创建了组件的 Fragment 对象,表示组件的模板部分。在模板处理阶段,会进行一些处理,包括处理忽略注释、遍历模板的 JS 代码块等。
  5. 处理实例 JS 代码(在模板处理后): walk_instance_js_post_template 方法用于在模板处理之后处理组件实例级别的 JavaScript 代码。它会调用 post_template_walk 方法,该方法用于一些额外的步骤,如移除不需要的节点,处理导入和导出等。
  6. 提取 JavaScript 代码: extract_javascript 方法用于提取 JavaScript 代码。这个方法会过滤掉不需要处理的节点,如 hoistable 节点、响应式声明节点等。
  7. 生成结果: generate 方法用于生成最终的编译结果。它会根据处理后的 AST、样式表等信息,生成 JavaScript 代码和样式表,并返回一个包含相关信息的对象。
  8. 变量管理: 包括添加变量、添加引用、创建别名、应用样式表等方法,用于管理组件中涉及的变量和样式表。

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里面返回了一个对象,对象中有以下几个方法:

  1. c() 方法: 这是"create"(创建)的缩写,用于创建虚拟 DOM 的节点。在这个方法中,我们调用了一些函数(例如 createElementForParagraphcreateTextcreateElementForButton)来创建节点,并通过 addEventListenersForButton 添加事件监听器。
  2. m() 方法: 这是"mount"(挂载)的缩写,用于将虚拟 DOM 插入到实际的 DOM 中。在这个方法中,我们调用了一些函数(例如 insertNode)来将创建的节点插入到指定的目标和锚点位置,并设置事件监听器。
  3. p() 方法: 这是"update"(更新)的缩写,用于处理组件状态的变化。在这个方法中,我们检查 dirty 参数,根据状态的变化更新相应的节点。
  4. i()o() 方法: 这是"intro"和"outro"的缩写,用于处理节点的进入和退出动画。在这个简化的例子中,它们被设置为 noop,表示没有动画。
  5. d() 方法: 这是"destroy"(销毁)的缩写,用于在组件销毁时清理资源。在这个方法中,我们调用一些函数(例如 removeNode)来移除节点,并通过 destroy_each 来销毁事件监听器。
相关推荐
冴羽3 天前
SvelteKit 最新中文文档教程(23)—— CLI 使用指南
前端·javascript·svelte
冴羽4 天前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte
冴羽8 天前
SvelteKit 最新中文文档教程(21)—— 最佳实践之图片
前端·javascript·svelte
冴羽9 天前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
冴羽yayujs15 天前
SvelteKit 最新中文文档教程(19)—— 最佳实践之身份认证
前端·javascript·vue.js·react.js·前端框架·svelte·sveltekit
冴羽16 天前
SvelteKit 最新中文文档教程(19)—— 最佳实践之身份认证
前端·javascript·svelte
冴羽17 天前
SvelteKit 最新中文文档教程(18)—— 浅层路由和 Packaging
前端·javascript·svelte
冴羽23 天前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·svelte
冴羽25 天前
SvelteKit 最新中文文档教程(16)—— Service workers
前端·javascript·svelte
冴羽1 个月前
SvelteKit 最新中文文档教程(15)—— 链接选项
前端·javascript·svelte