iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
Modern Web

Svelte 的奇妙冒險系列 第 29

[Svelte 的奇妙冒險] Day 29 - Svelte 編譯器與 Signal

  • 分享至 

  • xImage
  •  

之前一直沒特別說明 Svelte 中 reactivity 的細節以及如何實作的,今天就來「稍微」探討一下 Svelte 的編譯器到底做了什麼事情。

Svelte 編譯器

來看一個簡單的計數器例子

<script>
	let count = $state(0);

	function increment() {
		count += 1;
	}
</script>

<button onclick={increment}>
	clicks: {count}
</button>

上面這個 Svelte 程式經過編譯器後會變成下面 js code

這邊是用 Svelte 5 的 REPL 的輸出,實際上專案中的 output 會經過壓縮及混淆等等程序。

import * as $ from "svelte/internal/client";

function increment(_, count) {
	$.set(count, $.get(count) + 1);
}

var root = $.template(`<button> </button>`);

export default function App($$anchor) {
	let count = $.state(0);
	var button = root();

	button.__click = [increment, count];

	var text = $.child(button);

	$.reset(button);
	$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`));
	$.append($$anchor, button);
}

$.delegate(["click"]);

可以看出來 Svelte 的編譯器主要就是幫我們把狀態與畫面之間的互動邏輯給完成了,像是我們寫完的 markup 怎麼被畫在 DOM 上、事件與狀態更新的邏輯、事件的掛載以及狀態更新時 DOM 如何被更新。

如果只看這兩個片段

function increment(_, count) {
	$.set(count, $.get(count) + 1);
}


$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`));

會看到幾個比較有意思的 Svelte runtime 的 function setgettemplate_effect ,大概可以推論說 Svelte 底層自動地幫我把狀態的變更以及狀態變更後的畫面重新渲染給實作了,然後這邊也可以看出一個小細節

$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`))

這行程式碼的另外一層意義就是這個地方是單獨 re-render 的,也就是說它不像 React 一樣是「以 component 作為 re-render 的最小單位」,至於什麼情況下會觸發 template_effect 呢?基本上可以認為只有它所依賴的狀態有變更時才會 re-render ,以這個例子來說就是 count 有變動時才會觸發 re-render ,而且也只會讓這裡 re-render 而不是整個 Svelte component。

Signal

上面的例子或許有的讀者已經能夠聯想到 「 Signal」這個大概這一兩年在前端圈裡常常提到的概念,簡單來說就是「Fine-grained reactivity」直翻的話大概就是細顆粒度響應性,至於為什麼說是「細顆粒度」因為相比「以 component 作為 re-render 的最小單位」來說, Signal 只會在狀態更新時,有用到該狀態的地方會自動重新計算。

雖然聽起來有點像 observable ,但不管在實作還是概念上還是有相當大的差距,對於這個主題有興趣的讀者可以參閱這篇文章,這裡就不展開討論了。

這邊「簡單實現」一個 Signal 來理解看看這個概念

let subscriber

export const signal = (value) => {
  let subscriptions = [];
  return{
    get value(){
      if(subscriber){
        subscriptions.push(subscriber)
      }
      return value;
    },
    set value(updated){
      value = updated;
      subscriptions.forEach(fn=>fn())
    }
  }
}


export const effect = (fn)=>{
  subscriber = fn;
  fn()
  subscriber = null;
}

export const derived = (fn)=>{
  const dep = signal()
  effect(()=>{
    dep.value = fn()
  })
  return dep
}
import { effect, signal, derived } from "./signal.js";

document.querySelector("#app").innerHTML = `
  <div>
    <button id='counter' onclick={increment}>
    </button>
  </div>
`;

const $btn = document.querySelector("#counter");

const count = signal(0);

$btn.addEventListener("click", () => {
  count.value++;
});

effect(() => {
  $btn.textContent = `clicks: ${count.value}`;
});


這樣看應該就大概能理解是在使用 effect 時如果有使用到 signal 便會順便把 subscriber 記起來(或者可以理解就是訂閱),然後在 signal 更新時會再次執行所有的 subscriptions

當然這跟 Svelte 編譯器實際上做的事情還有一段差距,像是自動產生類似 template_effect 的行為來 re-render 以及某些效能優化的實作,但大致上應該可以為什麼 Svelte 的 $effectderived 是怎麼做到像是自動收集依賴的行為,就是因為這些狀態更新後會自動重新計算這些 subscriptions


參考資料

source code


上一篇
[Svelte 的奇妙冒險] Day 28 - 部署
下一篇
[Svelte 的奇妙冒險] Day 30 - 所以為什麼是 Svelte 而不是 React
系列文
Svelte 的奇妙冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言