摘要:一個字符串或者虛擬的數(shù)組用于表示該節(jié)點的。傳遞給函數(shù)的參數(shù)有兩個首先是當(dāng)前狀態(tài),其次是事件處理的回調(diào)函數(shù),對生成的視圖中觸發(fā)的事件進行處理。回調(diào)函數(shù)主要負(fù)責(zé)為應(yīng)用程序構(gòu)建一個新的狀態(tài),并使用新的狀態(tài)重啟循環(huán)。
原文鏈接原文寫于 2015-07-31,雖然時間比較久遠(yuǎn),但是對于我們理解虛擬 DOM 和 view 層之間的關(guān)系還是有很積極的作用的。
React 是 JavaScript 社區(qū)的新成員,盡管 JSX (在 JavaScript 中使用 HTML 語法)存在一定的爭議,但是對于虛擬 DOM 人們有不一樣的看法。
對于不熟悉的人來說,虛擬 DOM 可以描述為某個時刻真實DOM的簡單表示。其思想是:每次 UI 狀態(tài)發(fā)生更改時,重新創(chuàng)建一個虛擬 DOM,而不是直接使用命令式的語句更新真實 DOM ,底層庫將對應(yīng)的更新映射到真實 DOM 上。
需要注意的是,更新操作并沒有替換整個 DOM 樹(例如使用 innerHTML 重新設(shè)置 HTML 字符串),而是替換 DOM 節(jié)點中實際修改的部分(改變節(jié)點屬性、添加子節(jié)點)。這里使用的是增量更新,通過比對新舊虛擬 DOM 來推斷更新的部分,然后將更新的部分通過補丁的方式更新到真實 DOM 中。
虛擬 DOM 因為高效的性能經(jīng)常受到特別的關(guān)注。但是還有一項同樣重要的特性,虛擬 DOM 可以把 UI 表示為狀態(tài)函數(shù)的映射(PS. 也就是我們常說的 UI = render(state)),這也使得編寫 web 應(yīng)用有了新的形式。
在本文中,我們將研究虛擬 DOM 的概念如何引用到 web 應(yīng)用中。我們將從簡單的例子開始,然后給出一個架構(gòu)來編寫基于 Virtual DOM 的應(yīng)用。
為此我們將選擇一個獨立的 JavaScript 虛擬 DOM 庫,因為我們希望依賴最小化。本文中,我們將使用 snabbdom(paldepind/snabbdom),但是你也可以使用其他類似的庫,比如 Matt Esch 的 virtual-dom
snabbdom簡易教程snabbdom 是一個模塊化的庫,所以,我們需要使用一個打包工具,比如 webpack。
首先,讓我們看看如何進行 snabbdom 的初始化。
import snabbdom from "snabbdom"; const patch = snabbdom.init([ // 指定模塊初始化 patch 方法 require("snabbdom/modules/class"), // 切換 class require("snabbdom/modules/props"), // 設(shè)置 DOM 元素的屬性 require("snabbdom/modules/style"), // 處理元素的 style ,支持動畫 require("snabbdom/modules/eventlisteners"), // 事件處理 ]);
上面的代碼中,我們初始化了 snabbdom 模塊并添加了一些擴展。在 snabbdom 中,切換 class、style還有 DOM 元素上的屬性設(shè)置和事件綁定都是給不同模塊實現(xiàn)的。上面的實例,只使用了默認(rèn)提供的模塊。
核心模塊只暴露了一個 patch 方法,它由 init 方法返回。我們使用它創(chuàng)建初始化的 DOM,之后也會使用它來進行 DOM 的更新。
下面是一個 Hello World 示例:
import h from "snabbdom/h"; var vnode = h("div", {style: {fontWeight: "bold"}}, "Hello world"); patch(document.getElementById("placeholder"), vnode);
h 是一個創(chuàng)建虛擬 DOM 的輔助函數(shù)。我們將在文章后面介紹具體用法,現(xiàn)在只需要該函數(shù)的 3 個輸入?yún)?shù):
一個 CSS 選擇器(jQuery 的選擇器),比如 div#id.class。
一個可選的數(shù)據(jù)對象,它包含了虛擬節(jié)點的屬性(class、styles、events)。
一個字符串或者虛擬 DOM 的數(shù)組(用于表示該節(jié)點的children)。
第一次調(diào)用的時候,patch 方法需要一個 DOM 占位符和一個初始的虛擬 DOM,然后它會根據(jù)虛擬 DOM 創(chuàng)建一個對應(yīng)的真實 DO樹。在隨后的的調(diào)用中,我們?yōu)樗峁┬屡f兩個虛擬 DOM,然后它通過 diff 算法比對這兩個虛擬 DOM,并找出更新的部分對真實 DOM 進行必要的修改 ,使得真實的 DOM 樹為最新的虛擬 DOM 的映射。
為了快速上手,我在 GitHub 上創(chuàng)建了一個倉庫,其中包含了項目的必要內(nèi)容。下面讓我們來克隆這個倉庫(yelouafi/snabbdom-starter),然后運行 npm install 安裝依賴。這個倉庫使用 Browserify 作為打包工具,文件變更后使用 Watchify 自動重新構(gòu)建,并且通過 Babel 將 ES6 的代碼轉(zhuǎn)成兼容性更好的 ES5。
下面運行如下代碼:
npm run watch
這段代碼將啟動 watchify 模塊,它會在 app 文件夾內(nèi),創(chuàng)建一個瀏覽器能夠運行的包:build.js 。模塊還將檢測我們的 js 代碼是否發(fā)生改變,如果有修改,會自動的重新構(gòu)建 build.js。(如果你想手動構(gòu)建,可以使用:npm run build)
在瀏覽器中打開 app/index.html 就能運行程序,這時候你會在屏幕上看到 “Hello World”。
這篇文中的所有案例都能在特定的分支上進行實現(xiàn),我會在文中鏈接到每個分支,同時 README.md 文件也包含了所有分支的鏈接。
動態(tài)視圖本例的源代碼在 dynamic-view branch
為了突出虛擬 DOM 動態(tài)化的優(yōu)勢,接下來會構(gòu)建一個很簡單的時鐘。
首先修改 app/js/main.js:
function view(currentDate) { return h("div", "Current date " + currentDate); } var oldVnode = document.getElementById("placeholder"); setInterval( () => { const newVnode = view(new Date()); oldVnode = patch(oldVnode, newVnode); }, 1000);
通過多帶帶的函數(shù) view 來生成虛擬 DOM,它接受一個狀態(tài)(當(dāng)前日期)作為輸入。
該案例展示了虛擬 DOM 的經(jīng)典使用方式,在不同的時刻構(gòu)造出新的虛擬 DOM,然后將新舊虛擬 DOM 進行對比,并更新到真實 DOM 上。案例中,我們每秒都構(gòu)造了一個虛擬 DOM,并用它來更新真實 DOM。
事件響應(yīng)本例的源代碼在 event-reactivity branch
下面的案例介紹了通過事件系統(tǒng)完成一個打招呼的應(yīng)用程序:
function view(name) { return h("div", [ h("input", { props: { type: "text", placeholder: "Type your name" }, on : { input: update } }), h("hr"), h("div", "Hello " + name) ]); } var oldVnode = document.getElementById("placeholder"); function update(event) { const newVnode = view(event.target.value); oldVnode = patch(oldVnode, newVnode); } oldVnode = patch(oldVnode, view(""));
在 snabbdom 中,我們使用 props 對象來設(shè)置元素的屬性,props 模塊會對 props 對象進行處理。類似地,我們通過 on 對象進行元素的時間綁定,eventlistener 模塊會對 on 對象進行處理。
上面的案例中,update 函數(shù)執(zhí)行了與前面案例中 setInterval 類似的事情:從傳入的事件對象中提取出 input 的值,構(gòu)造出一個新的虛擬 DOM,然后調(diào)用 patch ,用新的虛擬 DOM 樹更新真實 DOM。
復(fù)雜的應(yīng)用程序使用獨立的虛擬 DOM 庫的好處是,我們在構(gòu)建自己的應(yīng)用時,可以按照自己喜歡的方式來做。你可以使用 MVC 的設(shè)計模式,可以使用更現(xiàn)代化的數(shù)據(jù)流體系,比如 Flux。
在這篇文章中,我會介紹一種不太為人所知的架構(gòu)模式,是我之前在 Elm(一種可編譯成 JavaScript 的 函數(shù)式語言)中使用過的。Elm 的開發(fā)者稱這種模式為 Elm Architecture,它的主要優(yōu)點是允許我們將整個應(yīng)用編寫為一組純函數(shù)。
主流程讓我們回顧一下上個案例的主流程:
通過 view 函數(shù)構(gòu)造出我們初始的虛擬 DOM,在 view 函數(shù)中,給 input 輸入框添加了一個 input 事件。
通過 patch 將虛擬 DOM 渲染到真實 DOM 中,并將 input 事件綁定到真實 DOM 上。
等待用戶輸入……
用戶輸入內(nèi)容,觸發(fā) input 事件,然后調(diào)用 update 函數(shù)
在 update 函數(shù)中,我們更新了狀態(tài)
我們傳入了新的狀態(tài)給 view 函數(shù),并生成新的虛擬 DOM (與步驟 1 相同)
再次調(diào)用 patch,重復(fù)上述過程(與步驟 2 相同)
上面的過程可以描述成一個循環(huán)。如果去掉實現(xiàn)的一些細(xì)節(jié),我們可以建立一個抽象的函數(shù)調(diào)用序列。
user 是用戶交互的抽象,我們得到的是函數(shù)調(diào)用的循環(huán)序列。注意,user 函數(shù)是異步的,否則這將是一個無限的死循環(huán)。
讓我們將上述過程轉(zhuǎn)換為代碼:
function main(initState, element, {view, update}) { const newVnode = view(initState, event => { const newState = update(initState, event); main(newState, newVnode, {view, update}); }); patch(oldVnode, newVnode); }
main 函數(shù)反映了上述的循環(huán)過程:給定一個初始狀態(tài)(initState),一個 DOM 節(jié)點和一個頂層組件(view + update),main 通過當(dāng)前的狀態(tài)經(jīng)過 view 函數(shù)構(gòu)建出新的虛擬 DOM,然后通過補丁的方式更新到真實 DOM上。
傳遞給 view 函數(shù)的參數(shù)有兩個:首先是當(dāng)前狀態(tài),其次是事件處理的回調(diào)函數(shù),對生成的視圖中觸發(fā)的事件進行處理?;卣{(diào)函數(shù)主要負(fù)責(zé)為應(yīng)用程序構(gòu)建一個新的狀態(tài),并使用新的狀態(tài)重啟 UI 循環(huán)。
新狀態(tài)的構(gòu)造委托給頂層組件的 update 函數(shù),該函數(shù)是一個簡單的純函數(shù):無論何時,給定當(dāng)前狀態(tài)和當(dāng)前程序的輸入(事件或行為),它都會為程序返回一個新的狀態(tài)。
要注意的是,除了 patch 方法會有副作用,主函數(shù)內(nèi)不會有任何改變狀態(tài)行為發(fā)生。
main 函數(shù)有點類似于低級GUI框架的 main 事件循環(huán),這里的重點是收回對 UI 事件分發(fā)流程的控制: 在實際狀態(tài)下,DOM API通過采用觀察者模式強制我們進行事件驅(qū)動,但是我們不想在這里使用觀察者模式,下面就會講到。
Elm 架構(gòu)(Elm architecture)基于 Elm-architecture 的程序中,是由一個個模塊或者說組件構(gòu)成的。每個組件都有兩個基本函數(shù):update和view,以及一個特定的數(shù)據(jù)結(jié)構(gòu):組件擁有的 model 以及更新該 model 實例的 actions。
update 是一個純函數(shù),接受兩個參數(shù):組件擁有的 model 實例,表示當(dāng)前的狀態(tài)(state),以及一個 action 表示需要執(zhí)行的更新操作。它將返回一個新的 model 實例。
view 同樣接受兩個參數(shù):當(dāng)前 model 實例和一個事件通道,它可以通過多種形式傳播數(shù)據(jù),在我們的案例中,將使用一個簡單的回調(diào)函數(shù)。該函數(shù)返回一個新的虛擬 DOM,該虛擬 DOM 將會渲染成真實 DOM。
如上所述,Elm architecture 擺脫了傳統(tǒng)的由事件進行驅(qū)動觀察者模式。相反該架構(gòu)傾向于集中式的管理數(shù)據(jù)(比如 React/Flux),任何的事件行為都會有兩種方式:
冒泡到頂層組件;
通過組件樹的形式進行下發(fā),在此階段,每個組件都可以選擇自己的處理方式,或者轉(zhuǎn)發(fā)給其他一個或所有子組件。
該架構(gòu)的另一個關(guān)鍵點,就是將程序需要的整個狀態(tài)都保存在一個對象中。樹中的每個組件都負(fù)責(zé)將它們擁有的狀態(tài)的一部分傳遞給子組件。
在我們的案例中,我們將使用與 Elm 網(wǎng)站相同的案例,因為它完美的展示了該模式。
案例一:計數(shù)器本例的源代碼在 counter-1 branch
我們在 “counter.js” 中定義了 counter 組件:
const INC = Symbol("inc"); const DEC = Symbol("dec"); // model : Number function view(count, handler) { return h("div", [ h("button", { on : { click: handler.bind(null, {type: INC}) } }, "+"), h("button", { on : { click: handler.bind(null, {type: DEC}) } }, "-"), h("div", `Count : ${count}`), ]); } function update(count, action) { return action.type === INC ? count + 1 : action.type === DEC ? count - 1 : count; } export default { view, update, actions : { INC, DEC } }
counter 組件由以下屬性組成:
Model: 一個簡單的 Number
View:為用戶提供兩個按鈕,用戶遞增、遞減計數(shù)器,以及顯示當(dāng)前數(shù)字
Update:接受兩個動作:INC / DEC,增加或減少計數(shù)器的值
首先要注意的是,view/update 都是純函數(shù),除了輸入之外,他們不依賴任何外部環(huán)境。計數(shù)器組件本身不包括任何狀態(tài)或變量,它只會從給定的狀態(tài)構(gòu)造出固定的視圖,以及通過給定的狀態(tài)更新視圖。由于其純粹性,計數(shù)器組件可以輕松的插入任何提供依賴(state 和 action)環(huán)境。
其次需要注意 handler.bind(null, action) 表達式,每次點擊按鈕,事件監(jiān)聽器都會觸發(fā)該函數(shù)。我們將原始的用戶事件轉(zhuǎn)換為一個有意義的操作(遞增或遞減),使用了 ES6 的 Symbol 類型,比原始的字符串類型更好(避免了操作名稱沖突的問題),稍后我們還將看到更好的解決方案:使用 union 類型。
下面看看如何進行組件的測試,我們使用了 “tape” 測試庫:
import test from "tape"; import { update, actions } from "../app/js/counter"; test("counter update function", (assert) => { var count = 10; count = update(count, {type: actions.INC}); assert.equal(count, 11); count = update(count, {type: actions.DEC}); assert.equal(count, 10); assert.end(); });
我們可以直接使用 babel-node 來進行測試
babel-node test/counterTest.js案例二:兩個計數(shù)器
本例的源代碼在 counter-2 branch
我們將和 Elm 官方教程保持同步,增加計數(shù)器的數(shù)量,現(xiàn)在我們會有2個計數(shù)器。此外,還有一個“重置”按鈕,將兩個計數(shù)器同時重置為“0”;
首先,我們需要修改計數(shù)器組件,讓該組件支持重置操作。為此,我們將引入一個新函數(shù) init,其作用是為計數(shù)器構(gòu)造一個新狀態(tài) (count)。
function init() { return 0; }
init 在很多情況下都非常有用。例如,使用來自服務(wù)器或本地存儲的數(shù)據(jù)初始化狀態(tài)。它通過 JavaScript 對象創(chuàng)建一個豐富的數(shù)據(jù)模型(例如,為一個 JavaScript 對象添加一些原型屬性或方法)。
init 與 update 有一些區(qū)別:后者執(zhí)行一個更新操作,然后從一個狀態(tài)派生出新的狀態(tài);但是前者是使用一些輸入值(比如:默認(rèn)值、服務(wù)器數(shù)據(jù)等等)構(gòu)造一個狀態(tài),輸入值是可選的,而且完全不管前一個狀態(tài)是什么。
下面我們將通過一些代碼管理兩個計數(shù)器,我們在 towCounters.js 中實現(xiàn)我們的代碼。
首先,我們需要定義模型相關(guān)的操作類型:
//{ first : counter.model, second : counter.model } const RESET = Symbol("reset"); const UPDATE_FIRST = Symbol("update first"); const UPDATE_SECOND = Symbol("update second");
該模型導(dǎo)出兩個屬性:first 和 second 分別保存兩個計數(shù)器的狀態(tài)。我們定義了三個操作類型:第一個用來將計數(shù)器重置為 0,另外兩個后面也會講到。
組件通過 init 方法創(chuàng)建 state。
function init() { return { first: counter.init(), second: counter.init() }; }
view 函數(shù)負(fù)責(zé)展示這兩個計數(shù)器,并為用戶提供一個重置按鈕。
function view(model, handler) { return h("div", [ h("button", { on : { click: handler.bind(null, {type: RESET}) } }, "Reset"), h("hr"), counter.view(model.first, counterAction => handler({ type: UPDATE_FIRST, data: counterAction})), h("hr"), counter.view(model.second, counterAction => handler({ type: UPDATE_SECOND, data: counterAction})), ]); }
我們給 view 方法傳遞了兩個參數(shù):
每個視圖都會獲得父組件的部分狀態(tài)(model.first / model.second)
動態(tài)處理函數(shù),它會傳遞到每個子節(jié)點的 view 。比如:第一個計數(shù)器觸發(fā)了一個動作,我們會將 UPDATE_FIRST 封裝在 action 中,當(dāng)父類的 update 方法被調(diào)用時,我們會將計數(shù)器需要的 action(存儲在 data 屬性中)轉(zhuǎn)發(fā)到正確的計數(shù)器,并調(diào)用計數(shù)器的 update 方法。
下面看看 update 函數(shù)的實現(xiàn),并導(dǎo)出組件的所有屬性。
function update(model, action) { return action.type === RESET ? { first : counter.init(), second: counter.init() } : action.type === UPDATE_FIRST ? {...model, first : counter.update(model.first, action.data) } : action.type === UPDATE_SECOND ? {...model, second : counter.update(model.second, action.data) } : model; } export default { view, init, update, actions : { UPDATE_FIRST, UPDATE_SECOND, RESET } }
update 函數(shù)處理3個操作:
RESET 操作會調(diào)用 init 將每個計數(shù)器重置到默認(rèn)狀態(tài)。
UPDATE_FIRST 和 UPDATE_SECOND,會封裝一個計數(shù)器需要 action。函數(shù)將封裝好的 action 連同其 state 轉(zhuǎn)發(fā)給相關(guān)的子計數(shù)器。
{...model, prop: val}; 是 ES7 的對象擴展屬性(如object .assign),它總是返回一個新的對象。我們不修改參數(shù)中傳遞的 state ,而是始終返回一個相同屬性的新 state 對象,確保更新函數(shù)是一個純函數(shù)。
最后調(diào)用 main 方法,構(gòu)造頂層組件:
main( twoCounters.init(), // the initial state document.getElementById("placeholder"), twoCounters );
“towCounters” 展示了經(jīng)典的嵌套組件的使用模式:
組件通過類似于樹的層次結(jié)構(gòu)進行組織。
main 函數(shù)調(diào)用頂層組件的 view 方法,并將全局的初始狀態(tài)和處理回調(diào)(main handler)作為參數(shù)。
在視圖渲染的時候,父組件調(diào)用子組件的 view 函數(shù),并將子組件相關(guān)的 state 傳給子組件。
視圖將用戶事件轉(zhuǎn)化為對程序更有意義的 actions。
從子組件觸發(fā)的操作會通過父組件向上傳遞,直到頂層組件。與 DOM 事件的冒泡不同,父組件不會在此階段進行操作,它能做的就是將相關(guān)信息添加到 action 中。
在冒泡階段,父組件的 view 函數(shù)可以攔截子組件的 actions ,并擴展一些必要的數(shù)據(jù)。
該操作最終在主處理程序(main handler)中結(jié)束,主處理程序?qū)⑼ㄟ^調(diào)用頂部組件的 update 函數(shù)進行派發(fā)操作。
每個父組件的 update 函數(shù)負(fù)責(zé)將操作分派給其子組件的 update 函數(shù)。通常使用在冒泡階段添加了相關(guān)信息的 action。
案例三:計數(shù)器列表本例的源代碼在 counter-3 branch
讓我們繼續(xù)來看 Elm 的教程,我們將進一步擴展我們的示例,可以管理任意數(shù)量的計數(shù)器列表。此外還提供新增計數(shù)器和刪除計數(shù)器的按鈕。
“counter” 組件代碼保持不變,我們將定義一個新組件 counterList 來管理計數(shù)器數(shù)組。
我們先來定義模型,和一組關(guān)聯(lián)操作。
/* model : { counters: [{id: Number, counter: counter.model}], nextID : Number } */ const ADD = Symbol("add"); const UPDATE = Symbol("update counter"); const REMOVE = Symbol("remove"); const RESET = Symbol("reset");
組件的模型包括了兩個參數(shù):
一個由對象(id,counter)組成的列表,id 屬性與前面實例的 first 和 second 屬性作用類似;它將標(biāo)識每個計數(shù)器的唯一性。
nextID 用來維護一個做自動遞增的基數(shù),每個新添加的計數(shù)器都會使用 nextID + 1 來作為它的 ID。
接下來,我們定義 init 方法,它將構(gòu)造一個默認(rèn)的 state。
function init() { return { nextID: 1, counters: [] }; }
下面定義一個 view 函數(shù)。
function view(model, handler) { return h("div", [ h("button", { on : { click: handler.bind(null, {type: ADD}) } }, "Add"), h("button", { on : { click: handler.bind(null, {type: RESET}) } }, "Reset"), h("hr"), h("div.counter-list", model.counters.map(item => counterItemView(item, handler))) ]); }
視圖提供了兩個按鈕來觸發(fā)“添加”和“重置”操作。每個計數(shù)器的都通過 counterItemView 函數(shù)來生成虛擬 DOM。
function counterItemView(item, handler) { return h("div.counter-item", {key: item.id }, [ h("button.remove", { on : { click: e => handler({ type: REMOVE, id: item.id}) } }, "Remove"), counter.view(item.counter, a => handler({type: UPDATE, id: item.id, data: a})), h("hr") ]); }
該函數(shù)添加了一個 remove 按鈕在視圖中,并引用了計數(shù)器的 id 添加到 remove 的 action 中。
接下來看看 update 函數(shù)。
const resetAction = {type: counter.actions.INIT, data: 0}; function update(model, action) { return action.type === ADD ? addCounter(model) : action.type === RESET ? resetCounters(model) : action.type === REMOVE ? removeCounter(model, action.id) : action.type === UPDATE ? updateCounter(model, action.id, action.data) : model; } export default { view, update, actions : { ADD, RESET, REMOVE, UPDATE } }
該代碼遵循上一個示例的相同的模式,使用冒泡階段存儲的 id 信息,將子節(jié)點的 actions 轉(zhuǎn)發(fā)到頂層組件。下面是 update 的一個分支 “updateCounter” 。
function updateCounter(model, id, action) { return {...model, counters : model.counters.map(item => item.id !== id ? item : { ...item, counter : counter.update(item.counter, action) } ) }; }
上面這種模式可以應(yīng)用于任何樹結(jié)構(gòu)嵌套的組件結(jié)構(gòu)中,通過這種模式,我們讓整個應(yīng)用程序的結(jié)構(gòu)進行了統(tǒng)一。
在 actions 中使用 union 類型在前面的示例中,我們使用 ES6 的 Symbols 類型來表示操作類型。在視圖內(nèi)部,我們創(chuàng)建了帶有操作類型和附加信息(id,子節(jié)點的 action)的對象。
在真實的場景中,我們必須將 action 的創(chuàng)建邏輯移動到一個多帶帶的工廠函數(shù)中(類似于React/Flux中的 Action Creators)。在這篇文章的剩余部分,我將提出一個更符合 FP 精神的替代方案:union 類型。它是 FP 語言(如Haskell)中使用的 代數(shù)數(shù)據(jù)類型 的子集,您可以將它們看作具有更強大功能的枚舉。
union類型可以為我們提供以下特性:
定義一個可描述所有可能的 actions 的類型。
為每個可能的值提供一個工廠函數(shù)。
提供一個可控的流來處理所有可能的變量。
union 類型在 JavaScript 中不是原生的,但是我們可以使用一個庫來模擬它。在我們的示例中,我們使用 union-type (github/union-type) ,這是 snabbdom 作者編寫的一個小而美的庫。
先讓我們安裝這個庫:
npm install --save union-type
下面我們來定義計數(shù)器的 actions:
import Type from "union-type"; const Action = Type({ Increment : [], Decrement : [] });
Type 是該庫導(dǎo)出的唯一函數(shù)。我們使用它來定義 union 類型 Action,其中包含兩個可能的 actions。
返回的 Action 具有一組工廠函數(shù),用于創(chuàng)建所有可能的操作。
function view(count, handler) { return h("div", [ h("button", { on : { click: handler.bind(null, Action.Increment()) } }, "+"), h("button", { on : { click: handler.bind(null, Action.Decrement()) } }, "-"), h("div", `Count : ${count}`), ]); }
在 view 創(chuàng)建遞增和遞減兩種 action。update 函數(shù)展示了 uinon 如何對不同類型的 action 進行模式匹配。
function update(count, action) { return Action.case({ Increment : () => count + 1, Decrement : () => count - 1 }, action); }
Action 具有一個 case 方法,該方法接受兩個參數(shù):
一個對象(變量名和一個回調(diào)函數(shù))
要匹配的值
然后,case方法將提供的 action 與所有指定的變量名相匹配,并調(diào)用相應(yīng)的處理函數(shù)。返回值是匹配的回調(diào)函數(shù)的返回值。
類似地,我們看看如何定義 counterList 的 actions
const Action = Type({ Add : [], Remove : [Number], Reset : [], Update : [Number, counter.Action], });
Add和Reset是空數(shù)組(即它們沒有任何字段),Remove只有一個字段(計數(shù)器的 id)。最后,Update 操作有兩個字段:計數(shù)器的 id 和計數(shù)器觸發(fā)時的 action。
與之前一樣,我們在 update 函數(shù)中進行模式匹配。
function update(model, action) { return Action.case({ Add : () => addCounter(model), Remove : id => removeCounter(model, id), Reset : () => resetCounters(model), Update : (id, action) => updateCounter(model, id, action) }, action); }
注意,Remove 和 Update 都會接受參數(shù)。如果匹配成功,case 方法將從 case 實例中提取字段并將它們傳遞給對應(yīng)的回調(diào)函數(shù)。
所以典型的模式是:
將 actions 建模為union類型。
在 view 函數(shù)中,使用 union 類型提供的工廠函數(shù)創(chuàng)建 action (如果創(chuàng)建的邏輯更復(fù)雜,還可以將操作創(chuàng)建委托給多帶帶的函數(shù))。
在 update 函數(shù)中,使用 case 方法來匹配 union 類型的可能值。
TodoMVC例子在這個倉庫中(github/yelouafi/snabbdom-todomvc),使用本文提到的規(guī)范進行了 todoMVC 應(yīng)用的實現(xiàn)。應(yīng)用程序由2個模塊組成:
task.js 定義一個呈現(xiàn)單個任務(wù)并更新其狀態(tài)的組件
todos.js,它管理任務(wù)列表以及過濾和更新
總結(jié)我們已經(jīng)了解了如何使用小而美的虛 擬DOM 庫編寫應(yīng)用程序。當(dāng)我們不想被迫選擇使用React框架(尤其是 class),或者當(dāng)我們需要一個小型 JavaScript 庫時,這將非常有用。
Elm architecture 提供了一個簡單的模式來編寫復(fù)雜的虛擬DOM應(yīng)用,具有純函數(shù)的所有優(yōu)點。這為我們的代碼提供了一個簡單而規(guī)范的結(jié)構(gòu)。使用標(biāo)準(zhǔn)的模式使得應(yīng)用程序更容易維護,特別是在成員頻繁更改的團隊中。新成員可以快速掌握代碼的總體架構(gòu)。
由于完全用純函數(shù)實現(xiàn)的,我確信只要組件代碼遵守其約定,更改組件就不會產(chǎn)生不良的副作用。
想查看更多前端技術(shù)相關(guān)文章可以逛逛我的博客:自然醒的博客
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/104047.html
摘要:市面上竟然擁有多個虛擬庫。虛擬庫,就是出來后的一種新式庫,以虛擬與算法為核心,屏蔽操作,操作數(shù)據(jù)即操作視圖。及其他虛擬庫已經(jīng)將虛擬的生成交由與處理了,因此不同點是,虛擬的結(jié)構(gòu)與算法。因此虛擬庫是分為兩大派系算法派與擬態(tài)派。 去哪兒網(wǎng)迷你React是年初立項的新作品,在這前,去哪兒網(wǎng)已經(jīng)深耕多年,擁有QRN(react-native的公司制定版),HY(基于React的hybird方案)...
摘要:閱讀源碼的時候,想了解虛擬結(jié)構(gòu)的實現(xiàn),發(fā)現(xiàn)在的地方。然而慢慢的人們發(fā)現(xiàn),在我們的代碼中布滿了一系列操作的代碼。源碼解析系列源碼解析一準(zhǔn)備工作源碼解析二函數(shù)源碼解析三對象源碼解析四方法源碼解析五鉤子源碼解析六模塊源碼解析七事件處理個人博客地址 前言 虛擬 DOM 結(jié)構(gòu)概念隨著 react 的誕生而火起來,之后 vue2.0 也加入了虛擬 DOM 的概念。 閱讀 vue 源碼的時候,想了解...
摘要:毫無疑問的是算法的復(fù)雜度與效率是決定能夠帶來性能提升效果的關(guān)鍵因素。速度略有損失,但可讀性大大提高。因此目前的主流算法趨向一致,在主要思路上,與的方式基本相同。在里面實現(xiàn)了的算法與支持。是唯一添加的方法所以只發(fā)生在中。 VirtualDOM是react在組件化開發(fā)場景下,針對DOM重排重繪性能瓶頸作出的重要優(yōu)化方案,而他最具價值的核心功能是如何識別并保存新舊節(jié)點數(shù)據(jù)結(jié)構(gòu)之間差異的方法,...
摘要:很多人認(rèn)為虛擬最大的優(yōu)勢是算法,減少操作真實的帶來的性能消耗。雖然這一個虛擬帶來的一個優(yōu)勢,但并不是全部?;氐阶铋_始的問題,虛擬到底是什么,說簡單點,就是一個普通的對象,包含了三個屬性。 是什么? 虛擬 DOM (Virtual DOM )這個概念相信大家都不陌生,從 React 到 Vue ,虛擬 DOM 為這兩個框架都帶來了跨平臺的能力(React-Native 和 Weex)。因...
閱讀 1160·2021-10-14 09:42
閱讀 1487·2021-09-22 15:11
閱讀 3390·2019-08-30 15:56
閱讀 1323·2019-08-30 15:55
閱讀 3692·2019-08-30 15:55
閱讀 939·2019-08-30 15:44
閱讀 2092·2019-08-29 17:17
閱讀 2135·2019-08-29 15:37