本文主要闡述前端框架中的運行時和編譯時的區別。首先說明本文內容參考書籍 《Vue.js 設計與實現》,並加以自己的理解創作。有錯誤的地方還請指出。
運行時#
運行時就是指程式碼實際執行時的階段。前端程式碼是在瀏覽器中執行的,換言之,如果一個框架的程式碼可以直接在瀏覽器中執行,那它就是一個純運行時的框架。
舉個例子,假設我們設計了一個框架,它提供一個 Render 函數,使用者使用時,為該函數提供一個描述 DOM 樹狀結構的參數物件,然後 Render 函數根據該物件遞迴地將資料渲染成 DOM 元素。假設規定要傳入的物件結構如下:
const obj = {
tag: "div",
children: [
{
tag: "span",
children: "hello world!",
},
],
};
物件中有兩個屬性:tag 代表標籤名稱,children 可以是陣列(代表子節點)也可以是文字(代表文字子節點)。然後實現 Render 函數:
function Render(obj, root) {
const el = document.createElement(obj.tag);
if (typeof obj.children === "string") {
const text = document.createTextNode(obj.children);
el.appendChild(text);
} else if (obj.children) {
obj.children.forEach((child) => Render(child, el));
}
root.appendChild(el);
}
現在可以在瀏覽器環境中呼叫 Render 函數:
// 渲染到 body 下
Render(obj, document.body);
此時就會看到預期的結果。這就是一個純運行時的框架例子,因為瀏覽器可以直接執行 JavaScript 檔案,所以它不需要任何的轉換操作。
編譯時#
了解了運行時,那編譯時就很好理解,它是指將原始程式碼轉換成可執行程式碼的階段。在前端中,最簡單的例子就是將高級的 ES6 語法轉換為瀏覽器可以理解和執行的 ES5 語法,這個過程也是編譯。
還是拿上面的程式碼範例來說明,如何將它改造成編譯時的框架?但是首先我們需要知道,不是為了有編譯時而加編譯時,而是為了解決問題才有的編譯時。
我們來看上面運行時的程式碼中有什麼問題?很顯然就是定義 DOM 樹狀結構物件有點麻煩,如果 DOM 結構很複雜,需要定義的物件就會嵌套很深,抽象不直觀。所以我們在想可以直接像寫 HTML 一樣來聲明 UI 嗎?對,就是 vue 中模板(template)那樣編寫。首先我們應該知道,vue 檔案中的 template 部分並不是 HTML,雖然長得很像,但是瀏覽器是不能直接識別它們的,所以 template 模板是需要編譯的。那麼編譯成什麼呢?
編譯的結果是為了讓瀏覽器能直接執行,那麼假設我們的模板這樣寫:
<div>
<span>hello world!</span>
</div>
記住它不是 HTML 檔案,所以我們需要將它編譯成命令式程式碼:
const div = document.createElement("div");
const span = document.createElement("span");
span.innerText = "hello world!";
div.appendChild(span);
document.body.appendChild(div);
此時瀏覽器就可以直接執行程式碼並達到預期效果了。將這個過程封裝成一個編譯器,使用者所有的程式碼只能通過編譯器編譯後才能執行,那它就是一個純編譯時的框架,代表框架如 Svelte
。
運行時 + 編譯時#
上面提到 vue 中的模板需要編譯才能被瀏覽器執行,那 vue 是一個編譯時框架嗎?答案是否定的。
vue 是一個運行時 + 編譯時的框架。這個在官方文件中有提及運行時 vs 編譯時響應性,文件中說到:
Vue 的響應式系統基本是基於運行時的。追蹤和觸發都是在瀏覽器中運行時進行的。
這裡先不管 vue 中的實現細節。我們還是拿上面的例子說明,如果它是運行時 + 編譯時,應該怎麼實現?
其實就是加一個編譯器就行,但不是直接編譯成命令式創建程式碼,而是將模板編譯成一個 DOM 描述物件,然後將物件傳入渲染器。就是這樣:
<div>
<span>hello world!</span>
</div>
編譯成 ⬇️
const obj = {
tag: "div",
children: [
{
tag: "span",
children: "hello world!",
},
],
};
然後渲染:
Render(obj, document.body);
這就是一個運行時 + 編譯時的過程,準確的說是一個運行時編譯,就是程式碼執行時才編譯。在實際框架使用中,我們可以在構建過程中執行編譯,等真正運行時就無需編譯,這樣性能更好。
程式碼寫到這,我們可能會想這樣的運行時編譯是不是多此一舉,直接編譯徹底不是更省事嗎?理論上純編譯時的性能確實會更好,不需要運行時參與,程式碼直接編譯成可執行的 JavaScript 程式碼,但是這也散失了部分靈活性,使用者的內容必須編譯後才能使用。而純運行時,由於沒有編譯過程,就沒辦法分析使用者提供的內容,從而不能做對應的優化,一般編譯器中我們可以實現語法分析,優化和轉換,可以執行語法錯誤檢查,模組打包和程式碼壓縮等任務。只能說框架作者在設計框架時,都是有自己的取捨,不能直接判斷哪種更好。
使用框架 or 直接寫 HTML#
看到上面分析的一堆,我們不禁會想拋棄框架直接寫 HTML 不是更直觀省事。但事實我們也感受到了,使用 vue 或 react 這樣的現代框架,能夠大大增加我們的開發效率。原因就在於這些框架幫我們封裝好了做事的過程,如果是傳統原生 HTML 開發,我們需要維護實現目標的整個過程,包括要手動完成 DOM 元素的創建、更新、刪除等工作。但框架讓我們只需要為結果聲明 UI,不需要關心實現過程。這就是框架的便利之處,了解運行時和編譯時也是為了更好的了解框架的實現原理。
結尾#
最後對運行時和編譯時做一個總結。
運行時(Runtime)是程式碼實際執行時的階段。在前端框架中,運行時通常指在瀏覽器環境中加載和執行已經編譯好的程式碼。這個階段包括了解析 HTML 結構、構建 DOM 樹、執行 JavaScript 程式碼以及處理使用者交互等任務。在運行時,前端框架會利用編譯時生成的程式碼來動態地更新和渲染頁面,處理事件和資料的變化等。
編譯時(Compile Time)是指在開發過程中,將開發者編寫的原始程式碼轉換為可執行的程式碼的階段。例如 es6 轉 es5,vue 模板轉成 html 等。在編譯時,前端框架的編譯器會對程式碼進行語法分析、優化和轉換,以生成可在瀏覽器中執行的程式碼。編譯時的任務包括語法檢查、模組打包、程式碼壓縮等。