Svelte筆記三:runtime源碼解讀

svelte的源碼很簡單是由兩大部分組成,compiler和runtime。

compiler就是一個(gè)編譯器將svelte模版語法轉(zhuǎn)換為瀏覽器能夠識(shí)別的代碼。而runtime則是在瀏覽器中幫助業(yè)務(wù)代碼運(yùn)作的運(yùn)行時(shí)函數(shù)。所以說runtime是svelte框架最核心的部分,它也解釋了svelte是如何在沒有virtual dom的情況下也照樣運(yùn)行的。今天我們r(jià)eview一下runtime代碼。

svelte的runtime主要由fragment和component組成,而component是包含了fragment。它們有著獨(dú)立的生命周期,將邏輯層和渲染層分離。

Fragment

Svelte官方example提供了compile出來的Js output。這些output就是運(yùn)行在瀏覽器的源碼,根據(jù)內(nèi)容知道svelte的基本運(yùn)作,讓開發(fā)者很清除它內(nèi)部的每一步運(yùn)作。下面這個(gè)栗子很簡單,就是對(duì)hello的一個(gè)字符串插值。而name是一個(gè)變量。

<script>
    let name = 'world';
</script>

<h1>Hello {name}!</h1>

編譯出來的結(jié)果:

/* App.svelte generated by Svelte v3.24.0 */
import {
    SvelteComponent,
    detach,
    element,
    init,
    insert,
    noop,
    safe_not_equal
} from "svelte/internal";

function create_fragment(ctx) {
    let h1;

    return {
        c() {
            h1 = element("h1");
            h1.textContent = `Hello ${name}!`;
        },
        m(target, anchor) {
            insert(target, h1, anchor);
        },
        p: noop,
        i: noop,
        o: noop,
        d(detaching) {
            if (detaching) detach(h1);
        }
    };
}

let name = "world";

class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, null, create_fragment, safe_not_equal, {});
    }
}

export default App;

編譯出來的結(jié)果就是有一個(gè)初始化函數(shù),叫create_fragment,它是用于dom的初始掛載。它使用了element函數(shù),通過查閱源碼src/runtime/internal/dom,我們知道它的作用就是用來創(chuàng)建h1標(biāo)簽實(shí)例,并且填入可變內(nèi)容。除了element之外,還有spacetext,svg_element等都是用于生成真實(shí)dom,分別是對(duì)空格,純文本,svg進(jìn)行生成處理。

export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
    return document.createElement<K>(name);
}

export function text(data: string) {
    return document.createTextNode(data);
}

export function space() {
    return text(' ');
}

create_fragment的過程還包含有c,m,p,i,o,d等特殊名稱的函數(shù),這些函數(shù)并非編譯混淆,而是Fragment內(nèi)部的生命周期縮寫。Fragment指得是真實(shí)dom的節(jié)點(diǎn),它擁有著獨(dú)立的生命周期和屬性。源碼中src/runtime/internal/Component介紹了它的定義,它是一個(gè)真實(shí)的dom元素集合,它的屬性并非組件屬性(如下方ts類型定義),分別包含了create,claim,hydrate,mount,update,mesure,fix,animate,intro,outro,destory,組件的真實(shí)變化會(huì)影響Fragment的變化,F(xiàn)ragment的變化影響真實(shí)的dom,從上面例子看在create的過程中它創(chuàng)建了h1標(biāo)簽,在mount的過程將剛才創(chuàng)建的h1掛載到頁面中,在update的過程沒有任何操作,在detach的過程銷毀該Fragment。

interface Fragment {
    key: string|null;
    first: null;
    /* create  */ c: () => void;
    /* claim   */ l: (nodes: any) => void;
    /* hydrate */ h: () => void;
    /* mount   */ m: (target: HTMLElement, anchor: any) => void;
    /* update  */ p: (ctx: any, dirty: any) => void;
    /* measure */ r: () => void;
    /* fix     */ f: () => void;
    /* animate */ a: () => void;
    /* intro   */ i: (local: any) => void;
    /* outro   */ o: (local: any) => void;
    /* destroy */ d: (detaching: 0|1) => void;
}

Component

SvelteComponent則是包含了svelte組件內(nèi)置的屬性和生命周期,它們與Fragment的屬性和生命周期是息息相關(guān),SvelteComponent是依賴于Fragment,組件的變化會(huì)觸發(fā)Fragment的變化。它是一個(gè)相輔相成的組合。源碼中還有SvelteComponent和SvelteElement的細(xì)分,不同點(diǎn)在于Web Component的組件的支持,這里就不再展開。

Component擁有四個(gè)生命周期,分別是mount,beforeUpdate, afterUpdate,destory。沒有create階段是因?yàn)閟velte沒有virtual dom。所以在組件層面,它沒有像vue那么復(fù)雜。

數(shù)據(jù)流

react的單向數(shù)據(jù)流,vue的雙向綁定,那么svelte是怎么樣實(shí)現(xiàn)數(shù)據(jù)流的呢?

下面是我們業(yè)務(wù)中經(jīng)常見到的代碼,點(diǎn)擊按鈕請(qǐng)求數(shù)據(jù)然后設(shè)置到變量,觸發(fā)dom內(nèi)容的變化。svelte的寫法形似vue的寫法,但是它的runtime原理并沒有雙向綁定。編譯后的代碼除了有上面所說的create和mount等fragment生命周期屬性外,其他代碼更多表現(xiàn)了數(shù)據(jù)流的形式。

<script>
    let num = 1;

    async function handleClick() {
        const res = await fetch(`tutorial/random-number`);
        const text = await res.text();

        if (res.ok) {
            num = text;
            return text;
        } else {
            throw new Error(text);
        }
    }
</script>

<button on:click={handleClick}>
    generate random number
</button>


<p>The number is {num}</p>

編譯出來的結(jié)果:

/* App.svelte generated by Svelte v3.24.0 */
import {
    SvelteComponent,
    append,
    detach,
    element,
    init,
    insert,
    listen,
    noop,
    safe_not_equal,
    set_data,
    space,
    text
} from "svelte/internal";

function create_fragment(ctx) {
    let button;
    let t1;
    let p;
    let t2;
    let t3;
    let mounted;
    let dispose;

    return {
        c() {
            button = element("button");
            button.textContent = "generate random number";
            t1 = space();
            p = element("p");
            t2 = text("The number is ");
            t3 = text(/*num*/ ctx[0]);
        },
        m(target, anchor) {
            insert(target, button, anchor);
            insert(target, t1, anchor);
            insert(target, p, anchor);
            append(p, t2);
            append(p, t3);

            if (!mounted) {
                dispose = listen(button, "click", /*handleClick*/ ctx[1]);
                mounted = true;
            }
        },
        p(ctx, [dirty]) {
            if (dirty & /*num*/ 1) set_data(t3, /*num*/ ctx[0]);
        },
        i: noop,
        o: noop,
        d(detaching) {
            if (detaching) detach(button);
            if (detaching) detach(t1);
            if (detaching) detach(p);
            mounted = false;
            dispose();
        }
    };
}

function instance($$self, $$props, $$invalidate) {
    let num = 1;

    async function handleClick() {
        const res = await fetch(`tutorial/random-number`);
        const text = await res.text();

        if (res.ok) {
            $$invalidate(0, num = text);
            return text;
        } else {
            throw new Error(text);
        }
    }

    return [num, handleClick];
}

class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, instance, create_fragment, safe_not_equal, {});
    }
}

export default App;

代碼中,handleClick函數(shù)被封裝在一個(gè)名為instance的方法當(dāng)中,而它的入?yún)?dāng)中有個(gè)$$invalidate的回調(diào)函數(shù),用于變量的設(shè)置,把接口異步獲取的數(shù)據(jù)設(shè)置回調(diào)函數(shù)當(dāng)中。而它在組件的調(diào)用如下,重點(diǎn)在于回調(diào)函數(shù)當(dāng)中,instance只會(huì)在初始化的時(shí)候調(diào)用,但是回調(diào)函數(shù)$$invalidate可以在各種異步情況調(diào)用。它會(huì)觸發(fā)make_dirty的方法,而它觸發(fā)了schedule_update,在一個(gè)微任務(wù)當(dāng)中,觸發(fā)flush將一段時(shí)間內(nèi)的變量操作都執(zhí)行掉。實(shí)現(xiàn)變量的處理,flush函數(shù)的具體實(shí)現(xiàn)請(qǐng)查看源碼(src/runtime/internal/Component.ts)。flush的過程中會(huì)觸發(fā)Fragment的update以及Component的update。

    $$.ctx = instance
        ? instance(component, prop_values, (i, ret, ...rest) => {
            const value = rest.length ? rest[0] : ret;
            if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
                if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
                if (ready) make_dirty(component, i);
            }
            return ret;
        })
        : [];

由此可見,svelte是單向數(shù)據(jù)流,很多數(shù)據(jù)工作已經(jīng)在compile的過程當(dāng)中已經(jīng)完成。runtime更多是服務(wù)于瀏覽器層面的數(shù)據(jù)流轉(zhuǎn)化。

題外話

shopee,又稱蝦皮,是一家騰訊投資的跨境電商平臺(tái)。這里加班少,技術(shù)氛圍好。如果想和我并肩作戰(zhàn)一起學(xué)習(xí),可以找我內(nèi)推。郵箱weiping.xiang@shopee.com,非誠勿擾。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容