Svelte 響應式資料
tags: Svelte
category: Front-End
description: Svelte 響應式資料
created_at: 2023/04/02 15:00:00
前言
老樣子,還是假設你已經做好開新專案與環境設定裡面所做的事
這邊也和 Vue
或 React
差很多,但也比較方便一些(?)
在 Vue
當中是使用 ref()
或 reactive()
去做到。
在 React
當中就是 useState()
這個 hook
去做。
而在 Svelte
中,你只要有 assign(=)
,就可以了。
前面都沒有提過 Svelte
核心跟其他像是 Vue
、 React
到底哪裡不一樣,這邊剛好比較適合帶出來提。
與其他框架的差異
也不見得是框架(framework
),因為你可能會說是函式庫(library
),總之就是到底 Svelte
與其他像是 Vue
、 React
到底哪裡不一樣?
在 Svelte
出現之前,大多都是採用虛擬DOM
(Virtual DOM
)去做即時的更新 UI
,而 Virtual DOM
雖然主打會比原生(native
)效能快,實際上是因為基於 diff
演算法先去找更新前後的 DOM
的差異,重複利用沒變化的部分,舉例簡單的例子來說:
假設有一個資料 data
的值是 Hello
,且假設下面的語法合法。
<div>{data}</div>
會輸出
<div>Hello</div>
然而 data
改成 Hello World
,理所當然的會輸出
<div>Hello World</div>
但他是用同一個 div
,所以實際上他是改了 innerText
之類的東西。
所以為什麼會比原生操作還快? 有些人可能會覺得不可思議,他跑的都是 JavaScript
,為什麼可以比自己還快?
更精確地說一些應該說操作 DOM
的速度? 以上面的例子來說,如果你每次都刪除所有 DOM
,重新 append
所有節點,就很慢。
但如果透過一個演算法去找出差異的部分,不要每次都去刪除又插入,只插入跟刪除必要做的,重複利用可以直接用的,當然就會比較快了。
但這時 diff 演算法就很重要,但也不是你我可以控制的就是了,但他也足夠快了
Svelte 怎麼做?
Svelte
主打沒有 Virtual DOM
,上面提到 Virtual DOM
他是在 runtime
的時候做運算,而 Svelte
像是一個編譯器(compiler
),他把 runtime
要做的事情移動到編譯的時候做,所以對於瀏覽器來說他只是執行 JavaScript
而已,不用再去跑 diff
演算法。
舉例來說在做更新的時候:
- 檢查前後
DOM
有無差異,假設都是div
,就可以沿用 - 去檢查所有屬性是否需要修改
- 發現有東西改變,所以去更新真實的
DOM
可以看到除了第三點是真的去動 DOM
,而其他步驟都只是在檢查,而且檢查也是跑在 runtime
,如果可以把檢查移動到編譯的時候做掉,那 runtime
效能就會提高許多。
套用官方原文:Unlike traditional UI frameworks, Svelte is a compiler that knows at build time how things could change in your app, rather than waiting to do the work at run time.
簡單說就是: Svelte
跟傳統的框架不同,Svelte
是一個編譯器,他知道你的應用程式什麼時候需要更新,而不是在 runtime
才跑那些額外的檢查。
舉例來說,當你寫下面簡單的語法:
<script>
let num = 5;
</script>
{#if num % 2 == 0}
{num} is even
{:else}
{num} is odd
{/if}
<button on:click={() => (num += 1)}>num++</button>
雖然還沒講到事件處理,不過反正就是點擊按鈕,num
會被 +1
。
這時 Svelte
會編譯出像下面的東西(節錄):
function create_else_block(ctx) {
// 略...
}
function create_if_block(ctx) {
// 略...
}
function create_fragment(ctx) {
// 略...
function select_block_type(ctx, dirty) {
if (/*num*/ ctx[0] % 2 == 0) return create_if_block;
return create_else_block;
}
let current_block_type = select_block_type(ctx, -1);
let if_block = current_block_type(ctx);
const block = {
// 略...
m: function mount(target, anchor) {
if_block.m(target, anchor);
// 略...
if (!mounted) {
dispose = listen_dev(button, "click", /*click_handler*/ ctx[1], false, false, false, false);
mounted = true;
}
},
p: function update(ctx, [dirty]) {
if (current_block_type === (current_block_type = select_block_type(ctx, dirty)) && if_block) {
if_block.p(ctx, dirty);
} else {
if_block.d(1);
if_block = current_block_type(ctx);
if (if_block) {
if_block.c();
if_block.m(t0.parentNode, t0);
}
}
},
// 略...
};
// 略...
}
function instance($$self, $$props, $$invalidate) {
// 略...
let num = 6;
// 略...
const click_handler = () => $$invalidate(0, num += 1);
// 略...
}
主要的秘密在 $$invalidate
當中,他會設定新的值進去,然後把變數變髒(標記dirty
),並告訴 Svelte
要更新。
你可能會問,設定新的值,設給誰,0
??
Svelte
會在底層為每個變數分配一個索引,在底層驗證變數是否 dirty
也是使用位元運算。
所以 Svelte 怎麼樣讓他知道要更新?
就如同最前面說的,你只要有 assign(=)
,就可以了。
這邊只要注意,一樣會有類似 Vue
碰到的問題,監聽不到 array.push
之類的。
而在 Svelte
當中,則是把變數設回給自己,去解決這個問題。
先嘗試寫一個正確會更新的
<script>
let nums = [1, 2, 3, 4, 5];
const pushNum = () => {
nums.push(nums.length + 1);
nums = nums;
};
</script>
<button on:click={pushNum}>num++</button>
{#each nums as num}
<p>{num}</p>
{/each}
會看到他編譯出這個:
const pushNum = () => {
nums.push(nums.length + 1);
$$invalidate(0, nums);
};
而假設你把 nums=nums
拿掉,他就不會編譯出 $$invalidate(0, nums);
,自然就不會更新。不信你自己試試
Svelte 如何更新?
上面有提到說 $$invalidate
當中,他會設定新的值進去,然後把變數變髒(標記dirty
),並告訴 Svelte
要更新。
而更新的函數原始碼如下,
const resolved_promise = /* @__PURE__ */ Promise.resolve();
let update_scheduled = false;
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
看到這個,大概可以猜測他會批次更新,且因為 promise
是微任務(microtask
),所以他會在最後才一起處理。()。microtask
相關的內容之後有機會再來跟 Event loop
一起寫一篇文章填坑
總之就先以一個範例帶過,假設你寫下面這段 js
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
上面的輸出會是
end
promise
timeout
end
先出現,這個有一點經驗的應該都能輕易判斷出來,但 promise
跟 setTimeout
的順序可能就會猶豫了一下。
總之可以先簡單想成下面這樣更新:
const resolvedPromise = Promise.resolve();
let isUpdating = false;
function onUpdated() {
console.log('onUpdated');
isUpdating = false;
}
function update() {
console.log('update');
if (!isUpdating) {
isUpdating = true;
resolvedPromise.then(onUpdated);
}
}
update();
update();
他會輸出
update
update
onUpdated
也就是說不管你觸發幾次 $$invalidate
,他都只會真的去執行 flush
一次。
懶人包總結
只要你有 =(assign)
出現,他編譯出 $$invalidate
,就可以了。
補充: 一個要注意的陷阱?
雖然說得很簡單,有 =
出現就可以,不過還是有些地方要注意。
雖然你應該不會寫出這麼髒的程式,但還是做個紀錄
<script>
let obj = {
foo: 'bar',
deep: {
foo: 'bar'
}
};
let x = {
foo: 'bar x'
};
const changeFoo = () => {
x = obj;
x.foo = 'baz x';
};
const changeDeepFoo = () => {
obj.deep.foo = 'baz';
};
</script>
<div>{obj.foo}</div>
<div>{obj.deep.foo}</div>
<hr />
<div>x: {x.foo}</div>
<hr />
<button on:click={changeFoo}>Change foo</button>
<br />
<button on:click={changeDeepFoo}>Change deep foo</button>
他會編譯出
let obj = { foo: 'bar', deep: { foo: 'bar' } };
let x = { foo: 'bar x' };
const changeFoo = () => {
$$invalidate(1, x = obj);
$$invalidate(1, x.foo = 'baz x', x);
};
const changeDeepFoo = () => {
$$invalidate(0, obj.deep.foo = 'baz', obj);
};
會發生什麼事大概可以想像,當你點 changeFoo
,他只會更新用到 x
的依賴,所以雖然 obj.foo
被更新了,但畫面上不會更新,直到你點下 changeDeepFoo
他觸發 obj
的更新,畫面上用到 obj.foo
才會更新。