半熟前端

軟體工程師 / 台灣人 / 在前端的路上一邊探索其他領域的可能性

本部落格使用 Gatsby 製作

本部落格有使用 Google Analytic 及 Cookie

前端

深入理解 Svelte(2)— 分析 Svelte 生成程式碼

深入理解 Svelte(2)— 分析 Svelte 生成程式碼

前言

Svelte 的核心理念可以得知,Svelte 希望從編譯過程中盡可能地獲取必要資訊,減少在動態的 overhead。上一篇文章中說明了 Svelte 從編譯到生成程式碼是如何運作的,今天要來觀察一下 Svelte 生成的程式碼是怎麼運作的。

先觀察一個簡單的 Svelte 元件:

html
<script>
import { onMount } from 'svelte';
let count = 1;
onMount(() => {
setInterval(() => count++, 1000);
})
</script>
{#if count != 100}
<span>{count}</span>
{/if}
<p>
this is text
</p>

Svelte 元件語法跟一般 HTML 相同,除了會加上類似樣板語法之外(if, await)基本上可以完全相容 HTML,但是生成的元件又是 JavaScript,舉例來說上面的元件編譯後會變成:

js
// 由 Svelte 生成,省略部分程式碼
import { onMount } from "svelte";
function create_if_block(ctx) {
let span;
let t;
return {
c() {
span = element("span");
t = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, span, anchor);
append(span, t);
},
p(ctx, dirty) {
if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
},
d(detaching) {
if (detaching) detach(span);
}
};
}
function create_fragment(ctx) {
let t0;
let p;
let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);
return {
c() {
if (if_block) if_block.c();
t0 = space();
p = element("p");
p.textContent = "this is text";
},
m(target, anchor) {
if (if_block) if_block.m(target, anchor);
insert(target, t0, anchor);
insert(target, p, anchor);
},
p(ctx, [dirty]) {
if (/*count*/ ctx[0] != 100) {
if (if_block) {
if_block.p(ctx, dirty);
} else {
if_block = create_if_block(ctx);
if_block.c();
if_block.m(t0.parentNode, t0);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
},
i: noop,
o: noop,
d(detaching) {
if (if_block) if_block.d(detaching);
if (detaching) detach(t0);
if (detaching) detach(p);
}
};
}
function instance($$self, $$props, $$invalidate) {
let count = 1;
onMount(() => {
setInterval(() => $$invalidate(0, count++, count), 1000);
});
return [count];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}

Svelte 本身也支援 SSR 功能,所以如果使用 Svelte SSR 功能編譯上面的程式碼,則會建立一個生成 HTML 字串的函數。

js
// 由 Svelte 生成,省略部分程式碼
import { onMount } from "svelte";
const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
let count = 1;
onMount(() => {
setInterval(() => count++, 1000);
});
return `${count != 100 ? `<span>${escape(count)}</span>` : ``}
<p>this is text
</p>`;
});
export default App;

觀察生成程式碼(dom)

為了方便說明,在這邊只針對 dom 的生成程式碼做解說, SSR 的部分則先跳過。

可以看到生成的程式碼主要以三個部分組成:create_fragment 函數、instance 函數、以及 SvelteComponent class

create_fragment

首先先看到 create_fragment

js
function create_fragment(ctx) {
let t0;
let p;
let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);
return {
c() {
if (if_block) if_block.c();
t0 = space();
p = element("p");
p.textContent = "this is text";
},
m(target, anchor) {
if (if_block) if_block.m(target, anchor);
insert(target, t0, anchor);
insert(target, p, anchor);
},
p(ctx, [dirty]) {
if (/*count*/ ctx[0] != 100) {
if (if_block) {
if_block.p(ctx, dirty);
} else {
if_block = create_if_block(ctx);
if_block.c();
if_block.m(t0.parentNode, t0);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
},
i: noop,
o: noop,
d(detaching) {
if (if_block) if_block.d(detaching);
if (detaching) detach(t0);
if (detaching) detach(p);
}
};
}

create_fragment 回傳一個物件,裡頭有很多單個英文字母當作屬性的函數看起來不知道在做什麼,其實分別代表著不同生命週期要做的事情:

  • c: 代表 create 也就是剛建立元件時要執行的函數
  • m: 代表 mount 也就是元件掛載到 DOM 上後要執行的函數
  • p: 代表 patch 也就是元件更新後要執行的函數
  • i: 代表 intro 也就是元件 transition 進場時要執行的函數
  • o: 代表 outro 也就是元件 transition 出場時要執行的函數
  • d: 代表 destorydetatch 也就是元件卸載時要執行的函數

詳細的原始碼及生成邏輯可以到 [src/compiler/compile/renderdom/Block.ts](src/compiler/compile/renderdom/Block.ts) 參考。

知道每個單字代表的意思的話,接下來他們在做的事情就會清楚許多:

  • 將條件式 (if count != 100)的結果賦值給 if_block
  • create

    • 建立 p element
    • p.textContent 賦值為 this is text
  • mount

    • 如果 if_blocktrue 則呼叫 if_block.m() (也就是 mount 要做的事)
    • t0 插入到 anchor 中
    • p 插入到 anchor 中
  • patch

    • 如果條件式 count != 100 為 true
    • 已經有 if_block 的話就呼叫 if_block.p()
    • 沒有的話呼叫一次 create_if_block 然後執行 if_block.m()
    • 如果條件式 count != 100 為 false
    • 代表 if_block 裡的東西要刪除,呼叫 if_block.m()

instance

再來看到 instance 函數

js
function instance($$self, $$props, $$invalidate) {
let count = 1;
onMount(() => {
setInterval(() => $$invalidate(0, count++, count), 1000);
});
return [count];
}

<script> 裡的程式碼都會被塞到 instance 的函數當中,在這裡有幾個比較特別的地方:

  • 原本程式碼是 setInterval(() => count++, 1000) 生成後變成了 setInterval(() => $$invalidate(0, count++, count), 1000)
  • 回傳值是個陣列,回傳 count 的值

Svelte 會在靜態分析時得知變數的相關訊息,所以能夠幫你做到依賴追蹤,這邊的 $$invalidate 運作有點像是 React 當中的 setState。只是一個是要自己手動加,一個是 Svelte 自動幫你偵測處理好。

$$invalidate 的實作長這樣(省略部分程式碼):

js
// 如果發現變數值不同,將 component 設為 dirty (代表需要更新)
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
if (ready) make_dirty(component, i);
}
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}

每次 setInterval 觸發 count++ 之後,$$invalidate 就會被呼叫,這時會先去比較更新前後的值是否相同,如果有更新的話就會呼叫 mark_dirty 函數,然後將 component 放到 dirty_components 裡頭,並且安排更新。Svelte 也實作了類似 batch update 機制,會盡量在一個 frame 裡頭盡可能地一次更新。

SvelteComponent

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

SvelteComponent 的實作相當簡單,呼叫了 init 函數,裡頭主要的邏輯是初始化 svelte 元件並且調用 create_fragment 函數與執行 instance,讓元件實際掛載到 DOM 上面去。

總結

Svelte 生成的程式碼大致上包含了三大部分 create_fragmentinstanceSvelteComponent

  • create_fragment:告訴 Svelte 元件中的每個生命週期應該如何處理
  • instance:執行 <script> 當中的程式碼,並且回傳 context(props、變數等)
  • SvelteComponent:透過 init 函數初始化 Svelte 元件

這篇文章闡述了 Svelte 生成的程式碼要如何解析,並且簡單說明了背後的 reactive 機制是如何達成的(實際上還有很多沒有提到的機制,會在後續文章中繼續說明)。這樣一來大家都看得懂 Svelte 生成的程式碼啦!

想要看更多 Svelte 相關的文章可以參考這裡

如果覺得這篇文章對你有幫助的話,可以考慮到下面的連結請我喝一杯 ☕️
可以讓我平凡的一天變得閃閃發光 ✨

Buy me a coffee