簡單、好懂的 Svelte 實現原理

大家好,我卡頌。

Svelte問世很久了,一直想寫一篇好懂的原理分析文章,拖了這麼久終於寫了。

本文會圍繞一張流程圖和兩個Demo講解,正確的食用方式是用電腦打開本文,跟着流程圖、Demo一邊看、一邊敲、一邊學。

讓我麼開始吧。

Demo1

Svelte的實現原理如圖:

圖中Component是開發者編寫的組件,內部虛線部分是由Svelte編譯器編譯而成的。圖中的各個箭頭是運行時的工作流程。

首先來看編譯時,考慮如下App組件代碼:

<h1>{count}</h1>

<script>
  let count = 0;
</script>

完整代碼見 Demo1 repl[1]

瀏覽器會顯示:

這段代碼經由編譯器編譯後產生如下代碼,包括三部分:

// 省略部分代碼…
function create_fragment(ctx) {
  let h1;

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

let count = 0;

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

export default App;

create_fragment

首先來看create_fragment方法,他是編譯器根據AppUI編譯而成,提供該組件與瀏覽器交互的方法,在上述編譯結果中,包含 3 個方法:

h1 = element("h1");
h1.textContent = `${count}`;
insert(target, h1, anchor);

insert方法會調用target.insertBefore

function insert(target, node, anchor) {
  target.insertBefore(node, anchor || null);
}
if (detaching) detach(h1);

detach方法會調用parentNode.removeChild

function detach(node) {
  node.parentNode.removeChild(node);
}

仔細觀察流程圖,會發現App組件編譯的產物沒有圖中fragment內的p方法。

這是因爲App沒有**「變化狀態」**的邏輯,所以相應方法不會出現在編譯產物中。

可以發現,create_fragment返回的cm方法用於組件首次渲染。那麼是誰調用這些方法呢?

SvelteComponent

每個組件對應一個繼承自SvelteComponentclass,實例化時會調用init方法完成組件初始化,create_fragment會在init中調用:

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

總結一下,流程圖中虛線部分在Demo1中的編譯結果爲:

可以改變狀態的 Demo

現在修改Demo,增加update方法,爲H1綁定點擊事件,點擊後count改變:

<h1 on:click="{update}">{count}</h1>

<script>
  let count = 0;
  function update() {
    count++;
  }
</script>

完整代碼見 Demo2 repl[2]

編譯產物發生變化,ctx的變化如下:

// 從module頂層的聲明語句
let count = 0;

// 變爲instance方法
function instance($$self, $$props, $$invalidate) {
  let count = 0;

  function update() {
    $$invalidate(0, count++, count);
  }

  return [count, update];
}

countmodule頂層的聲明語句變爲instance方法內的變量。之所以產生如此變化是因爲App可以實例化多個:

// 模版中定義3個App
<App/>
<App/>
<App/>

// 當count不可變時,頁面渲染爲:<h1>0</h1>
<h1>0</h1>
<h1>0</h1>

count不可變時,所有App可以複用同一個count。但是當count可變時,根據不同App被點擊次數不同,頁面可能渲染爲:

<h1>0</h1>
<h1>3</h1>
<h1>1</h1>

所以每個App需要有獨立的上下文保存count,這就是instance方法的意義。推廣來說,Svelte編譯器會追蹤<script>內所有變量聲明:

一旦發現,就會將該變量提取到instance中,instance執行後的返回值就是組件對應ctx

同時,如果執行如上操作的語句可以通過模版被引用,則該語句會被$$invalidate包裹。

Demo2中,update方法滿足:

所以編譯後的update內改變count的語句被$$invalidate方法包裹:

// 源代碼中的update
function update() {
  count++;
}

// 編譯後instance中的update
function update() {
  $$invalidate(0, count++, count);
}

從流程圖可知,$$invalidate方法會執行如下操作:

p方法是Demo2中新的編譯產物,除了p之外,create_fragment已有的方法也產生相應變化:

c() {
  h1 = element("h1");
  // count的值變爲從ctx中獲取
  t = text(/*count*/ ctx[0]);
},
m(target, anchor) {
  insert(target, h1, anchor);
  append(h1, t);
  // 事件綁定
  dispose = listen(h1, "click", /*update*/ ctx[1]);
},
p(ctx, [dirty]) {
  // set_data會更新t保存的文本節點
  if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
},
d(detaching) {
  if (detaching) detach(h1);
  // 事件解綁
  dispose();
}

p方法會執行$$invalidate中標記爲dirty的項對應的更新函數。

Demo2中,App UI中只引用了狀態count,所以update方法中只有一個if語句,如果UI中引用了多個狀態,則p方法中也會包含多個if語句:

// UI中引用多個狀態 
<h1 on:click="{count0++}">{count0}</h1>
<h1 on:click="{count1++}">{count1}</h1>
<h1 on:click="{count2++}">{count2}</h1>

對應p方法包含多個if語句:

p(new_ctx, [dirty]) {
  ctx = new_ctx;
  if (dirty & /*count*/ 1) set_data(t0, /*count*/ ctx[0]);
  if (dirty & /*count1*/ 2) set_data(t2, /*count1*/ ctx[1]);
  if (dirty & /*count2*/ 4) set_data(t4, /*count2*/ ctx[2]);
},

Demo2完整的更新步驟如下:

  1. 點擊H1觸發回調函數update

  2. update內調用$$invalidate,更新ctx中的count,標記countdirty,調度更新

  3. 執行p方法,進入dirty的項(即count)對應if語句,執行更新對應DOM Element的方法

總結

Svelte的完整工作流程會複雜的多,但是核心實現便是如此。

我們可以直觀的感受到,藉由模版語法的約束,經過編譯優化,可以直接建立**「狀態與要改變的 DOM 節點的對應關係」**。

Demo2中,狀態count的變化直接對應p方法中一個if語句,使得Svelte執行**「細粒度的更新」**時對比使用虛擬DOM的框架更有性能優勢。

上述性能分析中第四行**「select row」**就是一個**「細粒度的更新」**。想比較之下,React(倒數第三列)性能就差很多。

參考資料

[1]

Demo1 repl: https://svelte.dev/repl/9945d189204a4168b4c23890f1d92a3a?version=3.19.1

[2]

Demo2 repl: https://svelte.dev/repl/bf22a31a0eff4875b5b3084aa2b85fc3?version=3.19.1

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6nS6jI-1Q0BOkxEQ1HmD_A