Garlic Garlic

Svelte从入门到精通——compiler

发表于 阅读时长27分钟

经过了前两章的铺垫,我们正式开始对源码进行解读。到笔者目前写文章时,Svelte的最新版本是4.2.12。

我们要想在webpackvite中使用Svelte,必须安装svelte-loadervite-plugin-svelte,它们的重要性不言而喻。

看下svelte-loader的核心逻辑:

const svelte = require('svelte/compiler');

svelte.preprocess(source, options.preprocess).then(processed => {
  const compiled = svelte.compile(processed.toString(), compileOptions);
}

再看下vite-plugin-svelte的核心逻辑:

import * as svelte from 'svelte/compiler';

let preprocessed;
preprocessed = await svelte.preprocess(code, preprocessors, { filename });
const finalCode = preprocessed ? preprocessed.code : code;

let compiled;
compiled = svelte.compile(finalCode, finalCompileOptions);

从源码可以看到,Svelte官方的webpack插件svelte-loaderRollup插件rollup-plugin-svelte的主入口都是svelte.compile,这也是我们的切入点。

把项目下载下来,切换到4.2.12分支。

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

preprocess

首先是预处理。源码路径:packages/svelte/src/compiler/preprocess/index.js

export default async function preprocess(source, preprocessor, options) {
  const filename = (options && options.filename) || /** @type {any} */ (preprocessor).filename; // legacy
  const preprocessors = preprocessor
    ? Array.isArray(preprocessor)
      ? preprocessor
      : [preprocessor]
    : [];
  const result = new PreprocessResult(source, filename);
  for (const preprocessor of preprocessors) {
    if (preprocessor.markup) {
      result.update_source(await process_markup(preprocessor.markup, result));
    }
    if (preprocessor.script) {
      result.update_source(await process_tag('script', preprocessor.script, result));
    }
    if (preprocessor.style) {
      result.update_source(await process_tag('style', preprocessor.style, result));
    }
  }

  return result.to_processed();
}

简单理解:

  • 首先是从外部接收要编译的文件source和预处理器对象preprocessor
  • 判断preprocessor是不是数组,因为可能传了多个preprocessor,遍历执行预处理器的功能
  • 分别处理markupscriptstyle,即对应着html标记、js脚本和css样式的编译处理。

总的来说,这个文件提供了预处理Svelte组件源代码的功能,它允许你在编译前对源代码进行任意的转换。

compile

进入到packages/svelte/src/compiler/compile/index.js

export default function compile(source, options = {}) {
  const ast = parse(source, options);
  const component = new Component(
    ast,
    source,
    options.name || get_name_from_filename(options.filename) || 'Component',
    options,
  );
  const result = render_dom(component, options);
  return component.generate(result);
}

我们去掉不相关的代码,可以看到基本的compile逻辑:

  • 首先会将parse过程中拿到的语法树ast转换为component
  • 然后在render_dom中调整成特定的代码片段
  • 最后component.generate中,通过code-redprint将调整后的代码片段拼成完整的代码

用以下代码来进行编译为例:

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

<button on:click={addCount}>add</button>
count:{count}

我们分别把astcomponentresult这几个变量打印出来看下: alt text

alt text

alt text

parse

首先我们查看下compile方法:

const ast = parse(source, options);

compile第一步的逻辑便是调用parse来解析文件内容。

跳转到parse方法的主入口packages/svelte/src/compiler/parse/index.js

export default function parse(template, options = {}) {
  const parser = new Parser(template, options);
  
  const instance_scripts = parser.js.filter((script) => script.context === 'default');
  const module_scripts = parser.js.filter((script) => script.context === 'module');

  return {
    html: parser.html,
    css: parser.css[0],
    instance: instance_scripts[0],
    module: module_scripts[0]
  };
}

逻辑都封装在Parser类中,经过Parser处理后,返回带有htmlcssinstancemodule属性值的对象。htmlcss容易理解,而instance存储的是正常的script内容,module存储的则是<script context="module"></script>内的js内容。

我们在REPL中可以看到,AST的输出结构正是上述的返回对象。

Parser

export class Parser {
  index = 0;
  stack = [];
  html = undefined;
  css = [];
  js = [];

  constructor(template, options) {
    let state = fragment;
    while (this.index < this.template.length) {
      state = state(this) || fragment;
    }
  }
  current() {}
  eat(str, required, error) {}
  match(str) {}
  match_regex(pattern) {}
  allow_whitespace() {}
  read(pattern) {}
  read_identifier(allow_reserved = false) {}
  read_until(pattern, error_message) {}
  require_whitespace() {}
}

Parse类中,定义了一些如何解析模板字符串的方法,比如match用来判断是否匹配对应字符串、eat用来“吃掉”当前字符串,用于确保读取字符串模板的索引index的正确指向等等。

我们着重关注的是在constructor里的一段代码:

let state = fragment;
while (this.index < this.template.length) {
  state = state(this) || fragment;
}

fragment

进入到fragment的文件中packages/svelte/src/compiler/parse/state/fragment.js

import tag from './tag.js';
import mustache from './mustache.js';
import text from './text.js';

/**
 * @param {import('../index.js').Parser} parser
 */
export default function fragment(parser) {
  if (parser.match('<')) {
    return tag;
  }

  if (parser.match('{')) {
    return mustache;
  }

  return text;
}

可以看到,fragment内部是使用了三种不同的解析处理,分别是tagmustachetext

tag

当解析的内容是以<开头时,进入到tag的解析流程。tag除了解析原生html、css、script标签外,还支持了Svelte自定义的标签如svelte:htmlsvelte:bodysvelte:component等等。

if (parser.eat('!--')) {
  const data = parser.read_until(regex_closing_comment);
  parser.eat('-->', true, parser_errors.unclosed_comment);
  parser.current().children.push({
    start,
    end: parser.index,
    type: 'Comment',
    data,
    ignores: extract_svelte_ignore(data)
  });
  return;
}

当在<之后遇到的是!--,表明解析到了注释语句,返回Comment类型的节点。

const meta_tags = new Map([
  ['svelte:head', 'Head'],
  ['svelte:options', 'Options'],
  ['svelte:window', 'Window'],
  ['svelte:document', 'Document'],
  ['svelte:body', 'Body']
]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat(
  'svelte:self',
  'svelte:component',
  'svelte:fragment',
  'svelte:element'
);

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';

遇到Svelte的自定义标签时,返回特定类型的节点,剩余的原生html标签,则标记为Element类型节点。

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

if (is_top_level_script_or_style) {
  const special = specials.get(name);
  parser.eat('>', true);
  const content = special.read(parser, start, element.attributes);
  if (content) parser[special.property].push(content);
  return;
}

当遇到script标签时,用read_script进行解析;当遇到style标签时,用read_style进行解析。

read_script的逻辑如下,核心是调用code-redparse方法对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的逻辑:

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

read_style的逻辑如下,核心是使用了css-treefork功能:

import { parse } from './css-tree-cq/css_tree_parse.js'; // Use extended css-tree for 
import { walk } from 'estree-walker';

export default function read_style(parser, start, attributes) {
  ...

  let ast;

  try {
    ast = parse(styles, {
      positions: true,
      offset: content_start,
      onParseError(error) {
        throw error;
      }
    });
  } catch (err) {
    ...
  }

  ast = JSON.parse(JSON.stringify(ast));

  walk(ast, {
    enter: (node) => {
      ...

    }
  });

  parser.read(regex_starts_with_closing_style_tag);

  const end = parser.index;

  return {
    type: 'Style',
    start,
    end,
    attributes,
    children: ast.children,
    content: {
      start: content_start,
      end: content_end,
      styles
    }
  };
}

cssparse的逻辑:

import { fork } from 'css-tree';
import * as node from './node/index.js';

const cq_syntax = fork({
  atrule: {
    container: {
      parse: {
        prelude() {
          return this.createSingleNodeList(this.ContainerQuery());
        },
        block(is_style_block = false) {
          return this.Block(is_style_block);
        }
      }
    }
  },
  node
});

export const parse = cq_syntax.parse;

mustache

当解析的内容是以{开头时,进入到mustache的解析流程。除了识别正常的{xxx}语法外,还识别Svelte的逻辑渲染语法如{#if}{#each}{@html}等等。

export default function mustache(parser) {
  ...
  // {/if}, {/each}, {/await} or {/key}
  if (parser.eat('/')) {
    
    ...
  } else if (parser.eat(':else')) {
    ...
    // :else if
    if (parser.eat('if')) {
      ...
      block.else = {
        start: parser.index,
        end: null,
        type: 'ElseBlock',
        children: [
          {
            start: parser.index,
            end: null,
            type: 'IfBlock',
            elseif: true,
            expression,
            children: []
          }
        ]
      };
      parser.stack.push(block.else.children[0]);
    } else {
      // :else
      ...
      block.else = {
        start: parser.index,
        end: null,
        type: 'ElseBlock',
        children: []
      };
      parser.stack.push(block.else);
    }
  } else if (parser.match(':then') || parser.match(':catch')) {
    const block = parser.current();
    const is_then = parser.eat(':then') || !parser.eat(':catch');
    ...
    
    const new_block = {
      start,
      end: null,
      type: is_then ? 'ThenBlock' : 'CatchBlock',
      children: [],
      skip: false
    };
    await_block[is_then ? 'then' : 'catch'] = new_block;
    parser.stack.push(new_block);
  } else if (parser.eat('#')) {
    // {#if foo}, {#each foo} or {#await foo}
    let type;
    if (parser.eat('if')) {
      type = 'IfBlock';
    } else if (parser.eat('each')) {
      type = 'EachBlock';
    } else if (parser.eat('await')) {
      type = 'AwaitBlock';
    } else if (parser.eat('key')) {
      type = 'KeyBlock';
    } else {
      parser.error(parser_errors.expected_block_type);
    }
    parser.require_whitespace();
    const expression = read_expression(parser);
    const block =
      type === 'AwaitBlock'
        ? {
            start,
            end: null,
            type,
            expression,
            value: null,
            error: null,
            pending: {
              start: null,
              end: null,
              type: 'PendingBlock',
              children: [],
              skip: true
            },
            then: {
              start: null,
              end: null,
              type: 'ThenBlock',
              children: [],
              skip: true
            },
            catch: {
              start: null,
              end: null,
              type: 'CatchBlock',
              children: [],
              skip: true
            }
          }
        : {
            start,
            end: null,
            type,
            expression,
            children: []
          };
    parser.allow_whitespace();
    // {#each} blocks must declare a context – {#each list as item}
    if (type === 'EachBlock') {
      ...
    }
    
    ...
  } else if (parser.eat('@html')) {
    // {@html content} tag
    parser.require_whitespace();
    const expression = read_expression(parser);
    parser.allow_whitespace();
    parser.eat('}', true);
    parser.current().children.push({
      start,
      end: parser.index,
      type: 'RawMustacheTag',
      expression
    });
  } else if (parser.eat('@debug')) {
    let identifiers;
    // Implies {@debug} which indicates "debug all"
    if (parser.read(regex_whitespace_with_closing_curly_brace)) {
      identifiers = [];
    } else {
      const expression = read_expression(parser);
      ...
    }
    parser.current().children.push({
      start,
      end: parser.index,
      type: 'DebugTag',
      identifiers
    });
  } else if (parser.eat('@const')) {
    // {@const a = b}
    parser.require_whitespace();
    const expression = read_expression(parser);
    ...
    parser.current().children.push({
      start,
      end: parser.index,
      type: 'ConstTag',
      expression
    });
  } else {
    const expression = read_expression(parser);
    parser.allow_whitespace();
    parser.eat('}', true);
    parser.current().children.push({
      start,
      end: parser.index,
      type: 'MustacheTag',
      expression
    });
  }
}

笔者已经把大部分细节代码删除,从上述代码中,我们大体能够知道,mustache方法能够解析{}{@html}{@debug}{@const}{#if}{#each}{#await}{#key}{:else}{:else if}{:then}{:catch}{/if}{/each}{/await}{/key}等。

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);
}

const ast = parse(source, options);的流程解析到此,回到compile

alt text

Component

经过parse的处理,我们拿到了ast对象,然后我们往Component中传入字符串内容和ast对象:

const component = new Component(
  ast,
  source,
  options.name || get_name_from_filename(options.filename) || 'Component',
  options,
);
export default class Component {
  constructor(ast, source, name, compile_options, stats, warnings) {
    this.ast = ast;
    this.source = source;

    // styles
    this.stylesheet = new Stylesheet({
      source,
      ast,
      filename: compile_options.filename,
      component_name: name,
      dev: compile_options.dev,
      get_css_hash: compile_options.cssHash
    });

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

    this.walk_instance_js_post_template();
  }
  generate(result) {}
  walk_module_js() {}
  walk_instance_js_pre_template() {}
  walk_instance_js_post_template() {}
}

walk_module_js

walk_module_js() {
  const component = this;
  const script = this.ast.module;
  if (!script) return;
  walk(script.content, {
    /** @param {import('estree').Node} node */
    enter(node) {
      ...
    }
  });
  const { scope, globals } = create_scopes(script.content);
  this.module_scope = scope;
  scope.declarations.forEach((node, name) => {
    if (name[0] === '$') {
      return this.error(/** @type {any} */ (node), compiler_errors.illegal_declaration);
    }
    const writable =
      node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
    const imported = node.type.startsWith('Import');
    this.add_var(node, {
      name,
      module: true,
      hoistable: true,
      writable,
      imported
    });
  });
}

这个方法主要对ast.module的内容进行解析,即对<script context="module"></script>中的内容进行解析,比如判断里面是否声明了$相关的响应式语句,对importexport语句的处理等。

walk_instance_js_pre_template

walk_instance_js_pre_template() {
  const script = this.ast.instance;
  if (!script) return;
  // inject vars for reactive declarations
  script.content.body.forEach((node) => {
    ...
    extract_names(expression.left).forEach((name) => {
      if (!this.var_lookup.has(name) && name[0] !== '$') {
        this.injected_reactive_declaration_vars.add(name);
      }
    });
  });
  const { scope: instance_scope, map, globals } = create_scopes(script.content);
  this.instance_scope = instance_scope;
  this.instance_scope_map = map;
  instance_scope.declarations.forEach((node, name) => {
    ...
    const { type } = node;
    this.add_var(node, {
      name,
      initialised: instance_scope.initialised_declarations.has(name),
      imported: type.startsWith('Import'),
      writable: type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let')
    });
    this.node_for_declaration.set(name, node);
  });
  // NOTE: add store variable first, then only $store value
  // as `$store` will mark `store` variable as referenced and subscribable
  const global_keys = Array.from(globals.keys());
  const sorted_globals = [
    ...global_keys.filter((key) => key[0] !== '$'),
    ...global_keys.filter((key) => key[0] === '$')
  ];
  ...
  this.track_references_and_mutations();
}

walk_instance_js_pre_template()方法在处理html模板之前解析ast.instance<script></script>标签中的内容,主要处理功能包括处理变量声明、提取响应式声明的变量,解析作用域等。

Fragment

源码路径:packages/svelte/src/compiler/compile/nodes/Fragment.js

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

map_children源码路径:packages/svelte/src/compiler/compile/nodes/shared/map_children.js

export default function map_children(component, parent, scope, children) {
  let last = null;
  let ignores = [];
  return children.map((child) => {
    const constructor = get_constructor(child.type);
    ...
    const node = new constructor(component, parent, scope, child);
    ...
    return node;
  });
}

get_constructor经过new得到一个node对象:

function get_constructor(type) {
  switch (type) {
    case 'AwaitBlock':
      return AwaitBlock;
    case 'Body':
      return Body;
    case 'Comment':
      return Comment;
    case 'ConstTag':
      return ConstTag;
    case 'Document':
      return Document;
    case 'EachBlock':
      return EachBlock;
    case 'Element':
      return Element;
    case 'Head':
      return Head;
    case 'IfBlock':
      return IfBlock;
    ...
    default:
      throw new Error(`Not implemented: ${type}`);
  }
}

我们把this.fragment打印出来看下: alt text
children属性中,就是经过各种各种node类型实例化后的对象。

我们拿数组中一个子元素来看: alt text

里面的component属性其实就是在this.fragment = new Fragment(this, ast.html);时传递的this

walk_instance_js_post_template

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_pre_template是在html模板处理前对ast.instance实例进行解析,而walk_instance_js_post_template是在处理模板之后解析ast.instance。因为处理了html模板之后,模板文件中存在和变量相关的内容,比如在{}中渲染变量,在事件绑定中绑定变量等。

我们看下const component = new Component();得到的数据: alt text

render_dom

解析完new Component(),我们继续执行下一步:

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

关注render_dom部分,源码路径:packages/svelte/src/compiler/compile/render_dom/index.js

export default function dom(component, options) {
  const { name } = component;
  const renderer = new Renderer(component, options);
  const { block } = renderer;
  block.has_outro_method = true;
  ...

  const blocks = renderer.blocks.slice().reverse();
  push_array(
    body,
    blocks.map((block) => {
      if (/** @type {import('./Block.js').default} */ (block).render)
        return /** @type {import('./Block.js').default} */ (block).render();
      return block;
    })
  );

  ...

  const rest = uses_rest
    ? b`
    const ${omit_props_names.name} = [${props.map((prop) => `"${prop.export_name}"`).join(',')}];
    let $$restProps = ${compute_rest};
  `
    : null;
  ...
  
  // instrument assignments
  if (component.ast.instance) {
    let scope = component.instance_scope;
    const map = component.instance_scope_map;

    /** @type {import('estree').Node | null} */
    let execution_context = null;
    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;
          const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
          this.replace(invalidate(renderer, scope, node, names, execution_context === null));
        }
      }
    });
    ...
  }

  ...

  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()}
      }
    `);
  }

  const instance_javascript = component.extract_javascript(component.ast.instance);
  ...
  const definition = has_definition
    ? component.alias('instance')
    : { type: 'Literal', value: null };

    if (has_create_fragment) {
    console.log('svelte block', block)
    body.push(b`
      function create_fragment(#ctx) {
        ${block.get_contents()}
      }
    `);
  }
  ...
  const instance_javascript = component.extract_javascript(component.ast.instance);
  const has_definition =
    component.compile_options.dev ||
    (instance_javascript && instance_javascript.length > 0) ||
    filtered_props.length > 0 ||
    uses_props ||
    component.partly_hoisted.length > 0 ||
    renderer.initial_context.length > 0 ||
    component.reactive_declarations.length > 0 ||
    capture_state ||
    inject_state;
  const definition = has_definition
    ? component.alias('instance')
    : { type: 'Literal', value: null };
  ...
  if (has_definition) {
    ...
    
    body.push(b`
      function ${definition}(${args}) {
        ...
      }
    `);
  }

  ...

  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));

  ...

  return { js: flatten(body), css };
}

Renderer实例中包含了一系列用于生成各种DOM操作的代码,这些代码最终会通过component.generate包含在一个js文件中。

这里笔者删除了大量细节逻辑,我们关注其中几个部分即可:

this.replace(invalidate(renderer, scope, node, names, execution_context === null));

我们从REPL上看一下一个代码例子的编译后结果: alt text 很明显,this.replace(invalidate)的作用就是将我们的赋值语句进行$$invalidate的替换。

if (has_create_fragment) {
  body.push(b`
    function create_fragment(#ctx) {
      ${block.get_contents()}
    }
  `);
}

这部分则是处理以下片段,block.get_contents()我们在后面讲Block时进行说明: alt text

const definition = has_definition
  ? component.alias('instance')
  : { type: 'Literal', value: null };
...
if (has_definition) {
  ...
  
  body.push(b`
    function ${definition}(${args}) {
      ...
    }
  `);
}

这一部分则对应了instance片段: alt text 我们可以在源码中把definition这个变量打印出来看下: alt text

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]
);

这部分很明显则对应了以下片段: alt text

Renderer

import Block from './Block.js';
import FragmentWrapper from './wrappers/Fragment.js';
import { x } from 'code-red';
import flatten_reference from '../utils/flatten_reference.js';
import { reserved_keywords } from '../utils/reserved_keywords.js';
import { renderer_invalidate } from './invalidate.js';

export default class Renderer {

  ...

  constructor(component, options) {
    this.component = component;
    ...
    // main block
    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.blocks.forEach((block) => {
      if (block instanceof Block) {
        block.assign_variable_names();
      }
    });
    this.block.assign_variable_names();
    this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
    
    ...
  }

  ...

  invalidate(name, value, main_execution_context = false) {
    return renderer_invalidate(this, name, value, main_execution_context);
  }

  dirty(names, is_reactive_declaration = false) {
    const renderer = this;
    const dirty = /** @type {| import('estree').Identifier
                    | import('estree').MemberExpression} */ (
      is_reactive_declaration ? x`$$self.$$.dirty` : x`#dirty`
    );
    const get_bitmask = () => {
      /** @type {BitMasks} */
      const bitmask = [];
      names.forEach((name) => {
        const member = renderer.context_lookup.get(name);
        if (!member) return;
        if (member.index.value === -1) {
          throw new Error('unset index');
        }
        const value = /** @type {number} */ (member.index.value);
        const i = (value / 31) | 0;
        const n = 1 << value % 31;
        if (!bitmask[i]) bitmask[i] = { n: 0, names: [] };
        bitmask[i].n |= n;
        bitmask[i].names.push(name);
      });
      return bitmask;
    };
    return ({
      type: 'ParenthesizedExpression',
      get expression() {
        const bitmask = get_bitmask();
        if (!bitmask.length) {
          return /** @type {import('estree').BinaryExpression} */ (
            x`${dirty} & /*${names.join(', ')}*/ 0`
          );
        }
        if (renderer.context_overflow) {
          return bitmask
            .map((b, i) => ({ b, i }))
            .filter(({ b }) => b)
            .map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`)
            .reduce((lhs, rhs) => x`${lhs} | ${rhs}`);
        }
        return /** @type {import('estree').BinaryExpression} */ (
          x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}`
        );
      }
    });
  }

  ...

  remove_block(block) {
    this.blocks.splice(this.blocks.indexOf(block), 1);
  }
}

  • Block
import { b, x } from 'code-red';
import { is_head } from './wrappers/shared/is_head.js';
import { regex_double_quotes } from '../../utils/patterns.js';
import { flatten } from '../../utils/flatten.js';

export default class Block {
  ...

  /** @param {BlockOptions} options */
  constructor(options) {
    ...
    this.chunks = {
      declarations: [],
      init: [],
      create: [],
      claim: [],
      hydrate: [],
      mount: [],
      measure: [],
      restore_measurements: [],
      fix: [],
      animate: [],
      intro: [],
      update: [],
      outro: [],
      destroy: []
    };
    ...
  }

  assign_variable_names() {}

  ...

  add_element(id, render_statement, claim_statement, parent_node, no_detach) {
    this.add_variable(id);
    this.chunks.create.push(b`${id} = ${render_statement};`);
    if (this.renderer.options.hydratable) {
      this.chunks.claim.push(b`${id} = ${claim_statement || render_statement};`);
    }
    if (parent_node) {
      this.chunks.mount.push(b`@append(${parent_node}, ${id});`);
      if (is_head(parent_node) && !no_detach) this.chunks.destroy.push(b`@detach(${id});`);
    } else {
      this.chunks.mount.push(b`@insert(#target, ${id}, #anchor);`);
      if (!no_detach) this.chunks.destroy.push(b`if (detaching) @detach(${id});`);
    }
  }

  ...

  /** @param {any} [key] */
  get_contents(key) {
    ...
    /** @type {Record<string, any>} */
    const properties = {};
    const noop = x`@noop`;
    properties.key = key;
    if (this.first) {
      properties.first = x`null`;
      this.chunks.hydrate.push(b`this.first = ${this.first};`);
    }
    if (this.chunks.create.length === 0 && this.chunks.hydrate.length === 0) {
      properties.create = noop;
    } else {
      const hydrate =
        this.chunks.hydrate.length > 0 &&
        (this.renderer.options.hydratable ? b`this.h();` : this.chunks.hydrate);
      properties.create = x`function #create() {
        ${this.chunks.create}
        ${hydrate}
      }`;
    }
    ...
    if (this.chunks.mount.length === 0) {
      properties.mount = noop;
    } else if (this.event_listeners.length === 0) {
      properties.mount = x`function #mount(#target, #anchor) {
        ${this.chunks.mount}
      }`;
    } else {
      properties.mount = x`function #mount(#target, #anchor) {
        ${this.chunks.mount}
      }`;
    }
    ...
    if (this.has_animation) {
      ...
    }
    if (this.has_intro_method || this.has_outro_method) {
      ...
    }
    if (this.chunks.destroy.length === 0) {
      properties.destroy = noop;
    } else {
      const dispose_elements = [];
      // Coalesce if blocks with the same condition
      const others = flatten(this.chunks.destroy).filter(
        /** @param {import('estree').Node} node */
        (node) => {
          if (
            node.type === 'IfStatement' &&
            node.test.type === 'Identifier' &&
            node.test.name === 'detaching'
          ) {
            dispose_elements.push(node.consequent);
            return false;
          } else {
            return true;
          }
        }
      );

      properties.destroy = x`function #destroy(detaching) {
        ${dispose_elements.length ? b`if (detaching) { ${dispose_elements} }` : null}
        ${others}
      }`;
    }
    ...

    /** @type {any} */
    const return_value = x`{
      key: ${properties.key},
      first: ${properties.first},
      c: ${properties.create},
      l: ${properties.claim},
      h: ${properties.hydrate},
      m: ${properties.mount},
      p: ${properties.update},
      r: ${properties.measure},
      s: ${properties.restore_measurements},
      f: ${properties.fix},
      a: ${properties.animate},
      i: ${properties.intro},
      o: ${properties.outro},
      d: ${properties.destroy}
    }`;
    const block = dev && this.get_unique_name('block');
    const body = b`
      ${this.chunks.declarations}

      ${Array.from(this.variables.values()).map(({ id, init }) => {
        return init ? b`let ${id} = ${init}` : b`let ${id}`;
      })}

      ${this.chunks.init}

      ${
        dev
          ? b`
          const ${block} = ${return_value};
          @dispatch_dev("SvelteRegisterBlock", {
            block: ${block},
            id: ${this.name || 'create_fragment'}.name,
            type: "${this.type}",
            source: "${this.comment ? this.comment.replace(regex_double_quotes, '\\"') : ''}",
            ctx: #ctx
          });
          return ${block};`
          : b`
          return ${return_value};`
      }
    `;
    return body;
  }

  /** @returns {boolean} */
  has_content() {}

  render() {
    const key = this.key && this.get_unique_name('key');

    /** @type {any[]} */
    const args = [x`#ctx`];
    if (key) args.unshift(key);
    const fn = b`function ${this.name}(${args}) {
      ${this.get_contents(key)}
    }`;
    return this.comment
      ? b`
        // ${this.comment}
        ${fn}`
      : fn;
  }

  render_listeners(chunk = '') {}
  render_binding_groups() {}
}

从代码中我们可以知道,Block主要是为代码块添加生命周期,比如我们常见的ccreate、mmount、pupdate、ddestroy等。

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

...

export default class FragmentWrapper {
  nodes;
  constructor(renderer, block, nodes, parent, strip_whitespace, next_sibling) {
    this.nodes = [];
    ...
    while (i--) {
      const child = nodes[i];
      ...
      if (child.type === 'Window') {
        window_wrapper = new Window(renderer, block, parent, child);
        continue;
      }
      if (child.type === 'Text') {
        ...
      } else {
        const Wrapper = wrappers[child.type];
        if (!Wrapper || (child.type === 'Comment' && !renderer.options.preserveComments)) continue;
        const wrapper = new Wrapper(
          renderer,
          block,
          parent,
          child,
          strip_whitespace,
          last_child || next_sibling
        );
        this.nodes.unshift(wrapper);
        link(last_child, (last_child = wrapper));
      }
    }
    ...
  }

  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);
    }
  }
}

FragmentWrapper通过child.type来调用对应类型的Wrapper,每个Wrapper内部都实现了自己的render方法。

通过this.fragment = new FragmentWrapper()得到的fragment和原来在new Component()中生成的fragment并不相同,注意区分,我们可以把两者打印出来比较一下:

packages/svelte/src/compiler/compile/Component.js中:

  this.fragment = new Fragment(this, ast.html);
+  console.log('svelte new Fragment in Component', this.fragment);

和在packages/svelte/src/compiler/compile/render_dom/Renderer.js中:

  this.fragment = new FragmentWrapper(
    this,
    this.block,
    component.fragment.children,
    null,
    true,
    null
  );
+ console.log('svelte new FragmentWrapper in Renderer', this.fragment);

alt text

  • render 前面说到每个Wrapper都会实现自己的render方法,之后便是调用各自的render
this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));

比如Text的Wrapper:

import Wrapper from './shared/Wrapper.js';
import { x } from 'code-red';

export default class TextWrapper extends Wrapper {
  _data;
  skip;
  var;

  constructor(renderer, block, parent, node, data) {
    super(renderer, block, parent, node);
    this.skip = this.node.should_skip();
    this._data = data;
    this.var = /** @type {unknown} */ /** @type {import('estree').Identifier} */ (
      this.skip ? null : x`t`
    );
  }
  use_space() {
    return this.node.use_space();
  }
  set data(value) {
    this.node.data = this._data = value;
  }
  get data() {
    return this._data;
  }

  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)
    );
  }
}

Wrapper内部的render是对已经添加了生命周期处理的block对象进行二次处理。

generate

来到最后一步,首先看下result的大致结构: alt text

genetate隶属于Component中的一个方法。

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

    const program = { type: 'Program', body: result.js };
    walk(program, {
      enter: (node, parent, key) => {
        if (node.type === 'Identifier') {
          if (node.name[0] === '@') {
            ...
          } else if (node.name[0] !== '#' && !is_valid(node.name)) {
            const literal = { type: 'Literal', value: node.name };
            if (parent.type === 'Property' && key === 'key') {
              parent.key = literal;
            } else if (parent.type === 'MemberExpression' && key === 'property') {
              parent.property = literal;
              parent.computed = true;
            }
          }
        }
      }
    });
    ...
    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
    );
    css = compile_options.customElement ? { code: null, map: null } : result.css;
    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()
  };
}

整体逻辑就是把program变量进行create_module的模块化输出准备,然后调用print把完整的js文件内容输出。

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
  );
}

create_module内部调用了esm方法。

function esm(
  program,
  name,
  banner,
  svelte_path,
  internal_path,
  helpers,
  globals,
  imports,
  module_exports,
  exports_from
) {
  
  ...

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

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

    ${program.body}

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

我们可以把program.body打印出来看下: alt text

可以看到,到这一步,所有的代码片段已经整理好,之后调用code-redprint方法对编译好的节点进行整合输出。

小结

本章我们学习了:

  • parse如何把字符串模板内容解析成ast抽象语法树
  • new Component从ast中解析出html、css、js代码片段
  • render_dom()将分析好的html、css、js代码片段进行组装,切分成一个个代码块
  • generate将切分好的代码块进行整合输出