无虚拟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 来销毁事件监听器。
相关推荐
何遇mirror2 个月前
如何在 Svelte 中使用 <svelte:transition> 和 <svelte:animate> 来创建动画效果
游戏·svelte
天涯学馆2 个月前
Svelte Store与Vuex:轻量级状态管理对比
前端·vue·vuex·svelte
码上飞扬2 个月前
前端框架对比选择:如何在众多技术中找到最适合你的
vue.js·前端框架·react·angular·svelte
云轩奕鹤3 个月前
生财有迹 | 您专属的资产跟踪与分析工具
开源·应用·svelte
天涯学馆4 个月前
Svelte Store:状态管理的Svelte方式
前端·前端框架·svelte
上杉达也5 个月前
【Svelte从入门到精通】对比篇——for
前端·javascript·svelte
上杉达也5 个月前
【Svelte从入门到精通】对比篇——reactivity
前端·javascript·svelte
coderpai5 个月前
(一)SvelteKit教程:hello world
svelte
上杉达也5 个月前
【Svelte从入门到精通】实战篇——Alert组件之slot交互
前端·javascript·svelte
上杉达也5 个月前
【Svelte从入门到精通】实战篇——TodoList之页面布局
前端·javascript·svelte