compile-svelte-in-your-head-1

脑海中来编译一个svelte(1)

https://svelte.dev/repl/99aeea705b1e48fe8610b3ccee948280?version=3.23.2 这里可以看到一个最简单的svelte组件的输出结果(JS output), 如下:

/* App.svelte generated by Svelte v3.23.2 */
import {
    SvelteComponent,
    detach,
    element,
    init,
    insert,
    noop,
    safe_not_equal
} from "svelte/internal";

function create_fragment(ctx) {
    let h1;

    return {
        c() {
            h1 = element("h1");
            h1.textContent = "Hello World";
        },
        m(target, anchor) {
            insert(target, h1, anchor);
        },
        d(detaching) {
            if (detaching) detach(h1);
        }
    };
}

class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, null, create_fragment, safe_not_equal, {});
    }
}

export default App;

create_fragment

create_fragment就是给svelte的组件去构建DOM片段。

他的返回的对象里的方法:

c()

create 的短写

包含创建所有元素片段的指令。

这个例子里,就是创建h1元素

h1 = element('h1');
h1.textContent = 'Hello World';

// https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L21
export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
    return document.createElement<K>(name);
}

m(target, anctor)

mount 短写

就是挂载到对应的target上。

这个例子里就是mount h1到target上。

insert(target, h1, anchor);

// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.ts
export function insert(target, node, anchor) {
  target.insertBefore(node, anchor || null);
}

d(detaching)

destroy 短写

从target中删除某个元素。

这里的例子就是从DOM中移除h1。

detach(h1);

// https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L11
export function detach(node: Node) {
    node.parentNode.removeChild(node);
}

export default class App extends SvelteComponent

svelte api

初始化的时候就使用类似create_fragment这些信息来组成。Svelte只会传递需要它的信息,并在不需要的时候删除它们。

试试空的组件,会输出什么信息。

https://svelte.dev/repl/1f29ce52adf446fc9116bb957b7200ec?version=3.19.1

打开上面这个链接看看jsoutput:

/* App.svelte generated by Svelte v3.19.1 */
import { SvelteComponent, init, safe_not_equal } from "svelte/internal";

class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, null, null, safe_not_equal, {});
    // 下面是 hello world 版本
    // init(this, options, null, create_fragment, safe_not_equal, {});
    }
}

export default App;

create_fragment被改为null去传递了。

Svelte在init函数中设置了大多数内部的内容:

  • component的props, ctx(后面会解释ctx)和上下文

  • component生命周期事件

  • component更新机制

最后通过create_fragment去创建和挂载元素到DOM上。

并且所有的内部状态(state)和方法都被附加到this.$$上面。

所以你获取组件的$$属性,那么就在访问组件的内部了。

Adding data

目前,大致了解了基本的组件行为,现在看下如何添加数据之后编译的输出如何改变的。

<script>
    let name = 'World';
</script>
<h1>Hello {name}</h1>

Svelte REPL

这个在编译后的输出里有了改变:

function create_fragment(ctx) {
  // ...
  return {
    c() {
      h1 = element('h1');
      h1.textContent = `Hello ${name}`;    },
    // ...
  };
}
let name = 'World';
class App extends SvelteComponent {
  // ...
}

可以发现内部的内容被移动到了代码的顶层,并且h1元素的文本内容是一个模板字面量。

这有很多的东西发生在幕后。

Updating data

添加一个函数去更新name变量。

<script>
    let name = 'World';
    function update() {
        name = 'Svelte';
    }
</script>
<h1>Hello {name}</h1>

Svelte REPL

然后发现编译后改变的地方:

function create_fragment(ctx) {
  return {
    c() {
+      h1 = element('h1');
+         t0 = text('Hello ');
+           t1 = text(/*name*/ ctx[0]);
    },
    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, t0);
      append(h1, t1);
    },
+   p(ctx, [dirty]) {
+     if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
+   },
    d(detaching) {
      if (detaching) detach(h1);
    },
  };
}

+ function instance($$self, $$props, $$invalidate) {
+  let name = 'World';
+  function update() {
+    $$invalidate(0, (name = 'Svelte'));
+  }
+  return [name];
+ }

export default class App extends SvelteComponent {
  constructor(options) {
    super();
+   init(this, options, instance, create_fragment, safe_not_equal, {});
  }
}

一些新发现:

  • h1元素的文本内容被分成了两个文本节点,通过text()函数创建。 text()地址:https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L48

  • create_fragment返回的对象新增了一个p(ctx, dirty)方法。

    update 短写

  • 创建了一个新的instance函数。

  • <script>中的内容被移到了instance函数中。

  • 你是否发现create_fragment中使用的变量name被替换成了ctx[0],并且也不是模板字符串了。

那么,为什么要改变?

Svelte编译器跟踪所有在<script>标签中声明的变量,他跟踪变量是否存在以下情况:

  • 可否进行改变(mutated)?,类似 count++

  • 是否可以重新赋值?类似name = 'Svelte'

  • 是否在模板中被引用?类似 Hello {name}

  • 是否可写?类似 const i = 1;let i = 1;

  • 。。。以及更多

当Svelte编译器意识到可以重新分配变量name时,(由于name = 'Svelte';update函数中),它将h1的文本内容分解为若干部分,以便动态更新部分文本。

实际上,可以看到有一个新方法p来更新文本节点。

p(ctx, dirty)

update 短写

p(ctx, dirty) 包含一些更新元素,他是基于组件中被改变的状态(dirty)和状态(ctx)。

instance variable

编译器发现变量name没有在App的不同组件实例间共享,这就是为什么会把name声明放到名为instance的函数中。

在上一个示例中,无论App组件有多少个实例,变量name的值在这些实例中都是相同的,并且没有变化:

<App />
<App />
<App />

<!-- gives you -->
<h1>Hello world</h1>
<h1>Hello world</h1>
<h1>Hello world</h1>

目前的这个例子里,变量name在组件的一个实例里是可以改变的,因此变量name的声明被移到了instance函数中:

<App />
<App />
<App />

<!-- could possibly be -->
<h1>Hello world</h1>
<h1>Hello Svelte</h1>
<h1>Hello world</h1>
<!-- depending on the inner state of the component -->

instance($$self, $$props, $$invalidate)

instance函数返回一个instance变量列表,这些变量是:

  • 在模板中被引用

  • 可以改变(mutated)或重新分配(在组件的一个实例内改变)

在Svelte中,我们将这个实例变量列表称为ctx

init函数里,Svelte调用instance函数去创建 ctx ,并使用它去创建组件片段。

// 就概念而言,
const ctx = instance(/*...*/);
const fragment = create_fragment(ctx);
// create the fragment
fragment.c();
// mount the fragment onto the DOM
fragment.m(target);

现在,不是直接在组件外貌访问变量name,而是直接通过传递的ctx 来访问变量name

t1 = text(/*name*/ ctx[0]);

$$invalidate

Svelte系统的反应能力是背后秘密是$$invalidate函数。

存在的这些变量只要:

  • 重新赋值或者改变

  • 在模板中引用

会在改变或重新赋值之后通过$$invalidate函数正确的插入:

name = 'Svelte';
count++;
foo.a = 1;

// 编译成类似于这样
name = 'Svelte';
$$invalidate(/* name */, name);
count++;
$$invalidate(/* count */, count);
foo.a = 1;
$$invalidate(/* foo */, foo);

$$invalidate函数会标记变量为dirty并为组件安排更新:

// 概念上来说...
const ctx = instance(/*...*/);
const fragment = create_fragment(ctx);
// 跟踪哪些变量被改变
const dirty = new Set();
const $$invalidate = (variable, newValue) => {
  // 更新 ctx (variable对应的数组的下标)
  ctx[variable] = newValue;
  // 标记变量为dirty
  dirty.add(variable);
  // 为组件安排更新
  scheduleUpdate(component);
};

// 被安排更新时调用
function flushUpdate() {
  // update片段
  fragment.p(ctx, dirty);
  // 清楚dirty标记
  dirty.clear();
}

Adding event listeners

现在来加一个事件监听:

<script>
    let name = 'world';
    function update() {
        name = 'Svelte';
    }
</script>
<h1 on:click={update}>Hello {name}</h1>

Svelte REPL

发现与之前的不同点:

function create_fragment(ctx) {
  // ...
  return {
    c() {
      h1 = element('h1');
      t0 = text('Hello ');
      t1 = text(/*name*/ ctx[0]);
    },
    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, t0);
      append(h1, t1);
+     dispose = listen(h1, 'click', /*update*/ ctx[1]);    },
    p(ctx, [dirty]) {
      if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
    },
    d(detaching) {
      if (detaching) detach(h1);
+     dispose();    },
  };
}

function instance($$self, $$props, $$invalidate) {
  let name = 'world';

  function update() {
    $$invalidate(0, (name = 'Svelte'));
  }
+ return [name, update];}
// ...

一些发现:

  • instance函数现在返回两个变量

  • mount 期间侦听单击事件,并在destroy 中处理它

前面也提到了,instance函数返回的变量列表是被 在模板中引用可变和重新赋值的

因为在模板中引用了update函数,所以作为在instance函数返回的 ctx 的一部分。

你如果在模板中不引用update,这个是不会被加到instance函数返回的数组里的。

由于Svelte尽可能的去返回简洁的js输出,没有必要是不会返回额外的变量。

listen and dispose

dispose(处理)

每当在Svelte中添加事件侦听器时,Svelte将注入代码来添加事件侦听器,并在从DOM中删除DOM片段时将其删除。

尝试多加几个事件监听:

<h1
    on:click={update}
    on:mousedown={update}
    on:touchstart={update}>
  Hello {name}!
</h1>

Svelte REPL

并观察编译后的输出:

// ...
+ dispose = [
+  listen(h1, 'click', /*update*/ ctx[1]),
+  listen(h1, 'mousedown', /*update*/ ctx[1]),
+  listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true }),
+ ];
// ...
+ run_all(dispose);

Svelte并没有声明和创建一个新变量来删除每个事件侦听器,而是将它们全部分配给一个数组:

// instead of
dispose1 = listen(h1, 'click', /*update*/ ctx[1]);
dispose2 = listen(h1, 'mousedown', /*update*/ ctx[1]);
dispose2 = listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true });
// ...
dispose1();
dispose2();
dispose3();

这样变量名压缩可以做的更好。

同样,这是Svelte试图生成更小的JavaScript输出的另一个很好的例子。当只有一个事件侦听器时,Svelte不会创建dispose数组。

Summary

Svelte语法是HTML的超集,就像ts是js的超集。

当你写Svelte组件的时候,Svelte编译器会分析你的代码并生成优化过的Js代码输出。

输出大致可以分成三个部分:

1. create_fragment

  • 返回片段,他的内部是关于如何构建组件DOM片段的。

2. instance

  • <script>标签中写的大多数代码都会在这里。

  • 返回实例中用到的变量列表(可变和重新赋值的或模板中引用的)

  • $$invalidate 在实例变量被改变或者重新赋值的时候正确的插入

3. class App extends SvelteComponent

  • 使用create_fragmentinstance来初始化组件

  • 构建组件内部需要的变量,事件等

Svelte尽可能生成简洁的JavaScript输出:

  • 仅当部分文本可以更新时,将h1的文本内容拆分为单独的文本节点

  • create_fragmentinstance仅仅在需要的时候才会去定义

  • 根据事件侦听器的数量,以数组或函数的形式生成dispose变量

  • 。。。

结束语

这里介绍了Svelte编译输出的基本结构,而这仅仅是开始。

后面还有,待续

基本就是翻译过来的,原文地址:

https://lihautan.com/compile-svelte-in-your-head-part-1/

Last updated