JavaScript開(kāi)發(fā)簡(jiǎn)單易懂的Svelte實(shí)現(xiàn)原理詳解
Svelte問(wèn)世很久了,一直想寫一篇好懂的原理分析文章,拖了這么久終于寫了。
Demo1
首先來(lái)看編譯時(shí),考慮如下App
組件代碼:
<h1>{count}</h1> <script> let count = 0; </script>
這段代碼經(jīng)由編譯器編譯后產(chǎn)生如下代碼,包括三部分:
create_fragment
方法
count
的聲明語(yǔ)句
class App
的聲明語(yǔ)句
// 省略部分代碼… 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
首先來(lái)看create_fragment
方法,他是編譯器根據(jù)App
的UI
編譯而成,提供該組件與瀏覽器交互的方法,在上述編譯結(jié)果中,包含3個(gè)方法:
c
,代表create
,用于根據(jù)模版內(nèi)容,創(chuàng)建對(duì)應(yīng)DOM Element
。例子中創(chuàng)建H1
對(duì)應(yīng)DOM Element
:
h1 = element("h1"); h1.textContent = `${count}`;
m
,代表mount
,用于將c
創(chuàng)建的DOM Element
插入頁(yè)面,完成組件首次渲染。例子中會(huì)將H1
插入頁(yè)面:
insert(target, h1, anchor);
insert
方法會(huì)調(diào)用target.insertBefore
:
function insert(target, node, anchor) { target.insertBefore(node, anchor || null); }
d
,代表detach
,用于將組件對(duì)應(yīng)DOM Element
從頁(yè)面中移除。例子中會(huì)移除H1
:
if (detaching) detach(h1);
detach
方法會(huì)調(diào)用parentNode.removeChild
:
function detach(node) { node.parentNode.removeChild(node); }
仔細(xì)觀察流程圖,會(huì)發(fā)現(xiàn)App
組件編譯的產(chǎn)物沒(méi)有圖中fragment
內(nèi)的p
方法。
這是因?yàn)?code>App沒(méi)有變化狀態(tài)的邏輯,所以相應(yīng)方法不會(huì)出現(xiàn)在編譯產(chǎn)物中。
可以發(fā)現(xiàn),create_fragment
返回的c
、m
方法用于組件首次渲染。那么是誰(shuí)調(diào)用這些方法呢?
SvelteComponent
每個(gè)組件對(duì)應(yīng)一個(gè)繼承自SvelteComponent
的class
,實(shí)例化時(shí)會(huì)調(diào)用init
方法完成組件初始化,create_fragment
會(huì)在init
中調(diào)用:
class App extends SvelteComponent { constructor(options) { super(); init(this, options, null, create_fragment, safe_not_equal, {}); } }
總結(jié)一下,流程圖中虛線部分在Demo1
中的編譯結(jié)果為:
fragment
:編譯為create_fragment
方法的返回值
UI
:create_fragment
返回值中m
方法的執(zhí)行結(jié)果
ctx
:代表組件的上下文,由于例子中只包含一個(gè)不會(huì)改變的狀態(tài)count
,所以ctx
就是count
的聲明語(yǔ)句
可以改變狀態(tài)的Demo
現(xiàn)在修改Demo
,增加update
方法,為H1
綁定點(diǎn)擊事件,點(diǎn)擊后count
改變:
<h1 on:click="{update}">{count}</h1> <script> let count = 0; function update() { count++; } </script>
編譯產(chǎn)物發(fā)生變化,ctx
的變化如下:
// 從module頂層的聲明語(yǔ)句 let count = 0; // 變?yōu)閕nstance方法 function instance($$self, $$props, $$invalidate) { let count = 0; function update() { $$invalidate(0, count++, count); } return [count, update]; }
count
從module
頂層的聲明語(yǔ)句變?yōu)?code>instance方法內(nèi)的變量。之所以產(chǎn)生如此變化是因?yàn)?code>App可以實(shí)例化多個(gè):
// 模版中定義3個(gè)App <App/> <App/> <App/> // 當(dāng)count不可變時(shí),頁(yè)面渲染為:<h1>0</h1> <h1>0</h1> <h1>0</h1>
當(dāng)count
不可變時(shí),所有App
可以復(fù)用同一個(gè)count
。但是當(dāng)count
可變時(shí),根據(jù)不同App
被點(diǎn)擊次數(shù)不同,頁(yè)面可能渲染為:
<h1>0</h1> <h1>3</h1> <h1>1</h1>
所以每個(gè)App
需要有獨(dú)立的上下文保存count
,這就是instance
方法的意義。推廣來(lái)說(shuō),Svelte
編譯器會(huì)追蹤<script>
內(nèi)所有變量聲明:
- 是否包含改變?cè)撟兞康恼Z(yǔ)句,比如
count++
- 是否包含重新賦值的語(yǔ)句,比如
count = 1
- 等等情況
一旦發(fā)現(xiàn),就會(huì)將該變量提取到instance
中,instance
執(zhí)行后的返回值就是組件對(duì)應(yīng)ctx
。
同時(shí),如果執(zhí)行如上操作的語(yǔ)句可以通過(guò)模版被引用,則該語(yǔ)句會(huì)被$$invalidate
包裹。
在Demo2
中,update
方法滿足:
- 包含改變
count
的語(yǔ)句 ——?count++
- 可以通過(guò)模版被引用 —— 作為點(diǎn)擊回調(diào)函數(shù)
所以編譯后的update
內(nèi)改變count
的語(yǔ)句被$$invalidate
方法包裹:
// 源代碼中的update function update() { count++; } // 編譯后instance中的update function update() { $$invalidate(0, count++, count); }
- 更新
ctx
中保存狀態(tài)的值,比如Demo2
中count++
- 標(biāo)記
dirty
,即標(biāo)記App UI
中所有和count
相關(guān)的部分將會(huì)發(fā)生變化 - 調(diào)度更新,在
microtask
中調(diào)度本次更新,所有在同一個(gè)macrotask
中執(zhí)行的$$invalidate
都會(huì)在該macrotask
執(zhí)行完成后被統(tǒng)一執(zhí)行,最終會(huì)執(zhí)行組件fragment
中的p
方法
p
方法是Demo2
中新的編譯產(chǎn)物,除了p
之外,create_fragment
已有的方法也產(chǎn)生相應(yīng)變化:
c() { h1 = element("h1"); // count的值變?yōu)閺腸tx中獲取 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會(huì)更新t保存的文本節(jié)點(diǎn) if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]); }, d(detaching) { if (detaching) detach(h1); // 事件解綁 dispose(); }
p
方法會(huì)執(zhí)行$$invalidate
中標(biāo)記為dirty
的項(xiàng)對(duì)應(yīng)的更新函數(shù)。
在Demo2
中,App UI
中只引用了狀態(tài)count
,所以update
方法中只有一個(gè)if
語(yǔ)句,如果UI
中引用了多個(gè)狀態(tài),則p
方法中也會(huì)包含多個(gè)if
語(yǔ)句:
// UI中引用多個(gè)狀態(tài) <h1 on:click="{count0++}">{count0}</h1> <h1 on:click="{count1++}">{count1}</h1> <h1 on:click="{count2++}">{count2}</h1>
對(duì)應(yīng)p
方法包含多個(gè)if
語(yǔ)句:
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
完整的更新步驟如下:
- 點(diǎn)擊
H1
觸發(fā)回調(diào)函數(shù)update
update
內(nèi)調(diào)用$$invalidate
,更新ctx
中的count
,標(biāo)記count
為dirty
,調(diào)度更新- 執(zhí)行
p
方法,進(jìn)入dirty
的項(xiàng)(即count
)對(duì)應(yīng)if
語(yǔ)句,執(zhí)行更新對(duì)應(yīng)DOM Element
的方法
以上就是JavaScript開(kāi)發(fā)Svelte實(shí)現(xiàn)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于Svelte實(shí)現(xiàn)原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
UniApp使用manifest.json應(yīng)用配置的超詳細(xì)教學(xué)
這篇文章主要給大家介紹了關(guān)于uni-app應(yīng)用配置manifest.json最全最詳細(xì)配置,manifest.json文件是應(yīng)用的配置文件,用于指定應(yīng)用的名稱、圖標(biāo)、權(quán)限等,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01JS網(wǎng)頁(yè)圖片按比例自適應(yīng)縮放實(shí)現(xiàn)方法
這篇文章主要介紹了JS網(wǎng)頁(yè)圖片按比例自適應(yīng)縮放實(shí)現(xiàn)方法,有需要的朋友可以參考一下2014-01-01JavaScript算法學(xué)習(xí)之冒泡排序和選擇排序
這篇文章主要給大家介紹了關(guān)于JavaScript算法學(xué)習(xí)之冒泡排序和選擇排序的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用JavaScript具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11一個(gè)JavaScript獲取元素當(dāng)前高度的實(shí)例
這篇文章主要為大家介紹了一個(gè)JavaScript獲取元素當(dāng)前高度的實(shí)例,比較實(shí)用,建議新手朋友們可以看看2014-10-10使用JavaScript實(shí)現(xiàn)二值化圖像
這篇文章主要為大家詳細(xì)介紹了使用JavaScript將圖像轉(zhuǎn)換為黑白二值圖的兩種方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01