# compile-svelte-in-your-head-1

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

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

```javascript
/* 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元素

```javascript
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上。

```javascript
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。

```javascript
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](https://svelte.dev/docs#Client-side_component_API)

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

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

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

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

```javascript
/* 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](https://svelte.dev/repl/c149ca960b0444948dc0c00a9175bcb3?version=3.19.1)

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

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

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

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

### Updating data

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

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

[Svelte REPL](https://svelte.dev/repl/3841485f4d224774ba42617e4e964968?version=3.19.1)

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

```diff
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** ，并使用它去创建组件片段。

```javascript
// 就概念而言,
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`。

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

### $$invalidate

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

存在的这些变量只要：

* 重新赋值或者改变
* 在模板中引用

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

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

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

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

```javascript
// 概念上来说...
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](https://svelte.dev/repl/5b12ff52c2874f4dbb6405d9133b34da?version=3.19.1)

发现与之前的不同点：

```diff
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中添加[事件侦听器](https://svelte.dev/tutorial/dom-events)时，Svelte将注入代码来添加[事件侦听器](https://developer.mozilla.org/en-US/docs/Web/API/EventListener)，并在从DOM中删除DOM片段时将其删除。

尝试多加几个事件监听：

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

[Svelte REPL](https://svelte.dev/repl/efde6f2aaf624e708767f1bd3e94e479?version=3.19.1)

并观察编译后的输出：

```diff
// ...
+ 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并没有声明和创建一个新变量来删除每个事件侦听器，而是将它们全部分配给一个数组：

```javascript
// 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_fragment`和`instance`来初始化组件
* 构建组件内部需要的变量，事件等
* 提供[组件的API](https://svelte.dev/docs#Client-side_component_API)

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

* 仅当部分文本可以更新时，将h1的文本内容拆分为单独的文本节点
* `create_fragment`和`instance`仅仅在需要的时候才会去定义
* 根据事件侦听器的数量，以数组或函数的形式生成`dispose`变量
* 。。。

### 结束语

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

后面还有，待续

基本就是翻译过来的，原文地址：

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