現代框架背後的概念

前端小智發表於2023-02-09
本文首發於微信公眾號:大遷世界, 我的微信:qq449245884,我會第一時間和你分享前端行業趨勢,學習途徑等等。
更多開源作品請看 GitHub https://github.com/qq449245884/xiaozhi ,包含一線大廠面試完整考點、資料以及我的系列文章。

許多初學者會問“我應該學習哪個框架?”和“在學習框架之前,我需要學多少 JS 或 TS?” - 無數意見文章都在宣傳作者喜歡的框架或庫的優勢,而不是向讀者展示背後的概念,以便進行明智的決策。那麼,讓我們先解決第二個問題:

“在學習框架之前,我需要學多少 JS/TS?”

在學習框架之前,你需要掌握足夠多的基礎知識,使你能夠理解它們所基於的概念。這些知識包括基本資料型別、函式、基本運算子和文件物件模型 (DOM)。雖然除此之外的知識並不會有害,但嚴格來說不是掌握框架或庫所必需的。

如果你是完全的新手,JS for cats 可能是你的第一步的好資源。繼續前進,直到你感到自信為止。這就是你知道足夠多的 JS/TS 的時候,可以轉向框架。其餘的東西你可以在過程中學習。

你指的是哪些概念?

  • State
  • Effects
  • Memoization
  • Templating and rendering

所有現代框架都從這些概念中獲得其功能。

State

狀態只是為應用程式提供動力的資料。 它可能在應用程式的較大部分的全域性級別上,也可能是單個元件上。 以簡單的計數器為例。 它保留的計數即為狀態。 我們可以讀取狀態並寫入它以增加計數。

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

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

但是,這段程式碼存在一個問題:對 count 的更改(例如透過 increment 進行的更改)不會更新按鈕的文字內容。 我們可以手動更新所有內容,但對於更復雜的用例不太適用。

count 能夠更新其使用者的能力稱為響應性。 這是透過訂閱並重新執行應用程式的訂閱部分來更新而實現的。

幾乎每種現代的前端框架和庫都有一種方法來管理反應性狀態。 解決方案有三個部分,至少使用一個或多個部分:

  • 可觀測值/訊號
  • 不可變更新的協調
  • 轉換
可觀測值/訊號

可觀測值基本上是允許透過訂閱讀者的函式進行讀取的結構。 然後在更新時重新執行訂閱者:

const state = (initialValue) => ({
  _value: initialValue,
  get: function() {
    /* subscribe */;
    return this._value; 
  },
  set: function(value) {
    this._value = value;
    /* re-run subscribers */;
  }
});

此概念的第一次使用是在 knockout 中,它使用相同的函式,寫訪問時無引數,讀訪問時有引數。

這種模式目前正在以訊號的形式復興,例如在 Solid.jspreact signals 中,但 Vue 和 Svelte 也使用了相同的模式。

RxJS 是這個原則在簡單狀態之外的延伸,但可以說它模擬複雜性的能力是針對你的腳的一整套槍。 Solid.js 還提供了這些訊號的進一步抽象,即儲存(可以透過 setter 操作的物件)和可變物件(可以像正常的 JS 物件一樣使用的物件或 Vue 中的狀態來處理巢狀狀態物件)。

不可變更新的協調

不可變意味著,如果物件的屬性發生更改,則必須更改整個物件引用,因此可以輕鬆檢測是否存在更改(這就是協調器所做的),只需簡單比較引用。

const state1 = {
  todos: [{ text: 'understand immutability', complete: false }],
  currentText: ''
};
// updating the current text:
const state2 = {
  todos: state1.todos,
  currentText: 'understand reconciliation'
};
// adding a to-do:
const state3 = {
  todos: [
    state.todos[0],
    { text: 'understand reconciliation', complete: true }
  ],
  currentText: ''
};

//這破了不變性:
state3.currentText = 'I am not immutable!';

如你所見,未更改項的引用被重用。 如果協調器檢測到不同的物件引用,它將使用狀態(props,memos,effects,context)再次執行所有元件。 由於讀訪問是被動的,因此需要手動指定對響應性值的依賴關係。

顯然,我們不會以這種方式定義狀態。 要麼從現有屬性構造它,要麼使用所謂的 reducerreducer 是一個將一個狀態轉換為另一個狀態的函式。

Reactpreact 使用了這種模式。 它適用於與 vDOM 一起使用,我們將在後面描述模板時進一步探討。

並非每個框架都使用其 vDOM 使狀態完全響應性。 例如,Mithril.JS 在元件中設定的事件之後從狀態更改中更新; 否則,必須手動觸發 m.redraw()

轉換

轉換是一個構建步驟,它重寫我們的程式碼,使其在舊瀏覽器上執行或使其具有額外的能力;在這種情況下,技術用於將簡單變數變為反應系統的一部分。

Svelte 基於轉換器,該轉換器也從看似簡單的變數宣告和訪問中為其反應系統提供動力。

順便提一下,Solid.js 使用轉換,但不是用於其狀態,只是用於模板。

Effects

在大多數情況下,我們需要做更多的事情來處理響應性狀態,而不僅僅是從中匯出並將其渲染到 DOM 中。 我們必須管理副作用,這是所有由於狀態更改而發生的事情(儘管一些像 Solid.js 的框架將檢視更改視為effects )。

記得第一個來自狀態的示例嗎,其中訂閱處理故意省略了? 讓我們填充這個以在更新時處理effects :

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 中響應態的簡化,但沒有錯誤處理和狀態變化模式(使用接收前一個值並返回下一個值的函式),但是很容易新增。

這可會使上一個示例變成嚮應性:

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);

在大多數情況下,框架允許使用不同的時間來讓 effects 在渲染 DOM 之前、期間或之後執行。

Memoization

Memoization 指的是快取從狀態中計算出來的值,以便在它來源的狀態更改時更新。它基本上是一個 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

既然我們有了純狀態、派生狀態和快取狀態,我們想要向使用者顯示它。在我們的示例中,我們直接使用 DOM 新增了一個按鈕並更新了其文字內容。

為了更友好於開發人員,幾乎所有現代框架都支援一些領域特定語言來在程式碼內編寫與所需輸出類似的內容。儘管有不同的風格,例如 .jsx.vue.svelte 檔案,但這都是在類似於 HTML 的程式碼中表示 DOM 的東西,因此基本上

<div>Hello, World</div>

// in your JS
// becomes in your HTML:

<div>Hello, World</div>

你可能會問:“我應該把我的狀態放在哪裡?”這是個很棒的問題。在大多數情況下,{} 用於表示動態內容,既在屬性中也在節點周圍。

JS 的最常用模板語言擴充套件無疑是 JSX。對於 React,它被編譯成純 JavaScript,使它能夠建立 DOM 的虛擬表示,稱為虛擬文件物件模型(virtual document object model,簡稱 vDOM)的內部檢視狀態。

這是基於這樣一個前提:建立物件比訪問 DOM 快得多,因此如果你可以用當前值替換後者,就可以節省時間。然而,如果在任何情況下都有大量 DOM 更改或者為了沒有更改而建立無數個物件,這種解決方案的優勢很容易變成劣勢,需要透過快取來規避。

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

// transpiled to js
createElement("div", null, "Hello, ", name);

// executed js
{
  "$$typeof": Symbol(react.element),
  "type": "div",
  "key": null,
  "ref": null,
  "props": {
    "children": "Hello, World"
  },
  "_owner": null
}

// rendered vdom
/* HTMLDivElement */<div>Hello, World</div>

不過,JSX並不侷限於react。例如,Solid使用其轉碼器來更大幅度地改變程式碼。

// 1. original code
<div>Hello, {name()}</div>

// 2. transpiled to 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. executed js code
/* HTMLDivElement */<div>Hello, World</div>

這轉譯程式碼看起來有點嚇人,其實很容易解釋發生了什麼。首先,建立具有所有靜態部分的模板,然後克隆它以建立其內容的新例項,並將動態部分新增並連線到狀態更改上。Svelte甚至進一步轉譯了模板和狀態。

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

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

// 2. transpiled to js
/* generated by 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. executed JS code
/* HTMLDivElement */<div>Hello, World</div>

但也有例外。例如,在Mithril.js中,雖然可以使用JSX,但我們鼓勵你寫JS。

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

// 2. executed JS code
/* HTMLDivElement */<div>Hello, World</div>

雖然大多數人會發現開發人員體驗不夠,但其他人更喜歡對程式碼完全控制。根據他們要解決的問題,缺少轉譯步驟甚至可能是有益的。許多其他框架也允許在不進行轉譯的情況下使用,儘管很少像這樣推薦使用。

"那麼我現在應該學習哪個框架或庫?"

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

壞訊息是:沒有銀彈。沒有一個框架會在每個方面都比其他所有的框架好得多。它們中的每一個都有自己的優勢和妥協。React有它的鉤子規則,Angular缺乏簡單的訊號,Vue缺乏向後的相容性,Svelte不能很好地擴充套件,Solid.js禁止重構,Mithril.js不是真正的響應式,這只是舉幾個例子。

好訊息是:沒有錯誤的選擇--至少,除非一個專案的要求真的很有限,無論是在包的大小還是效能方面。每個框架都會完成它的工作。有些可能需要繞過他們的設計決定,這可能會拖慢你的速度,但在任何情況下你都應該能夠得到一個有效的結果。

也就是說,不使用框架可能也是一個可行的選擇。許多專案被過度使用的JavaScript破壞了,而靜態頁面加上一些互動性的東西也能完成工作。

現在你知道了這些框架和庫所應用的概念,選擇那些最適合你當前任務的框架。不要害怕在你的下一個專案中轉換框架。沒有必要學習所有的框架。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

原文:https://dev.to/lexlohr/concep...

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章