半熟前端

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

前端

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

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

前言

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

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

<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,舉例來說上面的元件編譯後會變成:

// 由 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 字串的函數。

// 由 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

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 函數

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 的實作長這樣(省略部分程式碼):

// 如果發現變數值不同,將 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

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 相關的文章可以參考這裡