前言
從 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: 代表
destory
或detatch
也就是元件卸載時要執行的函數
詳細的原始碼及生成邏輯可以到 src/compiler/compile/render_dom/Block.ts 參考。
知道每個單字代表的意思的話,接下來他們在做的事情就會清楚許多:
- 將條件式 (
if count != 100
)的結果賦值給if_block
- 在
create
時- 建立
p
element - 將
p.textContent
賦值為this is text
- 建立
- 在
mount
時- 如果
if_block
為true
則呼叫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_fragment
、instance
、SvelteComponent
-
create_fragment
:告訴 Svelte 元件中的每個生命週期應該如何處理 -
instance
:執行<script>
當中的程式碼,並且回傳 context(props、變數等) -
SvelteComponent
:透過init
函數初始化 Svelte 元件
這篇文章闡述了 Svelte 生成的程式碼要如何解析,並且簡單說明了背後的 reactive 機制是如何達成的(實際上還有很多沒有提到的機制,會在後續文章中繼續說明)。這樣一來大家都看得懂 Svelte 生成的程式碼啦!
想要看更多 Svelte 相關的文章可以參考這裡