框架背後有什麼必學觀念的?這篇文章簡單整理如下
原文出處:https://dev.to/lexlohr/concepts-behind-modern-frameworks-4m1g
很多初學者會問“我應該學哪個框架?”和“學一個框架之前需要學多少JS或TS?” - 無數自以為是的文章都在宣傳作者首選框架或庫的優勢,而不是向讀者展示其背後的概念、教他們如何做出明智的決定。讓我們先解決第二個問題:
盡可能多地理解它們的基本概念。您將需要了解基本資料類型、函數、基本運算符和文檔對像模型 (DOM),這是 HTML 和 CSS 在 JS 中的基礎。雖然先學一點當然沒關係,但沒必要先精通框架或庫。
如果您是一個完全的初學者,JS for cats 可能是您第一步的好資源。繼續前進,直到您感到自信為止,然後繼續前進,直到您開始感到自信不足。那就是你了解足夠的 JS/TS 並可以開始學框架的時間。其餘的你可以邊走邊學。
所有現代框架都從這些概念中衍伸出它們的功能。
狀態只是讓您的應用程式跑起來的資料。它可能在全局級別上,適用於應用程式的較大部分,或適用於單個元件。讓我們以一個簡單的計數器為例。它保留的計數是狀態。我們可以讀取狀態並寫入狀態以增加計數。
最簡單的表示通常是一個變數,其中包含我們的狀態所包含的資料:
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
更新其用戶的能力稱為反應性。這是通過訂閱並重新執行應用程式的訂閱部分來更新的。
幾乎每個現代前端框架和庫都有一種響應式管理狀態的方法。解決方案分為三部分,至少採用其中之一或混合使用:
Observables / Signals
Reconciliation of immutable updates
Transpilation
Observables 基本上是允許藉由訂閱閱讀器的函數來進行讀取的結構。然後訂閱者在更新時重新執行:
const state = (initialValue) => ({
_value: initialValue,
get: function() {
/* subscribe */;
return this._value;
},
set: function(value) {
this._value = value;
/* re-run subscribers */;
}
});
這個概念的第一個用途之一是在 knockout 中,它使用相同的函數,帶和不帶參數進行寫/讀存取。
這種模式目前正在以「信號」的形式復興,例如在 Solid.js 和 [preact signals](https://preactjs.com /guide/v10/signals/),但在 Vue 和 Svelte 的底層使用了相同的模式。 RxJS 為 Angular 的反應層提供動力,是這一原則的延伸,超越了簡單狀態,但有人可能會爭辯說它模擬複雜性的能力可能反而綁手綁腳。 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: ''
};
// this breaks immutability:
state3.currentText = 'I am not immutable!';
如您所見,未更改專案的引用被重新使用。如果協調器檢測到不同的物件引用,它會再次使用狀態(props, memos, effects, context)來重跑所有元件。由於讀取存取是被動的,這需要手動指定對反應值的依賴性。
顯然,您不是以這種方式定義狀態。您可以從現有屬性建置它,也可以使用所謂的 reducer。reducer 是一個函數,它接受一個狀態並返回另一個狀態。
react 和 preact 使用此模式。它適合與 vDOM 一起使用,我們將在稍後描述模板時探討它。
並非每個框架都使用其 vDOM 來使狀態完全響應。 例如 Mithril.JS,元件會在設置的事件後變化後更新狀態;否則你必須手動觸發 m.redraw()
。
Transpilation 是一個建置步驟,它重寫我們的程式碼以使其在舊瀏覽器上執行或賦予它額外的能力;在這種情況下,該技術用於將簡單變數更改為反應系統的一部分。
Svelte 基於一個轉譯器,該轉譯器還通過看似簡單的變數宣告和存取為其反應式系統提供動力。
順便說一句,Solid.js 使用轉譯,但不是針對它的狀態,只是針對模板。
在大多數情況下,我們需要對反應狀態做更多的事情,而不是從中衍伸並渲染到 DOM 中。我們必須管理副作用,這些都是由於視圖更新之外的狀態更改而發生的所有事情(儘管 Solid.js 等一些框架也將視圖更改視為效果)。
還記得第一個例子中,訂閱處理被故意遺漏的狀態嗎?讓我們完成這個處理效果,來作為對更新的反應:
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);
☝ 使用您的開發人員工具在 空白頁面 中嘗試上述兩個程式碼塊。
在大多數情況下,框架允許不同的時間安排,讓效果在渲染 DOM 之前、期間或之後執行。
Memoization 意味著緩存從狀態計算的值,它會從狀態衍伸的變化更新時更新。它基本上是一種回傳衍伸狀態的效果。
在重新執行元件功能的框架中,例如 react 和 preact,這讓某些複雜計算不需要每次都重複計算。
對於其他框架,情況恰恰相反:它允許您選擇部分組件進行響應式更新,同時緩存之前的計算。
對於我們簡單的反應式系統,memo 看起來像這樣:
const memo = (fn) => {
let memoized;
effect(() => {
if (memoized) {
memoized.set(fn());
} else {
memoized = state(fn());
}
});
return memoized.get;
};
現在我們有了純的、衍伸的和緩存形式的狀態,我們想把它展示給用戶。在我們的範例中,我們直接使用 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 的虛擬表示,一種稱為虛擬文檔對像模型或簡稱 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 並不是真正的反應式,僅舉幾例。
好訊息是:沒有錯誤的選擇——至少,除非專案的要求真的很有限,無論是在 bundle 大小還是性能方面。每個框架都會完成它的工作。有些人可能需要配合團隊的設計決策,這可能會使您的速度變慢,但無論如何您都應該能夠獲得可行的結果。
話雖這麼說,沒有框架也可能是一個可行的選擇。許多專案都被過度使用 JavaScript 破壞了。其實帶有一些互動性的靜態頁面就可以完成這項工作。
現在您已經了解了這些框架和庫應用的概念,請選擇最適合您當前任務的概念。不要害怕在下一個專案中切換框架。沒有必要學習所有這些。
如果你嘗試一個新的框架,我發現最有幫助的事情之一就是跟它的社群有所連結,無論是在社群媒體、discord、github 還是其他地方。他們可以告訴您哪些方法適合他們的框架,這將幫助您更快地獲得更好的解決方案。
如果你的主要目標是就業,我建議學習 react。如果您想要輕鬆的性能和控制體驗,請嘗試 Solid.js;你可能會在 Solid 的 Discord 上見到我。
但請記住,所有其他選擇都同樣有效。你不應該因為我這麼說就選擇一個框架,而應該使用最適合你的框架。