Garlic Garlic

Svelte从入门到精通——流程

发表于 阅读时长26分钟

本章笔者将演示Svelte从编译到运行的整体流程。

这里笔者使用的Node版本为18.15.0

首先我们下载源码,然后把版本分支切换到4.2.12分支。

git clone git@github.com:sveltejs/svelte.git

项目结构

Svelte使用的是monorepo的管理方式,首先我们参考README.md的建议安装依赖:

pnpm install

然后我们进入到sites/svelte.dev目录,执行pnpm run dev。 大概率会遇到这个问题: alt text

缺少sharp,我们按照提示安装:

pnpm add --force @img/sharp-darwin-arm64

重新运行,遇到一个warning: alt text

这是因为svelte.dev下缺少了一个.svelte-kit目录。 我们可以执行pnpm run checkalt text

里面的svelte-kit sync帮助我们同步下载这个目录。 alt text

现在我们看到svelte.dev下已经有了.svelte-kit目录。 alt text

我们重新运行下pnpm run dev,正常情况能运行起来。

请读者朋友们结合本章内容和源码对比阅读,效果更佳。

compile阶段

参照svelte-loader中的逻辑:

svelte.preprocess(source, options.preprocess).then(processed => {
  if (processed.dependencies && this.addDependency) {
    for (let dependency of processed.dependencies) {
      this.addDependency(dependency);
    }
  }

  if (processed.map) compileOptions.sourcemap = processed.map;

  const compiled = svelte.compile(processed.toString(), compileOptions);
  let { js, css, warnings } = compiled;

  callback(null, js.code, js.map);
})

首先Svelte文件经过预处理,比如像要添加typescript支持等,然后调用compile,最后返回js.code

为了简单直观地查看到各个阶段的执行结果,我们在网站首页sites/svelte.dev/src/routes/+layout.svelte直接引入svelte编译器文件:

  import '@sveltejs/site-kit/styles/index.css';

  import { browser } from '$app/environment';
  import { page } from '$app/stores';
  import { Icon, Shell, Banners } from '@sveltejs/site-kit/components';
  import { Nav, Separator } from '@sveltejs/site-kit/nav';
  import { Search, SearchBox } from '@sveltejs/site-kit/search';
+ import { compile, preprocess } from '../../../../packages/svelte/src/compiler';

然后我们仿照svelte-loader的逻辑,先调用preprocess,然后把预处理的结果打印出来,因为我们本次重点在编译和运行,所以我们只是打印经过preprocess后的结果,在compile阶段,我们直接传入Svelte文件的字符串内容。

let str = `<script>
  let count = 0;
  const addCount = () => {
    count++;
  }
<\/script>

<button on:click={addCount}>add</button>
count:{count}
`;
preprocess(str, {}).then(preResult => {
  console.log('preprocess result', preResult);
  let result = compile(str);
  console.log('compile result', result);
});

进入到网站目录cd sites/svelte.dev,执行pnpm run dev把网站跑起来,在控制台中查看打印结果: alt text

parse

compile(str)的内部逻辑首先是parse,地址:packages/svelte/src/compiler/compile/index.js

  console.log('svelte before parse', source);
  const ast = parse(source, options);
  console.log('svelte after parse', ast);

alt text
source即是我们的模板字符串,即Svelte文件的内容。

alt text
经过转换后得到ast对象

ast = {
  css: {}
  html: {},
  instance: {},
  module: {}
}

点击parse,进入到packages/svelte/src/compiler/parse/index.js
parse内部有个Parser类,通过实例化该类,对模板字符串文件进行解析。

const parser = new Parser(template, options);

Parser内部则通过四种不同的类型的节点解析器,分别是fragmenttagmustachetext。通过遍历文件的字符串内容,轮流调用这几个方法。

首先,我们没有使用<style></style>标签,忽略css解析的步骤。
在初始化时,默认的html内容整体的type是Fragment。把解析后的html数据存储到this.html

this.html = {
  start: null,
  end: null,
  type: 'Fragment',
  children: []
};

开始解析我们的Svelte文件内容,最开始解析<script>...的最左侧<,符合tag的解析。

if (parser.match('<')) {
  return tag;
}

点击tag,进入packages/svelte/src/compiler/parse/state/tag.js。很明显,script符合以下逻辑:

const specials = new Map([
  [
    'script',
    {
      read: read_script,
      property: 'js'
    }
  ],
  [
    'style',
    {
      read: read_style,
      property: 'css'
    }
  ]
]);

调用read_script

export default function read_script(parser, start, attributes) {
  ...
  let ast;
  try {
    ast = acorn.parse(source);
  } catch (err) {
    parser.acorn_error(err);
  }
  ...
  return {
    type: 'Script',
    start,
    end: parser.index,
    context: get_context(parser, attributes, start),
    content: ast
  };
}

点击acorn.parse,我们看到里面的核心便是调用code-readparse来解析js的内容:

export const parse = (source) =>
  code_red.parse(source, {
    sourceType: 'module',
    ecmaVersion: 13,
    locations: true
  });

这部分便是ast对象中的instance属性中的内容: alt text
到这里我们已经完成了对<script>...</script>标签内容的解析

接下来是重点处理的html的内容。

<script></script>解析完成之后,我们遇到了一个空行,空字符串也不能忽略,继续用fragment方法判断。

export default function fragment(parser) {
  ...

  return text;
}

使用text方法来解析这个空行内容。返回的格式:

export default function text(parser) {
  ...

  const node = {
    start,
    end: parser.index,
    type: 'Text',
    raw: data,
    data: decode_character_references(data, false)
  };

  parser.current().children.push(node);
}

alt text

空行解析完成,我们遇到了<button>标签,此时仍旧是调用fragment中的tag方法。

const type = meta_tags.has(name)
    ? meta_tags.get(name)
    : regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
    ? 'InlineComponent'
    : name === 'svelte:fragment'
    ? 'SlotTemplate'
    : name === 'title' && parent_is_head(parser.stack)
    ? 'Title'
    : name === 'slot'
    ? 'Slot'
    : 'Element';

const element = {
  start,
  end: null,
  type,
  name,
  attributes: [],
  children: []
};

我们遇到的不是<script><style>此类标签,也不是Svelte的自定义标签,最后得到的是Elementtype。

之后开始读取标签内属性的内容:

while ((attribute = read_attribute(parser, unique_names, is_top_level_script_or_style))) {
  element.attributes.push(attribute);
  parser.allow_whitespace();
}
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
let end = parser.index;
parser.allow_whitespace();
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));

parser.read_until读取到on:click

function get_directive_type(name) {
  if (name === 'use') return 'Action';
  if (name === 'animate') return 'Animation';
  if (name === 'bind') return 'Binding';
  if (name === 'class') return 'Class';
  if (name === 'style') return 'StyleDirective';
  if (name === 'on') return 'EventHandler';
  if (name === 'let') return 'Let';
  if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}

通过get_directive_type来得到attribute的type是EventHandler类型。

通过read_attribute_value来获取on:click={}{}内的值。

if (parser.eat('=')) {
  parser.allow_whitespace();
  value = read_attribute_value(parser, is_static);
  end = parser.index;
} 

read_attribute_value内部调用read_sequence

try {
  value = read_sequence(
    parser,
    () => {
      // handle common case of quote marks existing outside of regex for performance reasons
      if (quote_mark) return parser.match(quote_mark);
      return !!parser.match_regex(regex_starts_with_invalid_attr_value);
    },
    'in attribute value'
  );
} 

read_sequence内部则调用read_expression(parser);,深挖其核心逻辑,最终是调用code-redparseExpressionAt来解析{}的内容,将这部分内容赋值到expression属性中。

import * as code_red from 'code-red';

export const parse_expression_at = (source, index) =>
code_red.parseExpressionAt(source, index, {
  sourceType: 'module',
  ecmaVersion: 13,
  locations: true
});

alt text

此时我们已经解析到<button on:click={addCount}>

接着往下,这个Elementtype的children值只有一个节点,那就是add文案所代表的Text节点 alt text
之后我们算是已经解析到</button>

接着往下我们遇到count:,同样是Text节点。 alt text

接着遇到{,继续调用fragment内的mustache方法,解析得到MustacheTag类型的节点。

const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.current().children.push({
  start,
  end: parser.index,
  type: 'MustacheTag',
  expression
});

alt text

Component

执行完const ast = parse(source, options);后,我们进入下一步:

const component = new Component(
  ast,
  source,
  options.name || get_name_from_filename(options.filename) || 'Component',
  options,
  stats,
  warnings
);

Component中几个关键的步骤:

this.walk_module_js();
this.walk_instance_js_pre_template();
this.fragment = new Fragment(this, ast.html);
this.walk_instance_js_post_template();

我们没有声明过context='module'的script内容,所以跳过walk_module_js

walk_instance_js_pre_template中调用create_scopes来解析script标签内的作用域:

const { scope: instance_scope, map, globals } = create_scopes(script.content);

可以把这些变量打印出来看下console.log('svelte Component walk_instance_js_pre_template', instance_scope, map, globals);

create_scopes内部是使用了periscopicanalyse方法。

import { analyze, Scope, extract_names, extract_identifiers } from 'periscopic';

/**
 * @param {import('estree').Node} expression
 */
export function create_scopes(expression) {
  return analyze(expression);
}

alt text

调用this.add_var()将变量存入vars中:

add_var(node, variable, add_to_lookup = true) {
  this.vars.push(variable);
  if (add_to_lookup) {
    if (this.var_lookup.has(variable.name)) {
      const exists_var = this.var_lookup.get(variable.name);
      if (exists_var.module && exists_var.imported) {
        this.error(/** @type {any} */ (node), compiler_errors.illegal_variable_declaration);
      }
    }
    this.var_lookup.set(variable.name, variable);
  }
}

打印出来看下:

  this.walk_instance_js_pre_template();
+ console.log('svelte Component this', this);

alt text

执行完this.walk_instance_js_pre_template();,继续下一步:

this.fragment = new Fragment(this, ast.html);

Fragment内部,执行map_children方法:

export default class Fragment extends Node {
  constructor(component, info) {
    const scope = new TemplateScope();
    super(component, null, scope, info);
    this.scope = scope;
    this.children = map_children(component, this, scope, info.children);
  }
}
  this.children = map_children(component, this, scope, info.children);
+ console.log('svelte Fragment children', this.children);

alt text

把数组的每项展开看下: alt text alt text alt text alt text

map_children的作用是将ast中的节点进行转换。

执行完new Fragment(),继续下一步:

walk_instance_js_post_template() {
  const script = this.ast.instance;
  if (!script) return;
  this.post_template_walk();
  this.hoist_instance_declarations();
  this.extract_reactive_declarations();
  this.check_if_tags_content_dynamic();
}

执行walk_instance_js_post_template中的this.post_template_walk walk的content就是ast.instance.content的内容。

post_template_walk() {
  const script = this.ast.instance;
  if (!script) return;
  const component = this;
  const { content } = script;

  ...

  walk(content, {
    enter(node, parent, prop, index) {},
    leave(node) {}
  })
}

把content打印出来看下: alt text

这个walk_instance_js_post_template的主要作用是: 对节点进行一些额外的检查,例如检查是否有未关闭的标签,是否有不合法的属性等。 对节点进行一些优化,例如移除不必要的空白节点,合并连续的文本节点等。 收集一些信息,例如收集所有使用的组件,收集所有的依赖等。

const component = new Component(
  ast,
  source,
  options.name || get_name_from_filename(options.filename) || 'Component',
  options,
  stats,
  warnings
);
+  console.log('svelte component', component);

new Component()的结果打印出来看下: alt text

render_dom

往下执行render_dom,查看下render_dom的执行逻辑:

const result =
  options.generate === false
    ? null
    : options.generate === 'ssr'
    ? render_ssr(component, options)
    : render_dom(component, options);

点击render_dom进入内部,内部有个Renderer

const renderer = new Renderer(component, options);

Renderer内部:

this.block = new Block({
  renderer: this,
  name: null,
  type: 'component',
  key: null,
  bindings: new Map(),
  dependencies: new Set()
});
this.block.has_update_method = true;
this.fragment = new FragmentWrapper(
  this,
  this.block,
  component.fragment.children,
  null,
  true,
  null
);
this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));

看下Block的实现,Blockconstructor如下:

constructor(options) {
  this.parent = options.parent;
  this.renderer = options.renderer;
  this.name = options.name;
  this.type = options.type;
  this.comment = options.comment;
  this.wrappers = [];
  // for keyed each blocks
  this.key = options.key;
  this.first = null;
  this.bindings = options.bindings;
  this.chunks = {
    declarations: [],
    init: [],
    create: [],
    claim: [],
    hydrate: [],
    mount: [],
    measure: [],
    restore_measurements: [],
    fix: [],
    animate: [],
    intro: [],
    update: [],
    outro: [],
    destroy: []
  };
  this.has_animation = false;
  this.has_intro_method = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
  this.has_outro_method = false;
  this.outros = 0;
  this.get_unique_name = this.renderer.component.get_unique_name_maker();
  this.aliases = new Map();
  if (this.key) this.aliases.set('key', this.get_unique_name('key'));
}

alt text 用来表示一个代码块的类,它包含了一些关于这个代码块的信息,例如它的类型,它的依赖,它的绑定等。

了解完Block,下一步FragmentWrapper

this.fragment = new FragmentWrapper(
  this,
  this.block,
  component.fragment.children,
  null,
  true,
  null
);

Svelte内部用来表示一个模板片段的类,它包含了一些关于这个模板片段的信息,例如它的子节点,它的父节点等。

继续下一步:

this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));

就是FragmentWrapper内部的render方法:

render(block, parent_node, parent_nodes) {
  for (let i = 0; i < this.nodes.length; i += 1) {
    this.nodes[i].render(block, parent_node, parent_nodes);
  }
}

alt text

这里的nodes是调用各个类型的wrapper实例化后的对象,每个wrapper类有自己的render方法:

const wrappers = {
  AwaitBlock,
  Body,
  Comment,
  DebugTag,
  Document,
  EachBlock,
  Element,
  Head,
  IfBlock,
  InlineComponent,
  KeyBlock,
  MustacheTag,
  Options: null,
  RawMustacheTag,
  Slot,
  SlotTemplate,
  Text,
  Title,
  Window
};

简单看一些wrapper的实现,比如Text Wrapper:

render(block, parent_node, parent_nodes) {
  if (this.skip) return;
  const use_space = this.use_space();
  const string_literal = {
    type: 'Literal',
    value: this.data,
    loc: {
        start: this.renderer.locate(this.node.start),
        end: this.renderer.locate(this.node.end)
    }
  };
  block.add_element(
    this.var,
    use_space ? x`@space()` : x`@text(${string_literal})`,
    parent_nodes &&
        (use_space
          ? x`@claim_space(${parent_nodes})`
          : x`@claim_text(${parent_nodes}, ${string_literal})`),
    /** @type {import('estree').Identifier} */ (parent_node)
  );
}

这里调用了code-redx方法,把内容打印出来看下:

+ console.log('svelte text wrapper render', x`@text(${string_literal})`);
  block.add_element(
  this.var,
  use_space ? x`@space()` : x`@text(${string_literal})`,
  ...

alt text

Element的render方法:

render(block, parent_node, parent_nodes) {
  if (this.child_dynamic_element) {
    this.render_dynamic_element(block, parent_node, parent_nodes);
  } else {
    this.render_element(block, parent_node, parent_nodes);
  }
}

看下这个render_element的实现:

render_element(block, parent_node, parent_nodes) {
  const { renderer } = this;
  const hydratable = renderer.options.hydratable;
  if (this.node.name === 'noscript') return;
  const node = this.var;
  const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`);
  const children = x`@children(${this.node.name === 'template' ? x`${node}.content` : node})`;
  block.add_variable(node);
  const render_statement = this.get_render_statement(block);
  block.chunks.create.push(b`${node} = ${render_statement};`);
  const { can_use_textcontent, can_optimise_to_html_string, can_optimise_hydration } = this.node;
  ...
  if (parent_node) {
    ...
  } else {
    const insert = b`@insert(#target, ${node}, #anchor);`;
    ((insert[0]).expression).callee.loc = {
      start: this.renderer.locate(this.node.start),
      end: this.renderer.locate(this.node.end)
    };
    block.chunks.mount.push(insert);
    block.chunks.destroy.push(b`if (detaching) @detach(${node});`);
  }
  if (can_optimise_to_html_string && (!hydratable || can_optimise_hydration)) {
    if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
      /** @type {import('estree').Node} */
      let text = string_literal(
        /** @type {import('../Text.js').default} */ (this.fragment.nodes[0]).data
      );
      ...
      block.chunks.create.push(b`${node}.textContent = ${text};`);
      ...
    } else {
      ...
    }
  } else {
    this.fragment.nodes.forEach((child) => {
      child.render(block, this.node.name === 'template' ? x`${node}.content` : node, nodes, {
        element_data_name: this.element_data_name
      });
    });
  }
  
  ...
  block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
}

我们尝试把它的children打印出来看下:

const node = this.var;
  const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`); // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
  const children = x`@children(${this.node.name === 'template' ? x`${node}.content` : node})`;
  block.add_variable(node);
+ console.log('svelte render_element children', children);
  const render_statement = this.get_render_statement(block);

alt text

  const insert = b`@insert(#target, ${node}, #anchor);`;
  /** @type {import('estree').CallExpression} */ (
    /** @type {import('estree').ExpressionStatement} */ (insert[0]).expression
  ).callee.loc = {
    start: this.renderer.locate(this.node.start),
    end: this.renderer.locate(this.node.end)
  };
  block.chunks.mount.push(insert);
  block.chunks.destroy.push(b`if (detaching) @detach(${node});`);
+ console.log('svelte render_element insert', insert, node, b`if (detaching) @detach(${node});`)

alt text alt text

this.fragment.nodes.forEach((child) => {
  child.render(block, this.node.name === 'template' ? x`${node}.content` : node, nodes, {
    element_data_name: this.element_data_name
  });
});

依次调用render方法。

MustachTagrender方法:

render(block, parent_node, parent_nodes, data) {
  const contenteditable_attributes =
    this.parent instanceof ElementWrapper &&
    this.parent.attributes.filter((a) => a.node.name === 'contenteditable');
  const spread_attributes =
    this.parent instanceof ElementWrapper &&
    this.parent.attributes.filter((a) => a.node.is_spread);

  ...
  const { init } = this.rename_this_method(block, (value) => {
    if (contenteditable_attr_value) {
      ...
    } else {
      return x`@set_data(${this.var}, ${value})`;
    }
  });
  block.add_element(
    this.var,
    x`@text(${init})`,
    parent_nodes && x`@claim_text(${parent_nodes}, ${init})`,
    parent_node
  );
}
const { init } = this.rename_this_method(block, (value) => {
  if (contenteditable_attr_value) {
    if (contenteditable_attr_value === true) {
      return x`@set_data_contenteditable(${this.var}, ${value})`;
    } else {
      return x`@set_data_maybe_contenteditable(${this.var}, ${value}, ${contenteditable_attr_value})`;
    }
  } else {
+   console.log('svelte mustachetag rename_this_method', x`@set_data(${this.var}, ${value})`, this.var, value);
    return x`@set_data(${this.var}, ${value})`;
  }
});

alt text

返回到packages/svelte/src/compiler/compile/render_dom/index.js

export default function dom(component, options) {
  const { name } = component;
  const renderer = new Renderer(component, options);
+ console.log('svelte renderer', renderer);
  const { block } = renderer;
  ...

alt text 在刚才查看各个render方法时,我们注意到它一直在操作block.chunk,现在我们可以看到上面的createmountupdatedestroy上的逻辑代码已准备就绪。我们也知道了render_dom的主要作用就是把代码转化成能真正挂载到页面上的节点,同时处理了页面的各个生命周期。

render_dom中一段非常重要的解析:

walk(component.ast.instance.content, {
  enter(node) {
    ...
  },
  leave(node) {
    if (map.has(node)) {
      scope = scope.parent;
    }
    if (execution_context === node) {
      execution_context = null;
    }
    if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
      const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
      // normally (`a = 1`, `b.c = 2`), there'll be a single name
      // (a or b). In destructuring cases (`[d, e] = [e, d]`) there
      // may be more, in which case we need to tack the extra ones
      // onto the initial function call
      const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
      this.replace(invalidate(renderer, scope, node, names, execution_context === null));
    }
  }
});

大家对this.replace还有印象吗?不错,我们在《库》章节演示estree-walk时演示过这个方法,this.replace(invalidate(renderer, scope, node, names, execution_context === null))对Svelte文件中涉及到更新的代码进行替换,替换成$$invalidate相关形式的代码。

查看下invalidate的逻辑,packages/svelte/src/compiler/compile/render_dom/invalidate.js

invalidate = x`$$invalidate(${
  renderer.context_lookup.get(head.name).index
}, ${node}, ${extra_args})`;
+ console.log('svelte invalidate', invalidate, renderer.context_lookup, node);

alt text

继续回到render_dom方法

const has_create_fragment = component.compile_options.dev || block.has_content();
if (has_create_fragment) {
  body.push(b`
    function create_fragment(#ctx) {
      ${block.get_contents()}
    }
  `);
}

很明显,这段就是到时会编译到页面上的create_fragment方法。我们可以把block.get_contents打印出来看一下 alt text 笔者之前都是使用console.log这种粗暴直接的方法来进行演示,一是快速直接,二是在控制台中打印出数据的全貌便于浏览。如果读者对断点调试得心应手,也可像上图般对各个关键节点进行打断点调试。

const condition =
  !uses_rest_or_props && writable.length > 0 && renderer.dirty(writable, true);
let statement = d.node;
if (condition)
  statement = /** @type {import('estree').Statement} */ (
    b`if (${condition}) { ${statement} }`[0]
  );

此段代码是用来编译条件判断ifblock的展示。

继续往下:

body.push(b`
  function ${definition}(${args}) {
    ${injected.map((name) => b`let ${name};`)}

    ${rest}

    ${reactive_store_declarations}

    ${reactive_store_subscriptions}

    ${resubscribable_reactive_store_unsubscribers}

    ${
      component.slots.size || component.compile_options.dev || uses_slots
        ? b`let { $$slots: #slots = {}, $$scope } = $$props;`
        : null
    }
    ...

    ${instance_javascript}

    ...

    ${/* before reactive declarations */ props_inject}

    ${
      reactive_declarations.length > 0 &&
      b`
    $$self.$$.update = () => {
      ${reactive_declarations}
    };
    `
    }

    ...

    return ${return_value};
  }
`);

我们把definition和args打印出来看下 alt text 从打印结果我们能够推断出,这段代码编译后对应的是function instance(){}

const superclass = {
  type: 'Identifier',
  name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
};
...
const declaration = /** @type {import('estree').ClassDeclaration} */ (
  b`
  class ${name} extends ${superclass} {
    constructor(options) {
      super(${options.dev && 'options'});
      @init(this, options, ${definition}, ${
    has_create_fragment ? 'create_fragment' : 'null'
  }, ${not_equal}, ${prop_indexes}, ${optional_parameters});
      ${
        options.dev &&
        b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`
      }
    }
  }
`[0]
);
push_array(declaration.body.body, accessors);
body.push(/** @type {any} */ (declaration));

这段代码则对应编译后的组件的类class X extends SvelteComponent {}

const result =
  options.generate === false
    ? null
    : options.generate === 'ssr'
    ? render_ssr(component, options)
    : render_dom(component, options);
+ console.log('svelte compile result', result);

alt text

generate

终于来到编译的最后一步。

generate(result) {
  let js = null;
  let css = null;
  if (result) {

    /** @type {any} */
    const program = { type: 'Program', body: result.js };
    walk(program, {
      enter: (node, parent, key) => {
        if (node.type === 'Identifier') {
          ...
        }
      }
    });
    ...
    create_module(
      program,
      name,
      banner,
      compile_options.sveltePath,
      imported_helpers,
      referenced_globals,
      this.imports,
      this.vars
        .filter((variable) => variable.module && variable.export_name)
        .map((variable) => ({
          name: variable.name,
          as: variable.export_name
        })),
      this.exports_from
    );
    const js_sourcemap_enabled = check_enable_sourcemap(compile_options.enableSourcemap, 'js');
    if (!js_sourcemap_enabled) {
      js = print(program);
      js.map = null;
    } else {
      ...
    }
  }
  return {
    js,
    css,
    ast: this.original_ast,
    warnings: this.warnings,
    vars: this.get_vars_report(),
    stats: this.stats.render()
  };
}

create_module方法

export default function create_module(
  program,
  name,
  banner,
  svelte_path = 'svelte',
  helpers,
  globals,
  imports,
  module_exports,
  exports_from
) {
  const internal_path = `${svelte_path}/internal`;
  helpers.sort((a, b) => (a.name < b.name ? -1 : 1));
  globals.sort((a, b) => (a.name < b.name ? -1 : 1));
  return esm(
    program,
    name,
    banner,
    svelte_path,
    internal_path,
    helpers,
    globals,
    imports,
    module_exports,
    exports_from
  );
}

function esm(
  program,
  name,
  banner,
  svelte_path,
  internal_path,
  helpers,
  globals,
  imports,
  module_exports,
  exports_from
) {
  const import_declaration = {
    type: 'ImportDeclaration',
    specifiers: helpers.map((h) => ({
      type: 'ImportSpecifier',
      local: h.alias,
      imported: { type: 'Identifier', name: h.name }
    })),
    source: { type: 'Literal', value: internal_path }
  };

  ...

  program.body = b`
    /* ${banner} */

    ${import_declaration}
    ${internal_globals}
    ${imports}
    ${exports_from}

    ${program.body}

    export default ${name};
    ${exports}
  `;
}

核心就是编译生成页面最终的importexport部分的内容。

alt text
alt text

我们尝试把import部分的代码变量打印出来看下:

const import_declaration = {
  type: 'ImportDeclaration',
  specifiers: helpers.map((h) => ({
    type: 'ImportSpecifier',
    local: h.alias,
    imported: { type: 'Identifier', name: h.name }
  })),
  source: { type: 'Literal', value: internal_path }
};
+ console.log('svelte import_declaration', import_declaration, helpers);

alt text

最后经过code-red的print方法生成最终的js代码

if (!js_sourcemap_enabled) {
  js = print(program);
  js.map = null;
} 
const result =
  options.generate === false
    ? null
    : options.generate === 'ssr'
    ? render_ssr(component, options)
    : render_dom(component, options);
+ console.log('svelte final code',
+   component.generate(result),
+   component.generate(result).js.code
+ );

alt text
alt text

回看svelte-loader的代码,同样是返回js.code的代码。 https://github.com/sveltejs/svelte-loader/blob/master/index.js alt text

最后提供一个compile阶段的思维导图:

runtime

我们把在编译后得到的js代码复制进一个文件中index.js。 在svelte源码项目目录下新建一个目录叫test-runtime,新建一个index.html:

<div id="app"></div>
<script type="module" src="./index.js"></script>

alt text

修改index.js的几处地方:

  • 把引用svelte/internal的路径进行调整../packages/svelte/src/runtime/internal
  • import "svelte/internal/disclose-version";移除;
  • 注释最下面一行的export default Component,改成:
new Component({
  target: document.querySelector('#app')
})

test-runtime目录下

npm init -y
npm install vite

package.json中添加运行指令:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "vite"
},

npm run dev把项目跑起来: alt text

runtime阶段,我们主要观察数据是如何更新的。

首先是当我们点击button时

if (!mounted) {
  dispose = listen(button, "click", /*addCount*/ ctx[1]);
  mounted = true;
}

这里click绑定的监听方法是ctx[1],经过前面的学习,我们知道,ctx数组就是instance方法执行后的返回值。

function instance($$self, $$props, $$invalidate) {
  let count = 0;

  const addCount = () => {
    $$invalidate(0, count++, count);
  };

  return [count, addCount];
}

也就是我们点击按钮之后,会执行addCount方法,addCount内部的赋值语句已经被重新编译后,变成了$$invalidate()

instance(component, options.props || {}, (i, ret, ...rest) => {
  const value = rest.length ? rest[0] : ret;
+ console.log('svelte instance $$invalidate', i, ret, rest);
  if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) {
    if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
    if (ready) make_dirty(component, i);
  }
  return ret;
})

alt text i是0,ret是未更新前的值0,rest数组中是更新后的值1。 经过not_equal比较后0和1不相等,触发make_dirty。 我们把剩余的关键节点都逐个打印出来

function make_dirty(component, i) {
+ console.log('svelte make_dirty', component, i);
  if (component.$$.dirty[0] === -1) {
    dirty_components.push(component);
    schedule_update();
    component.$$.dirty.fill(0);
+   console.log('svelte component.$$.dirty before', [...component.$$.dirty]);
  }
  component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
+ console.log('svelte component.$$.dirty after', component.$$.dirty);
}
export function schedule_update() {
+ console.log('svelte schedule_update');
  if (!update_scheduled) {
    update_scheduled = true;
    resolved_promise.then(flush);
  }
}
export function flush() {
+ console.log('svelte flush', flushidx, dirty_components);
  
  ...

  do {
    try {
      while (flushidx < dirty_components.length) {
        const component = dirty_components[flushidx];
        flushidx++;
        set_current_component(component);
        update(component.$$);
      }
    } catch (e) {
      ...
    }
  }
  ...
}
function update($$) {
+ console.log('svelte update');
  if ($$.fragment !== null) {
    $$.update();
    run_all($$.before_update);
    const dirty = $$.dirty;
    $$.dirty = [-1];
    $$.fragment && $$.fragment.p($$.ctx, dirty);
    $$.after_update.forEach(add_render_callback);
  }
}
p(ctx, [dirty]) {
+ console.log('svelte runtime p', ctx, dirty);
  if (dirty & /*count*/ 1) set_data(t2, /*count*/ ctx[0]);
},

当我们刷新页面时,执行了flushalt text

当我们点击button进行更新: alt text

首先执行$$invalidate,然后执行make_dirty,然后执行schedule_update。 我们看到schedule_update内部其实就是把flush放到微任务中执行,所以我们会先看到component.$$.dirty在更新前的值是[0],然后执行位运算之后变成[1]。之后执行微任务中的flushflush内部调用update,而update方法则是调用组件内的p方法。p方法中通过dirty & 1的判断1 & 1,从而执行set_data

export function set_data(text, data) {
  data = '' + data;
  if (text.data === data) return;
  text.data = /** @type {string} */ (data);
}

set_data方法的主要逻辑则是更新text节点的data属性的值。

最后也提供一个简易的流程图:

小结

本章我们学习了:

  • compile的执行流程
  • runtime的执行流程

compile和runtime的难易程度,基本符合二八定律,百分之八十的内容都在编译器如何解析Svelte文件上。