【精】現代前端框架的重要概念

superZidan發表於2023-01-03

許多初學者經常會問 “我需要學習哪個框架 ?” 以及 “學習框架前需要掌握多少 JS 或者 TS ?” 無數帶有主觀色彩的文章都在宣傳作者首選框架或庫的優勢,而不是向讀者展示其背後的概念以做出更明智的決定。所以讓我們先解決第二個問題

學習框架前需要掌握多少 JS 或者 TS

儘可能多地去學以讓更好的你理解它們所基於的概念。你將需要了解基本資料型別、函式、基本運算子和文件物件模型 ( DOM ),這是 HTML 和 CSS 在 JS 中的表示。除此之外的一切也都 OK,但並不嚴格要求某個精通框架或庫。

如果你是一個完完全全的新手,JS for cats 應該是一個不錯的入門資料。持續學習,直到你感到自信為止,然後繼續前進,直到你再次感到自信為止。當掌握了足夠的 JS / TS 知識後,你就可以開始學習框架。其他的知識你可以並行學習。

哪些重要概念

  • State (狀態)
  • Effects (副作用)
  • Memoization (記憶化)
  • Templating and rendering (模板與渲染)

所有現代框架都從這些概念中派生出它們的功能

state

State 只是為你的應用程式提供動力的資料。它可能在全域性級別,適用於應用程式的大部分元件,或適用於單個元件。讓我們寫一個計數器的簡單例子來說明一下。它保留的計數是 state 。我們可以讀取 state 或者寫入 state 以增加計數

最簡單的表示通常是一個變數,其中包含我們的狀態所包含的資料:

let count = 0; 
const increment = () => { count++; }; 
const button = document.createElement('button'); 
button.textContent = count; 
button.addEventListener('click', increment); document.body.appendChild(button);

但這個程式碼有個問題:類似呼叫 increment 方法一樣去修改 count 的值 ,並不會自動修改 button 的文案。我們需要手動去更新所有的內容,但這樣的做法在複雜場景下程式碼的可維護性 & 擴充套件性都不是很好。

count 自動更新依賴它的使用方的能力稱之為 reactivity(響應式) 。這是透過訂閱並重新執行應用程式的訂閱部分來更新的。

幾乎所有的現代前端框架和庫都擁有讓 state 變成 reactivity 的能力。基本上可以分為 3 種解決方案,採用其中至少一種或者多種混用來實現這個能力:

  • Observables / Signals (可觀察的 / 訊號)
  • Reconciliation of immutable updates (協調不可變的更新)
  • Transpilation (轉譯)

這些概念還是直接用英文表達比較貼切 ?

Observables / Signals (可觀察的 / 訊號)

Observables 基本上是在讀取 state 的時候透過一個訂閱方法來收集依賴,然後在更新的時候觸發依賴的更新

const state = (initialValue) => ({
  _value: initialValue,
  get: function() {
    /* 訂閱 */;
    return this._value; 
  },
  set: function(value) {
    this._value = value;
    /* 觸發更新 */;
  }
});

knockout 是最早使用這個概念的框架之一,它使用帶有 / 不帶引數的相同函式進行寫/讀訪問

這種模式最近有開始有框架透過 signals 來實現,比如 Solid.js 和 preact signals ;相同的模式也在 Vue 和 Svelte 中使用到。RxJS 為 Angular 的 reactive 層提供底層能力,是這一模式的延伸,超越了簡單狀態。Solid.js 用 Stores(一些透過 setter 方法來操作的物件)的方式進一步抽象了 signals

Reconciliation of immutable states(協調不可變的更新)

不可變意味著如果物件的某個屬性發生改變,那麼整個物件的引用就會發生改變。所以協調器做的事情就包括透過簡單的引用對比就判斷出物件是否發生了改變

const state1 = {
  todos: [{ text: 'understand immutability', complete: false }],
  currentText: ''
};
// 更新 currentText 屬性
const state2 = {
  todos: state1.todos,
  currentText: 'understand reconciliation'
};
// 新增一個 todo
const state3 = {
  todos: [
    state1.todos[0],
    { text: 'understand reconciliation', complete: true }
  ],
  currentText: ''
};

// 由於不可變性,這裡將會報錯
state3.currentText = 'I am not immutable!';

如你所見,未變更專案的引用被重新使用。如果協調器檢測到不同的物件引用,那麼它將重新執行所有的元件,讓所有的元件的 state (props, memos, effects, context) 都使用最新的這個物件。由於讀取訪問是被動的,所以需要手動指定對響應值的依賴。

很顯然,你不會用上面這種方式定義 state 。要麼你是從一個已經存在的屬性構造 state ,要麼你會使用 reducer 來構造 state。一個 reducer 函式就是接收一個 state 物件然後返回一個新的 state 物件。

react 和 preact 就使用這種模式。它適合與 vDOM 一起使用,我們將在稍後描述模板時探討它。

並不是所有的框架都藉助 vDOM 將 state 變成完成響應式。例如 Mithril.JS 要不是在 state 修改後觸發對應的生命週期事件,要不是手動呼叫 m.redraw() 方法,才能夠觸發更新

Transpilation(轉譯)

Transpilation 是在構建階段,重寫我們的程式碼讓程式碼可以在舊的瀏覽器執行或者賦予程式碼其他的能力;在這種情況下,轉譯則是被用於把一個簡單的變數修改成響應式系統的一部分。

Svelte 就是基於轉譯器,該轉譯器還透過看似簡單的變數宣告和訪問為他們的響應式系統提供能力

另外,Solid.js 也是使用 Transpilation ,但 Transpilation 只使用到模版上,沒有使用到 state 上

Effects

大部分情況下,我們需要做的更多是操作響應式的 state,而很少需要操作基於 state 的 DOM 渲染。我們需要管理好副作用,這些副作用是由於檢視更新之外的狀態變化而發生的所有事情(雖然有些框架把檢視更新也當作是副作用,例如 Solid.js

記得之前 state 的例子中,我們故意把訂閱操作的程式碼留空。現在讓我們把這些留空補齊來處理副作用,讓程式能夠響應更新

const context = [];

const state = (initialValue) => ({
  _subscribers: new Set(),
  _value: initialValue,
  get: function() {
    const current = context.at(-1);
    if (current) { this._subscribers.add(current); }
    return this._value;
  },
  set: function(value) {
    if (this._value === value) { return; }
    this._value = value;
    this._subscribers.forEach(sub => sub());
  }
});

const effect = (fn) => {
  const execute = () => {
    context.push(execute);
    try { fn(); } finally { context.pop(); }
  };
  execute();
};

上面程式碼基本上是對 preact signals 或者 Solid.js 響應式 state 的簡化版本,它不包含錯誤處理和複雜狀態處理(使用一個函式接收之前的狀態值,返回下一個狀態值),但這些都是很容易就可以加上的

這允許我們使前面的示例具有響應性:

const count = state(0);
const increment = () => count.set(count.get() + 1);
const button = document.createElement('button');
effect(() => {
  button.textContent = count.get();
});
button.addEventListener('click', increment);
document.body.appendChild(button);
☝ 可以嘗試執行一下上面 Effect 的兩個程式碼塊的例子,原始碼地址在 這裡

在大多數情況下,框架允許在不同生命週期,讓 Effect 在渲染 DOM 之前、期間或之後執行。

Memoization

Memoization 意味著快取 state 值的計算結果,並且在結果的依賴發生改變的時候進行更新。它基本上是一種返回派生(derived) state 的 Effect

在某些會重新執行其元件函式的框架中,如 react 和 preact,允許在它所依賴的狀態沒有改變時避免這部分元件重新渲染

對於其他框架,情況恰恰相反:它允許你選擇部分元件進行響應式更新,同時快取之前的計算

對於我們簡單的響應系統,memo 大概是這樣實現

const memo = (fn) => {
  let memoized;
  effect(() => {
    if (memoized) {
      memoized.set(fn());
    } else {
      memoized = state(fn());
    }
  });
  return memoized.get;
};

Templating and rendering

現在有了原始的、派生的和快取形式的 state,我們想把它展示給使用者。在我們的例子中,我們直接操作 DOM 來新增按鈕和更新按鈕的內容文案。

為了提升開發體驗,幾乎所有的現代框架都支援 DSL 來在程式碼中編寫類似於所需輸出的內容。雖然有不同的風格,比如 .jsx.vue.svelte 檔案,但這一切都歸結為用類似於 HTML 的程式碼來表示 DOM。所以基本上是

<div>Hello, World</div>

// 在你的 JS 程式碼中
// 變成你的 HTML:

<div>Hello, World</div>

你可以能會問:“在哪裡放置我的 state ?”。非常好的問題,大部分的情況 下,{} 用於在屬性和節點中表達動態內容。

最常用的 JS 模板語言擴充套件無疑是 JSX。在 react 中,它被編譯為存粹的 JavaScript 語言,允許建立對於 DOM 的虛擬表示,也就是經常被提到的「虛擬文件物件」或者簡稱為 vDOM。

這是基於建立 JS 物件比訪問 DOM 快得多的前提,所以如果你可以用建立 JS 物件替換訪問 DOM,那麼你就可以節省時間

然而,如果你的專案在任何情況下都沒有大量的 DOM 修改或者只是建立不需要修改的物件;那麼上面這個方案的優點就會變成缺點,那這個時候就需要使用 memoization 來將缺點的影響降到最小。

// 1. 原始碼
<div>Hello, {name}</div>

// 2. 轉譯成 js 程式碼
createElement("div", null, "Hello, ", name);

// 3. 執行 js 後返回的物件
{
  "$$typeof": Symbol(react.element),
  "type": "div",
  "key": null,
  "ref": null,
  "props": {
    "children": "Hello, World"
  },
  "_owner": null
}

// 4. 渲染 vdom
/* HTMLDivElement */<div>Hello, World</div>

JSX 不僅僅用在 react,也用在了 Solid.js。例如,使用 Solid 轉譯器更徹底地改變程式碼

// 1. 原始碼
<div>Hello, {name()}</div>

// 2. 轉譯成 js 程式碼
const _tmpl$ = /*#__PURE__*/_$template(`<div>Hello, </div>`, 2);
(() => {
  const _el$ = _tmpl$.cloneNode(true),
    _el$2 = _el$.firstChild;
  _$insert(_el$, name, null);
  return _el$;
})();

// 3. 渲染 vdom
/* HTMLDivElement */<div>Hello, World</div>

雖然轉譯之後的程式碼一開始看到會覺得挺嚇人,但它更容易解釋其中程式碼的邏輯。首先,模版的靜態部分被建立出來;然後,建立出來的物件被克隆並建立一個新的例項,新的例項包含被新增的動態部分,以及將動態部分的更新與 state 的更新關聯起來。

Svelte 在轉譯的時候做的工作更多,不僅僅處理了模版,還處理了 state

// 1. 原始碼
<script>
let name = 'World';
setTimeout(() => { name = 'you'; }, 1000);
</script>

<div>Hello, {name}</div>

// 2. 轉譯成 js 程式碼
/* 生成自 Svelte v3.55.0 版本 */
import {
        SvelteComponent,
        append,
        detach,
        element,
        init,
        insert,
        noop,
        safe_not_equal,
        set_data,
        text
} from "svelte/internal";

function create_fragment(ctx) {
        let div;
        let t0;
        let t1;

        return {
                c() {
                        div = element("div");
                        t0 = text("Hello, ");
                        t1 = text(/*name*/ ctx[0]);
                },
                m(target, anchor) {
                        insert(target, div, anchor);
                        append(div, t0);
                        append(div, t1);
                },
                p(ctx, [dirty]) {
                        if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
                },
                i: noop,
                o: noop,
                d(detaching) {
                        if (detaching) detach(div);
                }
        };
}

function instance($$self, $$props, $$invalidate) {
        let name = 'World';

        setTimeout(
                () => {
                        $$invalidate(0, name = 'you');
                },
                1000
        );

        return [name];
}

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

export default Component;

// 3. 執行 JS 程式碼
/* HTMLDivElement */<div>Hello, World</div>

當然也有例外,在 Mithril.js 中,雖然可以使用 JSX,但鼓勵你編寫 JS 程式碼

// 1. 原始碼
const Hello = {
  name: 'World',
  oninit: () => setTimeout(() => {
    Hello.name = 'you';
    m.redraw();
  }, 1000),
  view: () => m('div', 'Hello, ' + Hello.name + '!')
};

// 2. 執行 JS 程式碼
/* HTMLDivElement */<div>Hello, World</div>

有的人會覺得這樣做的開發體驗不太好,但有的人更希望對自己的程式碼有更多的控制權。這取決於他們想要解決的是哪一類的問題,缺少 transpilation 這個步驟也可能成為優點。

許多其他框架也允許在不進行 transpilation 的情況下使用,儘管很少有人這樣推薦。

“現在我應該學習什麼框架或者庫?”

我有一些好訊息和一些壞訊息要告訴你

壞訊息是:沒有銀彈。沒有任何一個框架是在所有層面都優於其他框架的。任何一個框架都有它的優點和妥協。React 有它的 hook 規則,Angular 缺乏簡單的 signals,Vue 的向後相容性問題,Svelte 的伸縮性不太好,Solid.js 禁止解構,Mithril.js 不是真正的響應式,等等

好訊息是:沒有錯誤選擇 —— 除非專案的要求確實受到限制,無論是在捆綁包大小還是效能方面。每個框架都可以完成工作。有些人可能需要解決他們的設計決策,這可能會使你的速度變慢,但無論如何你都能夠獲得可行的結果。

話雖這麼說,沒有框架也可能是一個可行的選擇。許多專案都被過度使用 JavaScript 破壞了,而帶有一些互動性的靜態頁面也可以完成這項工作。

現在你已經瞭解了這些框架和庫所應用的概念,請選擇最適合你當前任務的方案。不要為下個專案的框架選型而感到擔心。你不需要學習所有的內容。

如果你嘗試一個新的框架,我發現最有幫助的事情之一就是關注它的社群,無論是在社交媒體、Discord、github 還是其他地方。他們可以告訴你哪些方法適合他們的框架,這將幫助你更快地獲得更好的解決方案。

衝吧,你可以有個人喜好!

如果你的主要目標是就業,我建議學習 React 或者 Vue。 如果你想要輕鬆的獲取效能和控制體驗,請嘗試 Solid.js

但請記住,所有其他選擇都同樣有效。 你不應該因為我這麼說就選擇一個框架,而應該使用最適合你的框架。

如果你看完了整篇文章,感謝你的耐心等待。 希望對你有所幫助。 在這裡發表你的評論,祝你有美好的一天 ?

參考文章

相關文章