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