Svelte 5 ทำงานยังไง Part 1: Reactivity
📅 July 7, 2025 • JS Framework
ผมเชื่อว่าหลายคนที่เขียน Svelte คงรู้กันดีแล้วว่า Svelte จะคอมไพล์โค้ดที่คุณเขียนให้กลายเป็น tightly optimized JavaScript ดังนั้นเรามาแงะ svelte กันดีกว่าครับ
ℹ️ บทความนี้เขียนตอน Svelte 5.35.3
สั้นๆ คือมันจะแปลง
<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 ก็ยังเท่าเดิมอยู่
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 จะได้แบบนี้แทน
let a = $state(1);
let b = $state(2);
let c = $derived(a + b);
console.log(c); // 3
b = 10;
console.log(c); // 12c จะอัพเดทโดยอัตโนมัติเมื่อ dependency ของมัน (a กับ b) ถูกอัพเดท
Signals ใน framework อื่นๆ เช่น Vue กับ Solid สามารถใช้ตรงๆ ได้เลย โดยที่ไม่ต้อง compile ก่อน ส่วน Svelte เลือกที่จะใช้ compiler เพื่อลดความยุ่งยากจากการใช้ signals ตรงๆ ลง
let count = $state(0);
$effect(() => {
console.log(count); // print 0 แล้ว 1
});
count += 1;const count = ref(0); // คล้าย $state
watchEffect(() => { // $effect
console.log(count.value); // print 0 แล้ว 1
})
count.value += 1;ใน vue ค่าของ state จะอยู่ภายใต้ .value
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 ที่
- สนใจแค่ value ในชั่วขณะหนึ่งๆ เท่านั้น ไม่ยุ่งกับ stream ของ value ที่เปลี่ยนแปลงเมื่อระยะเวลาผ่านไป
- track dependency เองได้ ไม่จำเป็นต้องระบุ
- unsubscribe เองได้
- การันตีความถูกต้องของข้อมูลในทุกชั่วขณะ (aka "Glitch free")
- Lazy by default
- and a lot more
เราจะไม่แตะ 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 ต้องทำก็มีแค่
- ใส่จุดหลัง $ ใน
$stateกับ$effect(กับที่เหลืออีก 6-7 อัน) - ใส่
$.getกับ$.setทุกที่ ที่มีการอ่านหรือเขียนตัวแปรที่เป็น state - ใน template ส่วนที่มีการ interpolate ข้อความก็จะแปลงเป็น effect ที่ครอบการ update dom ส่วนนั้นแทน
- (มีเพิ่มพาร์ทหลัง)
แล้วก็ปล่อยให้ Signals จัดการที่เหลือก็จบละ
และเนื่องจากว่า Svelte compiler มัน compile ทีละไฟล์ มันไม่มีทางรู้ได้เลยว่าตัวแปรไหน (ที่ไฟล์อื่น export มา) เป็น state บ้าง นี่คือสาเหตุที่เราใช้ state จากไฟล์อื่นตรงๆ ไม่ได้
เดี๋ยว Part 2 มีเรื่องอื่นต่อ (if/else, each, props/binding, anchor มั้ง)