Svelte 5 ทำงานยังไง Part 1: Reactivity

📅 July 7, 2025 JS Framework

ผมเชื่อว่าหลายคนที่เขียน Svelte คงรู้กันดีแล้วว่า Svelte จะคอมไพล์โค้ดที่คุณเขียนให้กลายเป็น tightly optimized JavaScript ดังนั้นเรามาแงะ svelte กันดีกว่าครับ

ℹ️ บทความนี้เขียนตอน Svelte 5.35.3

สั้นๆ คือมันจะแปลง

Counter
<script>
  let count = $state(5);
</script>

<h1>Counter</h1>
<p>count = {count}</p>
<button onclick={() => (count += 1)}> increment </button>

ให้กลายเป็นอะไรประมาณนี้ (เน้นคำว่าประมาณมากๆ) ส่วนนี่ output จริง (กดที่แท็บ JS output เพื่อดู)

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

export default function App($$parent: Element) {
	// โค้ดที่เราเขียน
	let count = $.state(5);

	// โค้ดที่ generate
	const h1 = document.createElement("h1");
	h1.textContent = "Counter";
	$$parent.appendChild(h1);

	const p = document.createElement("p");
	$.effect(() => {
		p.textContent = `count = ${$.get(count)}`;
	});
	$$parent.appendChild(p);

	const button = document.createElement("button");
	button.addEventListener("click", () => {
		$.set(count, $.get(count) + 1);
	});
	$$parent.appendChild(button);
}

Wtf is this

ในส่วนที่ generate ออกมามีโค้ดอยู่ 2 ส่วนด้วยกัน นั่นคือส่วนที่ใช้สร้าง tree ของ dom node และส่วนที่ update มัน (p.textContent = ...)

จะเห็นได้ว่าในโค้ดพวกนั้นมันจะ update dom ตรงๆ เลย ไม่เหมือน React ที่ต้องมานั่งไล่ diff virtual dom ก่อนทุกครั้งที่มีการแก้ไข state

Reactivity

พอดู Compiler output แล้วผมเชื่อว่าคุณคงจะสงสัยว่าทำไมโค้ดเกี่ยวกับ state ถึงหน้าตาเกือบจะเหมือนเดิม 100% มันควรจะมีโค้ดสักส่วนที่ diff state หรือสั่งให้ render ใหม่ แต่เราไม่เจออะไรแบบนั้นเลย

ความมหัศจรรย์ของ Svelte (5) เนี่ยจะอยู่บนสิ่งที่เรียกว่า Signal ซะเยอะ เพราะ Signal นี่แหละทำให้เราสามารถ update dom เฉพาะส่วนที่เกี่ยวข้องได้ โดยที่ไม่จำเป็นต้องไปยุ่งกับ user code มาก

Signals เป็นหนึ่งในวิธีที่ใช้ทำ Reactivivity เช่นเดียวกับ Rxjs มันทำให้เราสามารถเขียนโปรแกรมที่ "ตอบสนองต่อการเปลี่ยนแปลงของข้อมูล" ในแบบที่ declarative มากๆ ได้

เขียนแบบปกติ
let a = 1;
let b = 2;
let c = a + b;

console.log(c); // 3
b = 10;
console.log(c); // 3 เหมือนเดิม

จะเห็นว่าถึงเราเปลี่ยน b แล้ว c ก็ยังเท่าเดิมอยู่

แก้ให้ c อัพเดทตาม
let a = 1;
let b = 2;
let c = a + b;

console.log(c); // 3
b = 10;
c = a + b;
console.log(c); // 12

ถ้าเขียนโดยใช้ reactivity ของ svelte จะได้แบบนี้แทน

Svelte
let a = $state(1);
let b = $state(2);
let c = $derived(a + b);

console.log(c); // 3
b = 10;
console.log(c); // 12

c จะอัพเดทโดยอัตโนมัติเมื่อ dependency ของมัน (a กับ b) ถูกอัพเดท

Signals ใน framework อื่นๆ เช่น Vue กับ Solid สามารถใช้ตรงๆ ได้เลย โดยที่ไม่ต้อง compile ก่อน ส่วน Svelte เลือกที่จะใช้ compiler เพื่อลดความยุ่งยากจากการใช้ signals ตรงๆ ลง

Svelte 5
let count = $state(0);

$effect(() => {
  console.log(count); // print 0 แล้ว 1
});

count += 1;
Vue 3
const count = ref(0); // คล้าย $state 

watchEffect(() => { // $effect
  console.log(count.value); // print 0 แล้ว 1
})

count.value += 1;

ใน vue ค่าของ state จะอยู่ภายใต้ .value

SolidJS
const [count, setCount] = createSignal(0);

createEffect(() => { 
  console.log(count());
});

setCount(count() + 1);

ส่วนใน Solid เราจะต้องใช้ getter กับ setter ในการเข้าถึงค่า

ที่จริง Vue ทำแบบเดียวกัน เพียงแต่ใช้ property getter (+ proxy) แทนใช้ฟังก์ชั่นตรงๆ

ใน Svelte ถ้าย้อนกลับไปดูข้างบนจะเห็น $.get กับ $.set ครอบตัวแปรที่เรามาร์คด้วย $state ไว้ เพราะว่าในโค้ดที่คอมไพล์ count ที่ return มาจาก $.state ไม่ได้เป็น number แต่เป็น object ที่ครอบ number อีกที (คล้ายๆ ref ของ Vue)

Signals ทำงานยังไง

เนื่องจากว่า signals ก็เป็น Observer ประเภทนึง เราดู Observable กันก่อน

Observable คือสิ่งที่สามารถ "แจ้งเตือน" "ผู้สังเกต" (Subscriber) ได้ เมื่อเกิด event บางอย่างขึ้น ที่ใช้กันบ่อยๆ ก็เช่น

button.addEventListener("click", (event) => {
  console.log("Clicked");
});

ผู้สังเกตในที่นี้คือ callback ที่ pass ไป มันจะถูกเรียกทุกครั้งที่มี event คลิก

ทีนี้ เราสามารถทำ Value wrapper แบบง่ายๆ ที่จะแจ้งเตือน subscriber (aka effect) เมื่อค่าเปลี่ยนได้ด้วยการใช้ observable ประมาณนี้

function observable(initialValue) {
  let value = initialValue;
  const subscriberFns = [];

  return {
    subscribe(fn) {
      subscriberFns.push(fn);
    },
    set(newValue) {
      value = newValue;
      // แจ้ง subscriber ทุกราย
      for (const subscriberFn of subscriberFns) {
        subscriberFn(value);
      }
    },
    get() {
      return value;
    }
  };
}

ถ้าเอามาทำเป็น counter แบบเดียวกับด้านบน จะได้

const p = document.querySelector("p");
const button = document.querySelector("button");

const count = observable(5);

p.textContent = `count = ${count.get()}`;

button.addEventListener("click", () => {
  count.set(count.get() + 1);
});

count.subscribe((newCount) => {
  p.textContent = `count = ${newCount}`;
});

So what's signals

มันคือ Observable ที่

เราจะไม่แตะ 3 ถึง 6

ในตัวอย่าง Observable เราระบุ dependency และ subscribe โดยใช้ method .subscribe ใน signals เราจะใช้ฟังก์ชั่น effect แทน มันจะ track dependency จากการอ่านค่าของ signal

ก็คือเมื่อเรารัน effect อยู่ และมีการอ่านค่า signal (ไม่ว่าจะด้วย .get() หรือ .value) มันก็จะ subscribe signal นั้นให้อัตโนมัติ

อันนี้คือ implementation ที่เอา observable จากตัวอย่างก่อนหน้ามาปรับ

function signal(initialValue) {
  let value = initialValue;
  const subscribers = new Set();

  return {
    get() {
      if (currentEffect) {
        subscribers.add(currentEffect);
      }
      return value;
    },
    set() {
      value = initialValue;
      for (const subscriber of subscribers) {
        subscriber();
      }
    }
  };
}

let currentEffect = null;

function effect(fn) {
  currentEffect = fn;
  fn();
  currentEffect = null;
}

ℹ️ implementation นี้ละรายละเอียดไปหลายอย่าง เพื่อไม่ให้ไม่ให้โค้ดเยอะเกินไป

ตอนใช้ก็

const p = document.querySelector("p");
const button = document.querySelector("button");

const count = signal(5);

button.addEventListener("click", () => {
  count.set(count.get() + 1);
});

effect(() => {
  p.textContent = `count = ${count.get()}`;
});

ข้อแตกต่างสำคัญของ effect กับการเรียก subscribe ตรงๆคือ effect จะถูกรันทันทีเสมอ* ไม่งั้นก็จะไม่สามารถ track dependency ได้ ไม่เหมือน subscribe ที่จะรันเฉพาะเมื่อ observable สั่ง

สรุปแล้วในเรื่อง reactivity สิ่งที่ compiler ของ Svelte ต้องทำก็มีแค่

แล้วก็ปล่อยให้ Signals จัดการที่เหลือก็จบละ

และเนื่องจากว่า Svelte compiler มัน compile ทีละไฟล์ มันไม่มีทางรู้ได้เลยว่าตัวแปรไหน (ที่ไฟล์อื่น export มา) เป็น state บ้าง นี่คือสาเหตุที่เราใช้ state จากไฟล์อื่นตรงๆ ไม่ได้

เดี๋ยว Part 2 มีเรื่องอื่นต่อ (if/else, each, props/binding, anchor มั้ง)

ดูเพิ่ม