<style> .anti-white { display: inline-block; color: #fff; background-color: #070; padding: 1px 5px; border-radius: 5px; } .pink-bubbles-color { color: #cc0033; text-decoration: underline; /* color: #333; */ /* background-color: #333; */ /* transition: all .3s ease-in-out; */ } .pink-bubbles-color:hover { /* color: #fcc; */ /* background-color: #fff; */ /* transition: all .1s ease-in-out; */ } </style> # 前端框架原理概覽 #### React 和 Vue 在此書中定義為是一個構建ui的library(程式庫),其有兩部分組成: 1. 基於狀態的聲明式渲染:只需告訴Raect或Vue當前狀態預期需要顯示的結果,不必去理會處理過程。 2. 組件化的層次架構 #### 為什麼不是定義為framework(框架)呢? 其原因為 React 和 Vue 本身僅<span class="pink-bubbles-color">只解決了Ui建構的部份</span>,並不包含: 路由處理React-router / Vue-router...)、 狀態基管理(Redux / Vuex...)、 資料流管理(React-query / Axios...)、 編譯方案(Babel / SWC...)、 壓縮方案(Webpack / Vite / Turbopack...)等相關附加功能,故只能成為library(程式庫)。 只有包含library(程式庫)及其附加功能的解決方案(如:Next.js / AngularJS)才能稱為框架 <br/> <br/> <br/> <br/> ## 1-1 初識現代前端框架 現代框架的實現原理可於下列公式囊刮概括: ```=a UI = f(state) ``` * state代表 當前狀態 * f()代表 構建UI程式庫的內部運行機制 * UI代表 最終於載體上畫面呈現結果(瀏覽器 / webView) 由上方公式敘述我們可清楚且直觀的了解到現代框架的運作原理基本上皆是將狀態放入構建UI程式庫中,待處理完成後再交由載體逕行畫面呈現,但這中間的處理過程哪些呢?此書該章節整理了下列問題已理解: > 如何描述UI? > 如何組織UI與邏輯? > 如何實現組件間的資料傳輸? > 如何分類前端框架呢? > React中的自變數和應變數是如何詮釋呢? <br/> <br/> <br/> ### 1-1-1 前端框架是如何描述UI呢? 目前前端框架較為主流的描述UI方案為JSX和模板語言為主: #### JSX: 是一種類似XML語法的ECMAScript語法糖,以邏輯為導向強調先描述邏輯在描述UI,該方案能以較為動態的方式處理UI樣式與邏輯,因本身為ES語法組合能輕鬆描述複雜的UI變化與操作。 其使用該方案的好處為: 1. 在UI綁定事件 2. 狀態變化後改變UI樣式及結構 3. 可將JSX當作參數傳遞或返還 4. 可將JSX附值給變數 ```=React const app = () => { const [isLoading, setIsloading] = useState(false); // 可當成變數 const testViews1 = <span>Hello world 2</span>; // 也可當參數傳遞 const testViews2 = (value) => value useEffect(() => { setTimeout(() => { setIsLoading(true); }, 3000); }, []); return ( <div> // 綁定事件 <button onClick={() => setIsLoading(!isLoading)}>getHelloWorld</button> <div> // 狀態變化後改變 { isLoading ? <span>Hello world1</span> : <span>...isLoading</span> } </div> <div> {testViews1} </div> <div> {testViews2(<span>Hello world3</span>)} </div> </div> ) } ``` #### 模板語法: 是一種傳統Html語法的擴展,以UI樣式為導向強調先描述UI在描述邏輯,該方案以較為靜態的方式處理UI樣式與邏輯,搭配每次更新時會分析結構中哪些是需要更新的部分進行更新,搭配Mustache和其他library的模板語法可達成類似JSX的效果。 其使用該方案的好處為: 1. 貼近原生降低學習曲線 2. 狀態變化後改變指定節點UI樣式 3. 使用 Mustache 塞值顯示 4. 搭配Mustache和其他library的模板語法結合處理邏輯(這裡用vue表示) ```=Vue <template> <div> <button @click="setLoading(isLoading)">getHelloWorld</button> <div> <span v-if="isLoading">Hello world1</span> <span v-else>...isLoading</span> </div> <div v-html="testView2"></div> <div v-html="testView3()"></div> <div> {{ testView4 }} </div> </div> </template> <script> export default { name: "HelloWord", data() { return { isLoading: false, testView2: "<span>Hello world2</span>", testView4: "Hello world4", }; }, methods: { setLoading(nowState) { this.isLoading = !nowState; }, testView3() { return "<span>Hello world3</span>"; }, }, }; </script> ``` 其該篇是說明下列兩點: JSX:以邏輯出發,擴展邏輯,描述UI 模板語法:以UI出發,擴展UI,描述邏輯 <br/> <br/> <br/> ### 1-1-2 前端框架是組織UI與邏輯呢? 現在前端框架以組件為單位進行組織,其組織方式可採用數學自變數和應變數為理解依據,首先先了解何謂自變數和應變數? * 自變數:能獨立變化而影響或引起其他變量變化的條件或因素。 * 應變數:研究的目標變量,其取值可被觀測且隨自變數的變化而變化。 **自變數簡單理解就是自設變數/狀態,普遍以`getter`和`setter`組成。** ```=React // 初始化 0 ,這裡以count為自變數 const [count, setCount] = useState(0) // 取值getter console.log(count); // 附值setter setCount(2); ``` **應變數簡單來說就是計算值結果,其中分為下列兩種類型:** 1. 無副作用的應變數 ```=React const [count, setCount] = useState(0) const mathCount = useMemo(()=> count + 1, [count]); console.log(mathCount); ``` 2. 含副作用的應變數 ```=React const [count, setCount] = useState(0) const mathCount = useMemo(()=> { document.title = 'DJ in the home' return count + 1; }, [count]); console.log(mathCount); ``` (這裡的副作用的解釋是以Pure Function為概念) 現在理解了自變數和應變數的區別,那我們就可以理解現代框架採用`邏輯中的自變數或因變數的變化導致UI變化`的方式來組織UI和邏輯,其中如應變數中如有副作用其會導致副作用跟著改變,以下面圖示示例了解一下整個組件內部的工作流程: ```mermaid flowchart TB a1[自變數] b1[應變數] c1[副作用] d1[UI] subgraph component[組件] a1-->b1 a1 & b1-->d1 end b1-->c1 ``` 由上方流程圖可得知組件的工作流程可分類為下列三種方式進行: **1.邏輯中的自變數變化,導致UI變化** ```=React const app = () => { const [count, setCount] = useState(0); return ( <div> // 1.點擊後重新附值於自變數中 <button onClick={()=> setCount(count + 1)}>Add</button> <br/> // 2.附值後改變UI {count} </div> ) } ``` **2.邏輯中的自變數變化,導致無副作用應變數變化,進而導致UI變化** ```=React const app = () => { const [count, setCount] = useState(0); // 2.經過計算後產生應變數 const mathCount = useMemo(()=>. count.toFixed(5), [count]); return ( <div> // 1.點擊後重新附值於自變數中 <button onClick={()=> setCount(count + 1)}>Add</button> <br/> // 3.應變數變化後改變UI {mathCount} </div> ) } ``` **2.邏輯中的自變數變化,導致有副作用應變數變化,進而導致副作用及UI變化** ```=React const app = () => { const [count, setCount] = useState(0); // 2.經過計算後產生應變數 const mathCount = useMemo(()=> count.toFixed(5), [count]); // 2.但有副作用故也會跟著改變 useEffect(()=>{ document.getElementById('Dj not in the home').innerText = `Dj${mathCount} in the home`; }, [mathCount]) return ( <div> // 1.點擊後重新附值於自變數中 <button onClick={()=> setCount(count + 1)}>Add</button> <div> // 3.應變數變化後改變UI {mathCount} </div> // 3.副作用也會跟著改變 <div id="testEffect"> Dj not in the home </div> </div> ) } ``` <br/> <br/> <br/> ### 1-1-3 前端框架是如何實現組件間的資料傳輸? 以現代前端框架組件間互相傳遞數據的方式是透過下列兩種面向方式面行傳遞: **1. 父子組件間的數據傳遞** 其該方法採用的是`父組件的自變數或應變數通過UI傳遞給另一組件`,通過父層層層向下傳地直至目標組件。 <br/> ![](https://hackmd.io/_uploads/B10036j1a.png) **<div style="text-align:center; color: #cc0033;">父傳子組件示意圖</div>** <br/> **2. 跨層級間的組件的數據傳遞** 其該方法採用的是store的方式傳遞自變數,在不同的框架下實現方式和管理方案均有所不同,這裡以React為例,在該框架下使用store須遵循三個步驟: 1. 在需傳遞參數組件的邏輯中調用React.createContext建立context 2. 在需傳遞參數組件的UI中定義context的provider 3. 在接收參數組件上通過useContext取出並使用傳遞的自變數 <br/> ![](https://hackmd.io/_uploads/By1kS0jJ6.png) **<div style="text-align:center; color: #cc0033;">React 跨組件示意圖</div>** <br/> 在接收數據後將組件中的自變數分為state(組件內定義的自變數)和props(外部傳入的自變數),以方便進行辨識區分字變數的來源。 <br/> <br/> <br/> ### 1-1-4 如何分類前端框架呢? 依據前面的論述,我們可以歸結重點於下方 >* state的本質為自變數 >* 前端框架以組件為最小單位 >* 框架會依據自變數(state)變化計算出UI變化 置於上方的重點歸結出了現代框架如何描述UI,那我們就可以再進一步理解從框架到宿主(這裡以瀏覽器替代)之間的工作原理,那框架到瀏覽器間的工作原理可分為下面兩步驟: 1. 框架會依據自變數(state)的變化計算出相應的UI 2. 依據框架計算UI結果執行瀏覽器的DOM API 通常的步驟二的執行方式無論是React或Vue處理方式都相同,其差異在於步驟一上。 於步驟一上,自變數(state)到UI變化上運行的細膩度可分為應用級、組件級和元素級,處理的細膩度越細再處理的變化上時間越少,下方列出各個框架細膩度的運作原理: * 應用級:依據自變數變化,重新更新整個應用(React) ```mermaid flowchart LR a1[state變化] b1[應用變化] c1[比對應用前後變化] d1[更新UI] a1 --> b1 --> c1 --> d1 ``` * 組件級:依據自變數變化,重新更新對應自變數的組件(Vue) ```mermaid flowchart LR a1[state變化] b1[組件變化] c1[比對組件前後變化] d1[更新UI] a1 --> b1 --> c1 --> d1 ``` * 元素級:依據自變數變化,重新更新DOM節點的元素(Solid) ```mermaid flowchart LR a1[state變化] b1[元素變化] d1[更新UI] a1-->b1------>d1 ``` <br/> <br/> <br/> ### 1-1-5 React中的自變數和應變數該如何理解呢? 依據前面自變數和應變數理論,我們可以將該理論套用於React Hooks可加以理解為: * useState:定義組件內部的自變數。 * useReducer:簡單來說為借鑑Redux概念的useState,也相當於組件內部的自變數。 * useContext:實現store概念,專門處理跨層級組件間的自變數傳遞。 * useMemo:定義組件內的無副作用應變數。 * useCallback:定義組件內的無副作用應變數,但是是以function定義。 * useEffect:定義組件內有副作用的應變數。 上述是套用了自變數和應變數理論詮釋的結果,但其有一例外hook需加以理解,且無法使用自變數和應變數理論定義,該項為`useRef`,因該hook的定義為`在組件的邏輯與UI之間多次交互緩存的一個引用類型的值`,做用在於只提供操作的靈活性僅此而已。 <br/> <br/> <br/> <br/> ## 1-2 現代前端框架使用技術 於1-1的論述,我們理解了現代框架的內部工作原理和框架類型的分類標準,現在我們來理解現代框架技術的運行方式概觀,以下為此書規整出來對於現代前端框架需要探討的重點: > 現代框架的細粒度更新技術是什麼?為什麼React不用? > 什麼是編譯呢? > 什麼是Virtual DOM呢? <br/> <br/> <br/> ### 1-2-1 現代框架的細粒度更新技術是什麼?為什麼React不用? 於上一章節概念中有一細節`React 中定義應變數時需要顯示指明的自變數做為依賴`,顧民思意當Raect在定義應變數時在需要一個自變數作為依據,當自變數更新時React會依據自變數更新變化執行定義應變數的操作,於下方示例展示: ```=React // React 定義應變數的方法 const mathCount = useMemo(()=> count.fixed(2), [count]) <-- 可看見需要一個自變數進行依賴 ``` 但在Vue中卻不需要有這項依賴,可直接定義計算值邏輯並返還應變數,於下方示例展示: ```=Vue // Vue 定義應變數的方法 computed :{ mathCount:()=>{ return this.count.fixed(2) } } ``` 其是透過`能自動追蹤依賴的技術`去處理自變數依賴問題,該技術稱為`細粒度更新`、`響應式更新`,這項技術同時也是許多前端框架`自變數變化到UI變化`的底層邏輯,那該項技術的實際用運作方式,我們將於下方利用React API的名稱進行簡單示例: ```=js //儲存 effect 的調用棧 const effectStack = []; // 建立發布訂閱關係 function subscribe(effect, sub) { // 訂閱關係建立 subs.add(effect); // 依賴關係建立 effect.deps.add(subs); } // 重置依賴 function cleanup(effect) { // 從該effect訂閱的所有state中找到對應的sub中並刪除該effect for(const subs of effect.deps) { subs.delete(effect); } // 將該effect所有依賴的subs移除 effect.deps.clear(); } function useState(value) { //保存訂閱該state變化的effect(這裡的實現是利用Set 不會添加重複值的特性處理) const subs = new Set(); const getter = () => { // 獲取當前上下文的effect const effect = effectStack[effectStack.length - 1]; if(effect) { // 執行建立訂閱發佈行為 subscribe(effect, subs) }; return value; }; const setter = (nextValue) => { value = nextValue; //通知所有有訂閱該state的effect變化執行 for(const effect of [...subs]) { effect.execute(); }; }; return [getter, setter]; } function useEffect(callback) { const execute = () => { //重置所有依賴 cleanup(effect); // 將當前effect 放入 調用棧近行保存 effectStack.push(effect); try { callback(); //執行回調 } finally { effectStack.pop(); //清除調用棧 }; }; const effect = { execute, deps:new Set() }; // 立刻執行建立訂閱發布關係 execute(); } function useMemo(callback) { const [s, set] = useState(); //初始化 useEffect(()=> set(callback())); return s; } ``` ```=a const [name1, setName1] = useState('laladog') const [name2, setName2] = useState('lalacat') const [showRealName, tiggerShowRealName] = useState(false); const whoIsHere = useMemo(() => { return !!showRealName ? `${name1()}` : `${name1()} and ${name2()}` }) useEffect(()=> alert('Echo your Name', whoIsHere())) ``` 由上方示例中我們需要理解`響應式更新`的運作原理及思路: <!-- 依據示例及運作原理思路,我們可清楚了解到`響應式更新`兩個特點: 1. 在執行自變數取值時,每一次都會保存自變數的變化於該調用棧中。 2. 每次的更新自變數時,會對使用該自變數的調用棧發布通知。 --> 既然`響應式更新` 有很多的好處,哪為何React Hook為何出使用該項技術呢?原因在於React 的預設更新模式都屬屬於應用級(依據自變數變化,重新更新整個應用)的,其不需要過於細膩度的更新,因此無需使用`響應式更新`,作為不使用該項技術之代價,React Hook在使用上會有`自變數顯示指名依賴`及`不能在條件式聲明Hook`的限制。 <br/> <br/> <br/> ### 1-2-2 什麼是編譯? 現代前端框架在執行完描述UI後,都會需要進行編譯,以作用於 * 將框架計算產生的UI轉換為宿主環境可識別的程式碼 * 程式碼轉化(如將Es6轉換為Es5/Typescript轉換成Javascript) * 在編譯過程中優化程式碼 * 程式碼打包、壓縮大小、混淆化 以確保宿主環境拿到開發者預期的程式碼,於現代框架的編譯方式可分為`AOT`及`JIT`兩種編譯方式,下方為兩種編譯方式的解釋: * AOT(Ahead Of Time 提前編譯或預編譯):在轉化完程式碼後再提給宿主環境,讓宿主環境得到的程式碼都是進行編譯過的。 ![](https://hackmd.io/_uploads/SkYyylklT.png) * JIT(Just In Time 即時編譯):當宿主環境在執行時,程式碼在宿主環境中進行編譯和執行。 ![](https://hackmd.io/_uploads/SJgp01kxT.png) 其兩種編譯方式各有好壞,其區別在於: 1. 使用JIT的應用在宿主環境初始化時會慢於AOT,原因為需先編譯程式碼,而AOT在宿主環境已建構編譯完成,可直接使用執行程式碼。 2. 使用JIT的應用體積有可能會較為肥大,因為在運行時需要包含編譯器的程式碼。 基於上述的兩種區別在大部分採用模板語法的前端框架(Vue、Angular)均採用AOT的編譯方式進行編譯優化,原因為模板語法是固定的,意味著在編譯時可以標記靜態部分與動態部分在變化UI時可忽略靜態部分直接尋找有更動的動態部分(PS:元素級框架會在這時期建立自變數與UI的動態部分)。 那反觀思維採用JSX的前端框架(React/solid.js)其實也是採用AOT的編譯方式,但難以從中受益,因為ES語法靈活特性,故難以進行靜態分析,那要如何讓JSX的前端框架在AOT中受益呢?此書提供兩種思路: * 優化AOT * 約束JSX的靈活性 React 曾在嘗試過`優化AOT`推出名為prepack的React編譯器,該項編譯器思維在於`程式碼在編譯時將計算結果保留在編譯後的程式碼中,而不是在運行時才去求值`, 但目前該編譯器停更,現用React Forget編譯器。 Solid.js 的思維則用`約束JSX的靈活性`這個思維進行,其本身框架的特性讓`在UI中描述邏輯`實現,其就是擁有JSX特性,但使用模板語法概念的前端框架。 <br/> <br/> <br/> ### 1-2-3 什麼是Virtual DOM呢? Virtual DOM又稱虛擬DOM(簡稱VDOM)是現今現代框架實現`依據自變數變化計算出UI變化`的主流技術,其工作原理有下列兩步驟: 1. 將`元素描述的UI`轉化為`VDOM描述的UI` 2. 對比前後`VDOM描述的UI`,計算出發生變化的部分。 現代使用該技術的框架基本都是遵循上面兩步驟,只是在細節上有所區別,下面來理解JSX和模板語法對於使用該技術的區別: * 模板語法:這裡已Vue為例,該框架採用render function執行編譯並返回`VNode描述的UI`(這段在Vue上稱為render),如有更新數據,會將變化前後UI進行比較並算出變化的部分(這段在Vue上稱為patch) * JSX:這裡已React為例,該框架採用createElement方法執行編譯並返回`React Element 描述的 UI`,如有更新數據,會將`React Element描述的UI`與變化前的`FiberNode描述的UI`進行比較並算出變化部分,同時生成變化後的`FiberNode描述的UI` 使用VDOM技術的框架有下面三個優點: 1. 採用較少冗餘屬性進行比對更新,能夠降低記憶體消耗。 2. 相較於AOT有更強大的描述能力。 3. 多平台渲染的抽象能力,實現一個框架多平台渲染概念。 <br/> <br/> <br/> ## 1-3 總結 1. 以公式UI=f(state)為出發點,將現代前端框架進行分類: * 應用級框架:依據自變數與應用的對應關係,重新更新整個應用。 * 組件級框架:依據自變數與組件的對應關係,重新更新對應自變數的組件。 * 元素級框架:依據自變數與元素的對應關係,重新更新DOM節點的元素。 2. 現代框架使用的主流技術: * 在運行時建立`自變數和應變數關係`的響應式更新。 * 現代框架編譯方式有AOT和JIT兩種方式,大部分的框架會使用AOT進行框架與宿主間的UI變化。 * Virtual DOM是現代框架實現`依據自變數變化計算出UI變化`的主流技術,當完成變化在由AOT編譯成宿主環境使用程式碼。