R·ex / Zeng


音遊狗、安全狗、攻城獅、業餘設計師、段子手、苦學日語的少年。

從 Svelte 3 的編譯器中得到的啟示

前端的編譯器,除了可以做轉換,是否還有其它用途?

由於現在國內“前端主流框架”的稱號基本被 React、Vue、Angular 佔領,前二者在渲染層的技術,本質上都是極為相似的:在記憶體中維護 Virtual DOM,透過某些方式獲取到變數被修改,然後利用 DOM Diff 演算法(以及一些最佳化)計算出需要對 DOM 做的操作,再 Patch 到真實的 DOM 上。相信大家(尤其是準備過面試的小夥伴)一定對這些內容比較瞭解。

Angular 的最新版使用了一個叫 Ivy 的引擎。對於 Angular 來說,髒值檢測跟之前的方法類似(監聽使用者輸入、劫持 setTimeout 等),渲染方面則是用了 incremental-dom 的思想做增量渲染。

我第一次聽說 Svelte(讀作 [svɛlt]),也只是在兩個月前。我剛看到 Svelte 3 文章時,就被“編譯型框架”的字樣吸引住了。據說它的編譯器幫忙做了很多工作,可以不用髒值檢測、setState、Proxy 等方式來觸發檢視更新,也沒有了 DOM Diff 的代價。我決定好好研究一番。

幾種前端框架的資料響應原理

Svelte 3 的特點

在翻閱 Svelte 的部落格時,很容易翻到這篇文章:Virtual DOM is pure overhead。通讀下來,作者的觀點是,Virtual DOM 可以做到狀態驅動 UI,效能也不差,並且程式碼的 Bug 更少;但 Svelte 不用 Virtual DOM 也可以做到類似的效果,並且效能更高。

模板語法、模板資料分離

本文的重點不是詳細講述 Svelte 3 的語法(照著文件來就可以了),這裡只舉一些最常用也最簡單的例子,一個元件基本長這樣,跟 Vue 的元件長得差不多,但有點區別。我列舉了變數、雙向繫結、事件處理、Watch、常見的控制語句、生命週期這幾個特性:

<!-- App.svelte -->

<script lang="ts">
import { onMount } from 'svelte';

// 1:變數、雙向繫結、事件處理
let name = 'world';
const onClick = () => name = 'Rex';

// 2:`if` 塊、Watch
let x = 0;
const incX = () => x++;
// 這就像 Vue 中的 `watch` 功能
// 語法很奇怪,但是是合法的
$: doubleX = x * 2;

// 3:`each` 塊
let characters = [
    { name: 'Aroma White' },
    { name: 'Neko Asakura' },
    { name: 'ROBO Head' }
];
const append = (name: string) => {
    if (!characters.find(t => t.name === name)) {
        characters = [...characters, { name }];
    }
};

// 生命週期
onMount(async () => {
    const res = await fetch(`/photos`);
    const photos = await res.json();
    console.log(photos);
});
</script>

<main>
    <section class="my-section-1">
        <h1>Hello {name}!</h1>
        <input bind:value={name}>
        <button on:click={onClick}>I'm Rex</button>
    </section>
    <section class="my-section-2">
        {#if x > 10}
            <p>{x} is greater than 10</p>
        {:else if x < 5}
            <p>{x} is less than 5</p>
        {:else}
            <p>{x} is between 5 and 10</p>
        {/if}
        <p>Double x is {doubleX}</p>
        <button on:click={incX}>x++</button>
    </section>
    <section class="my-section-3">
        <button on:click={() => append('Simon Jackson')}>
            Add Simon
        </button>
        <ul>
            {#each characters as c, i}
                <li>{i + 1}: {c.name}</li>
            {/each}
        </ul>
    </section>
</main>

<style>
main {
    font-size: 18px;
}
</style>

單看 <main> 的部分,是不是感覺找回了上古時期寫 PHP 和 Django 模板的感覺?雖然語法比較復古,但其實現代前端支援的東西,它基本都能做:

  • 元件化:每個 .svelte 檔案就是一個元件,並且也完整支援 Props、Context、Slot;
  • 完整的生命週期:onMountonDestroybeforeUpdateafterUpdate

利用編譯器實現響應式

那麼它是怎麼做的呢?記得我之前年少輕狂寫 rsjs 的時候(那時候的主流還是 Angular 1),由於水平太菜,遲遲不知道如何做到 Angular 的髒值檢測,於是只好退而求其次,搞了個 setData 函式,並且每次資料更新都是完整渲染,可以說是效率很低了。這其實跟 React 更貼近一些……

其實 Svelte 以前也是這樣做的,但 Svelte 3: Rethinking reactivity 這篇文章的意思是,由於 Svelte 3 在編譯期間就可以知道每個變數在模板的什麼地方被用到,因此只要變數有被模板用到,編譯器就會在該變數的所有“直接賦值”的語句外面包一個 $$invalidate 函式,例如這樣:

// 原始程式碼:
count += 1;
// 會被編譯成這樣的形式:
$$invalidate('count', count += 1);

至於 Array.prototype.push 之類的函式,以及修改引用值,Svelte 沒有像 Vue 那樣劫持原生函式,也沒有在編譯器中做引用判斷,因此需要開發者變通一下,轉換成對變數直接賦值的語句。鑑於 React 也是這樣的要求(每次 setState 都需要設定一個新物件),因此這一點我覺得是可以接受的。

區域性更新和直接操作 DOM

由於 Svelte 3 使用的是類似上古時期的模板語法,沒有 TSX Render Function 這麼靈活的功能,因此可以更好地被用作靜態分析。

我我把剛剛的程式碼編譯了一下(詳見彩蛋),在未壓縮的模式下,很容易找出每個函式和變數在原始碼中的位置。大概看了一眼,能得出這樣的結論:

  • 資料會被統一打包到一個數組中方便管理,類似 React Hooks 的線性儲存方式;
  • 模板被轉換成渲染函式,每個 if 和 each 塊也被轉換成渲染函式,方便區域性更新;
  • 對於每個渲染函式,有 Create、Make Tree、Patch、Detach 四個操作;
  • 沒有 Virtual DOM,而是為每個塊定製 Create 和 Make Tree 操作,直接修改 DOM;
  • Vue 透過 Proxy 來實現的監聽,在 Svelte 3 中成了為每個塊定製的 Patch 操作;
  • 在解除安裝的時候,會先解除安裝元件,再執行清理函式(取消監聽器之類的)。

其它

對 Svelte 3 的更具體的分析,由於網上已經有人寫了(詳見文末的參考資料),這裡就不多提了。

我從中得到的啟示

1. Virtual DOM 並不是唯一出路

由於我和我的前端朋友們幾乎全都是寫 React 和 Vue 的,因此我們很自然地認為 Virtual DOM、TSX 等方式已經足夠了。但是 Svelte 3 跟快被我們忽略的 Angular,堅持採用模板的方式,沒有擁抱 Virtual DOM,並且也取得了不錯的成果。這說明 Virtual DOM 並不是唯一出路。

個人覺得,Angular 現在之所以沒有成為主流,是因為它是一整套服務,上手難度高,而非因為沒有擁抱 Virtual DOM。

2. 無論如何編碼,總會有 Tradeoff

我們應該聽到過很多“最佳實踐”,或者是“錯誤的寫法”,例如:

  • 在 React 中,不管是 setState 還是 Reducer,每次必須返回一個新的物件;
  • 在 React Hooks 中,一個 Hooks 必須只能寫在函式式元件裡或另一個 Hooks 裡;
  • 在 Vue 的 data 中必須要宣告所有用到的欄位,否則必須要用 $set 來更新;
  • 在 Vue 的模板中,不能直接使用模組內變數或全域性變數,需要透過 data 傳過去;
  • 在 immerjs 中,不能認為 a=b 之後修改 a 物件就是修改 b,它倆不是同一個東西。

這些都是為了實現庫或框架的功能而必須的 Tradeoff,因為過度自由的編碼,會導致你的程式碼無法透過統一的演算法來檢測和最佳化,也無法讓你的程式碼更加工程化。舉個例子,我在寫面單編輯器的時候針對業務資料做了一些限制,否則不可能一個人在一週的時間內快速完成;TypeScript 的程式碼可達性檢測也不能檢測停機問題。

Svelte 3 針對編譯器的功能,限制了使用者使用 Render Function 以及 Virtual DOM,以及不能對需要響應變化的變數用 push 這些可以原地操作資料的函式(物件的深層次賦值是可以的),雖然會失去一定的靈活性,但可以讓編譯器幫你做更多的事情。

時間、空間、程式設計複雜度往往互相牽制

3. 編譯器能做的事情,比我認為的要多

其實這一點,搞二進位制開發、逆向的大佬們感觸應該會更深。例如:

  • C 語言程式開始執行後並不會先進入 main 函式,而是一個叫做 _start 的函式;
  • 上古時期 x * 9 會被開發者寫成 x << 3 + x,但現在大部分最佳化都被編譯器做了(而且絕對比你手工最佳化做的好);
  • 事實上,C 編譯的過程就是把支援了很多特性的高階語言,轉換成只支援基本指令的機器語言,原始碼和編譯產物往往很不相似(這也是為什麼 C 逆向比 JS 逆向難得多)。

前端編譯器最開始的作用,無非是轉換一些語法、新增一些 Runtime;後來逐漸出現了各種 Webpack Loader 和 Rollup 外掛,支援了更多的檔案格式(.vue 檔案轉換前後整個結構就變了),這也在某種程度上反映出前端在複雜度和工程化程度上的提升。這些東西也成了一些公司面試的必考點,用來區分普通程式設計師和高階工程師。

這次 Svelte 3 的編譯器,在保留了“響應式前端”概念的基礎之上,進一步讓程式碼得到了簡化,並且也極大減少了執行時程式碼的執行,為世界的碳排放做出了一定的貢獻。(咦?)

我有一個小預測:隨著前端複雜度和工程化程度的進一步提升,未來的編譯器一定可以幫我們做更多的事情,現有的框架可能也會將一部分在執行時的功能使用編譯器輔助實現。由於編譯器的功能越來越多,面試的要點會逐漸從框架的原理,變成編譯器的原理。

4. 最佳化除了提高演算法效率,還有預處理

在以前當賽棍的時候,經常會提到兩個概念:時間複雜度、空間複雜度。利用空間換時間的技術一般被叫做“預處理”,即提前計算好一些東西,用的時候直接呼叫。

谷歌出的一個程式碼壓縮器 GCC(Google Closure Compiler),可以幫忙做一些 Inline 的操作,以及提前計算,避免在執行的過程中重複取值。不過一些最佳化過於激進,可能會導致程式碼無法執行,加上後來出現的 Terser 等工具做的確實不錯,因此 GCC 沒有被廣泛使用。

而 Svelte 3 的做法,則是利用編譯器直接計算出每個值改變時需要做什麼,相當於直接把髒值檢測跟 DOM Diff 計算後的執行流程寫死到了元件的程式碼裡,免去了在執行時計算的過程。雖然程式碼會比較長,但由於重複操作多,資訊量小,因此壓縮後並不會佔用太多的空間。這是個既省空間又省時間的做法。

P.S. 三年前我面試 Shopee 的時候,老闆問了我一個主觀問題(之前的文章可以看 這裡):

如果單從修改 DOM 結構來看,React + DOM Diff 演算法與純 jQuery 直接操作(假設已經知道該如何最快的 Patch)相比,哪個效率比較高?

我的答案是:當結構不復雜的時候,DOM Diff 的耗時佔比重是不能被忽略的,此時當然是 jQuery 快;但是當結構複雜了之後,DOM Diff 的耗時佔比重就比較低了,更多的時間被消耗在 DOM 的操作上——React 是直接 Patch 到 DOM 上,但 jQuery 是在原生 DOM 上面封裝了一層,所以會再慢一些,這個的時間是要多於 DOM Diff 的。

當時我並不知道標準答案是什麼,現在看前端的趨勢也沒必要再考慮 jQuery 了。但如果再加上 Svelte 3,問題就有意思起來了。Svelte 由於把預處理的時間放到了編譯期間,在執行時也只對 DOM 操作做了最簡單的封裝,因此既沒有 React 的 DOM Diff 時間,又比 jQuery 要快。

究竟封裝有多簡單呢?可以看一下 Svelte 3 的工具函式,基本相當於沒有封裝啊……估計是為了以後新增更多的鉤子吧:

function append(target, node) {
    target.appendChild(node);
}
function insert(target, node, anchor) {
    target.insertBefore(node, anchor || null);
}
function detach(node) {
    node.parentNode.removeChild(node);
}
function element(name) {
    return document.createElement(name);
}
function text(data) {
    return document.createTextNode(data);
}
function space() {
    return text(' ');
}
function listen(node, event, handler, options) {
    node.addEventListener(event, handler, options);
    return () => node.removeEventListener(event, handler, options);
}
function attr(node, attribute, value) {
    if (value == null)
        node.removeAttribute(attribute);
    else if (node.getAttribute(attribute) !== value)
        node.setAttribute(attribute, value);
}
function children(element) {
    return Array.from(element.childNodes);
}
function set_input_value(input, value) {
    input.value = value == null ? '' : value;
}

彩蛋

到底該信哪一份資料

我在統計框架使用量的時候,一開始認為開發者提的問題越多,代表這個框架越火。根據 StackOverflow 在 2020 年對接近 65000 個開發者的調查(傳送門),可以發現最主流的幾個框架依次是 Angular/Angular.js、React、Vue。這跟我的認知不符,因為在國內我幾乎沒有遇到多少 Angular 的開發者,反而因為 Vue 的上手難度低,導致中小型公司更喜歡用 Vue。

來自 StackOverflow 的統計資料

我在想,可能是因為倖存者偏差,可以再看看 NPM 上面的下載量。於是我用 npm-stat 統計了一下三大框架在 2020 年的下載量,結果是這樣的:

來自 npm-stat 的統計資料

可以看到 React 基本佔了主流,跟 Angular 簡直是數量級的差距。

但是 NPM 的下載量就一定能說明結果嗎?是否有可能因為 Angular 的使用者由於構建最佳化的更好,避免了每次流水線都從 NPM 下載?我不瞭解,也不知道從何瞭解。

編譯結果究竟長啥樣

答案:長這樣,長相併不好看。(攤手)

我已經儘可能添加註釋幫助理解了,如果不願意看程式碼,把註釋當文章來讀也是可以的。

// 一堆通用的工具函式,用來建立 DOM、設定資料等
import { ... } from "svelte/internal";
import { onMount } from "svelte";

// 一個取決於元件資料的工具函式
// 用於找到 `each` 塊中的 Context
function get_each_context(ctx, list, i) {
    const child_ctx = ctx.slice();
    child_ctx[9] = list[i];
    child_ctx[11] = i;
    return child_ctx;
}

// 第 48 行的 else 塊,主要包含了四個函式
function create_else_block(ctx) {
    // 這些是每一個 DOM 元素
    let p;
    let t0;
    let t1;

    return {
        // c:建立(Create)
        // 呼叫工具函式,依次建立每一個 DOM 元素
        c() {
            p = element("p");
            // 這裡 ctx[0] 就對應了 x,編譯器的註釋標的恰到好處
            t0 = text(/*x*/ ctx[0]);
            t1 = text(" is between 5 and 10");
        },
        // m:建樹(Make Tree)
        // 透過建立好的 DOM 元素,將 DOM 樹構建好
        m(target, anchor) {
            insert(target, p, anchor);
            append_1(p, t0);
            append_1(p, t1);
        },
        // p:更新(Patch)
        // 在這個 else 塊中,只依賴了一個 x,因此只有一個 if 語句
        // 若元件被標記為 dirty,且 x 被改
        // 則呼叫工具函式 set_data 直接更新 DOM 元素中的 innerText
        // Svelte 將“某變數被修改”的標記用位運算做了壓縮,詳見參考資料
        p(ctx, dirty) {
            if (dirty & /*x*/ 1) set_data(t0, /*x*/ ctx[0]);
        },
        // d:解除安裝(Detach)
        // 當元件被解除安裝時需要做的事情,一般是解除安裝元素
        // 如果有事件監聽的話也會取消監聽
        d(detaching) {
            if (detaching) detach(p);
        }
    };
}

// 接下來的兩個函式分別是:
// 第 46 行的 else 塊
// 第 44 行的 if 塊
// 裡面長得跟剛才基本一樣,不多提了
function create_if_block_1(ctx) { ... }
function create_if_block(ctx) { ... }

// 第 59 行的 each 塊
// 稍微有一點區別,因為有 key
// 並且模板渲染用到的資料不是 JS 的基本型別
function create_each_block(ctx) {
    let li;
    // 因為在模板中我們寫了 i + 1
    let t0_value = /*i*/ ctx[11] + 1 + "";
    let t0;
    let t1;
    // 因為在模板中我們寫了 c.name
    let t2_value = /*c*/ ctx[9].name + "";
    let t2;

    return {
        c() {
            li = element("li");
            // 這裡直接呼叫 i + 1 的值
            t0 = text(t0_value);
            t1 = text(": ");
            // 這裡直接呼叫了 c.name 的值
            t2 = text(t2_value);
        },
        m(target, anchor) { ... },
        p(ctx, dirty) {
            // 這裡除了判斷元件是 dirty 和 characters 被修改以外
            // 還多判斷了一步 characters[i].name 是否被修改
            if (
                dirty & /*characters*/ 4 &&
                t2_value !== (t2_value = /*c*/ ctx[9].name + "")
            ) {
                set_data(t2, t2_value);
            }
        },
        d(detaching) { ... }
    };
}

// 整個元件的建立函式
function create_fragment(ctx) {
    // 依舊是各種 DOM 元素
    // ...為了簡潔,我只保留了 main,省略其它所有變數
    let main;
    // 這個用來判斷元件是否已經掛載結束
    let mounted;
    // 這個用來收集在解除安裝時需要做的事情,是一個數組
    // Svelte 的監聽器跟 Angular、React.useEffect 類似
    // 都是返回一個解除安裝函式
    let dispose;

    // 第 44 行的 if 塊,在這裡被轉換成了真正的 JS
    // 這裡判斷該使用哪個渲染函式來渲染 if 塊
    function select_block_type(ctx, dirty) {
        if (/*x*/ ctx[0] > 10) return create_if_block;
        if (/*x*/ ctx[0] < 5) return create_if_block_1;
        return create_else_block;
    }

    // 第 44 行的 if 塊,需要用這個渲染函式來渲染
    let current_block_type = select_block_type(ctx, -1);
    let if_block = current_block_type(ctx);

    // 第 59 行的 each 塊,需要轉換為若干個渲染函式
    // 每個函式都只負責渲染其中的一次迭代
    let each_value = /*characters*/ ctx[2];
    let each_blocks = [];
    for (let i = 0; i < each_value.length; i += 1) {
        each_blocks[i] = create_each_block(
            get_each_context(ctx, each_value, i)
        );
    }

    // 跟剛才類似的幾個函式,只不過複雜了很多
    return {
        // 這裡的 c 裡面除了建立基本的元素以外
        // 還要呼叫剛剛 if、each 相關的 c 函式
        c() {
            main = element("main"); // ...省略其它
            // 這裡呼叫了 if 塊的 c 函式
            if_block.c();
            t7 = space(); // ...省略其它

            // 這裡呼叫了 each 陣列中每個元素的 c 函式
            for (let i = 0; i < each_blocks.length; i += 1) {
                each_blocks[i].c();
            }

            // 為剛建立好的 DOM 元素設定屬性
            attr(section0, "class", "my-section-1");
            attr(section1, "class", "my-section-2");
            attr(section2, "class", "my-section-3");
            // 元件自身的 Scoped CSS 在編譯階段即完成
            attr(main, "class", "svelte-5if2as");
        },
        // 跟之前類似,利用建立好的 DOM 元素,構建 DOM 樹結構
        // 這裡也會呼叫每個 if、each 塊中的 m 函式
        m(target, anchor) {
            insert(target, main, anchor); // ...省略其它
            // 模板中的 input 有個雙向繫結
            set_input_value(input, /*name*/ ctx[1]);
            append_1(section0, t4); // ...省略其它

            for (let i = 0; i < each_blocks.length; i += 1) {
                each_blocks[i].m(ul, null);
            }

            // 執行到這裡,其實已經掛載完成了
            // 收集一下解除安裝時需要執行的函式,然後標記為 mounted
            if (!mounted) {
                dispose = [
                    listen(input, "input", /*input_input_handler*/ ctx[7]),
                    listen(button0, "click", /*onClick*/ ctx[4]),
                    listen(button1, "click", /*incX*/ ctx[5]),
                    listen(button2, "click", /*click_handler*/ ctx[8])
                ];
                mounted = true;
            }
        },
        // 需要 Patch 的變數變得多了起來
        // 可以看出來,這是依據元件內容生成的程式碼
        // 隨著元件中用到的變數增多,這個函式會變得越來越大
        p(ctx, [dirty]) {
            if (dirty & /*name*/ 2) set_data(t1, /*name*/ ctx[1]);

            // 這裡是雙向繫結
            if (dirty & /*name*/ 2 && input.value !== /*name*/ ctx[1]) {
                set_input_value(input, /*name*/ ctx[1]);
            }

            // ...省略其它
        },
        i: noop,
        o: noop,
        // 在解除安裝的時候:
        // 先解除安裝元件
        // 再解除安裝所有塊
        // 最後執行之前收集的清理函式
        d(detaching) {
            if (detaching) detach(main);
            if_block.d();
            destroy_each(each_blocks, detaching);
            mounted = false;
            run_all(dispose);
        }
    };
}

// 這一段是原始碼中的 <script> 部分
// 編譯器很貼心,把我在原始碼中寫的註釋都給保留了
function instance($$self, $$props, $$invalidate) {
    let doubleX;
    let name = "world";
    const onClick = () => $$invalidate(1, name = "Rex");

    // 2:`if` 塊、Watch
    let x = 0;

    const incX = () => $$invalidate(0, x++, x);

    // 3:`each` 塊
    let characters = [
        { name: "Aroma White" },
        { name: "Neko Asakura" },
        { name: "ROBO Head" }
    ];

    const append = name => {
        if (!characters.find(t => t.name === name)) {
            $$invalidate(2, characters = [...characters, { name }]);
        }
    };

    // 生命週期
    onMount(async () => {
        const res = await fetch(`/photos`);
        const photos = await res.json();
        console.log(photos);
    });

    function input_input_handler() {
        name = this.value;
        $$invalidate(1, name);
    }

    const click_handler = () => append("Simon Jackson");

    // 補充:
    // 在這裡 doubleX 被直接套在了一個叫 update 的函式中
    // 並且也用跟 p 函式類似的方式來判斷是否需要更新(如果 x 被改變)
    // 如果有其它 Watch 的變數,也會被放在這裡
    $$self.$$.update = () => {
        if ($$self.$$.dirty & /*x*/ 1) {
            // 這就像 Vue 中的 `watch` 功能
            // 語法很奇怪,但是是合法的
            $: $$invalidate(3, doubleX = x * 2);
        }
    };

    return [x, name, characters, doubleX, onClick, incX, append, input_input_handler, click_handler];
}

// 很簡單地對外暴露一個例項
class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, instance, create_fragment, safe_not_equal, {});
    }
}

export default App;

參考資料


Update 2021-01-18

經大佬提示,有個叫 State of JS 的網站會對開發者做調查,併發布一份報告。在連結中,可以看到目前用途最廣泛的是 React,然後 Angular 比 Vue 略高一籌。此外,Svelte 無論是滿意度、使用量還是價值,都在穩步提升,公眾對它也始終保持很高的興趣。

Update 2021-01-21

從另一個大佬那裡得知了 Vue 有兩個新提案:New script setup and ref sugar,第一個是為了直接用最新的 setup 函式替代之前的元件建立流程,第二個是跟 Svelte 類似,利用 JS Label 的語法來做響應式的語法糖。

它倆分別是這樣用的:

<script setup>
// imported components are also directly usable in template
import Foo from './Foo.vue'
import { ref } from 'vue'

// write Composition API code just like in a normal setup()
// but no need to manually return everything
const count = ref(0)
const inc = () => { count.value++ }
</script>

<template>
    <Foo :count="count" @click="inc" />
</template>
<script setup>
// declaring a variable that compiles to a ref
ref: count = 1

function inc() {
    // the variable can be used like a plain value
    count++
}

// access the raw ref object by prefixing with $
console.log($count.value)
</script>

<template>
    <button @click="inc">{{ count }}</button>
</template>

跟大佬聊了一下,我的觀點主要是這樣的:

  • 我對這提案的看法,和對 Svelte 的看法類似,甚至我都覺得 Evan You 也發現了編譯器可能是個未來的最佳化點。
  • 至於它破壞了 JS 語義的問題,我覺得與其讓 Label 這個語法被忘記,不如賦予它新的意義,因為目前寫 JS 已經基本離不開編譯器了(例如 TypeScript、JSX、Vue Loader)。
  • 此外,語言本身就是在變化中的,JS 正是因為以前的語法不好,才有了那麼多提案和 Linter。
  • 在其它行業,Lisp 有 Clojure、Common Lisp、Scheme 這樣一堆方言,Java“語系”中的 Groovy、Scala、Kotlin 也差不多能算是方言,我覺得 JS 演變出 React、Vue、Angular、Svelte 幾種方言是完全合理的。

之後,大佬拿出了另外一份 JS 的提案:Reference (ref) declarations and expressions,這樣確實不會 Break the Web 了,不過看起來有點像 C 中的 int const a = 1 或者 Rust 中的 let mut x = 5,這在我看來也不是壞事。

當然,語言不可能無限新增 Feature,否則容易長成這樣:cRAzY eSnEXt (*all* proposals mixed in)……

Disqus 載入中……如未能載入,請將 disqus.com 和 disquscdn.com 加入白名單。

這是我們共同度過的

第 3847 天