[譯] 基於虛擬DOM(Snabbdom)的迷你React

Shenfq發表於2019-05-01

原文連結

原文寫於 2015-07-31,雖然時間比較久遠,但是對於我們理解虛擬 DOM 和 view 層之間的關係還是有很積極的作用的。

React 是 JavaScript 社群的新成員,儘管 JSX (在 JavaScript 中使用 HTML 語法)存在一定的爭議,但是對於虛擬 DOM 人們有不一樣的看法。

對於不熟悉的人來說,虛擬 DOM 可以描述為某個時刻真實DOM的簡單表示。其思想是:每次 UI 狀態發生更改時,重新建立一個虛擬 DOM,而不是直接使用命令式的語句更新真實 DOM ,底層庫將對應的更新對映到真實 DOM 上。

需要注意的是,更新操作並沒有替換整個 DOM 樹(例如使用 innerHTML 重新設定 HTML 字串),而是替換 DOM 節點中實際修改的部分(改變節點屬性、新增子節點)。這裡使用的是增量更新,通過比對新舊虛擬 DOM 來推斷更新的部分,然後將更新的部分通過補丁的方式更新到真實 DOM 中。

虛擬 DOM 因為高效的效能經常受到特別的關注。但是還有一項同樣重要的特性,虛擬 DOM 可以把 UI 表示為狀態函式的對映(PS. 也就是我們常說的 UI = render(state)),這也使得編寫 web 應用有了新的形式。

在本文中,我們將研究虛擬 DOM 的概念如何引用到 web 應用中。我們將從簡單的例子開始,然後給出一個架構來編寫基於 Virtual DOM 的應用。

為此我們將選擇一個獨立的 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'),          // 設定 DOM 元素的屬性
  require('snabbdom/modules/style'),          // 處理元素的 style ,支援動畫
  require('snabbdom/modules/eventlisteners'), // 事件處理
]);
複製程式碼

上面的程式碼中,我們初始化了 snabbdom 模組並新增了一些擴充套件。在 snabbdom 中,切換 class、style還有 DOM 元素上的屬性設定和事件繫結都是給不同模組實現的。上面的例項,只使用了預設提供的模組。

核心模組只暴露了一個 patch 方法,它由 init 方法返回。我們使用它建立初始化的 DOM,之後也會使用它來進行 DOM 的更新。

下面是一個 Hello World 示例:

import h from 'snabbdom/h';

var vnode = h('div', {style: {fontWeight: 'bold'}}, 'Hello world');
patch(document.getElementById('placeholder'), vnode);
複製程式碼

h 是一個建立虛擬 DOM 的輔助函式。我們將在文章後面介紹具體用法,現在只需要該函式的 3 個輸入引數:

  1. 一個 CSS 選擇器(jQuery 的選擇器),比如 div#id.class
  2. 一個可選的資料物件,它包含了虛擬節點的屬性(class、styles、events)。
  3. 一個字串或者虛擬 DOM 的陣列(用於表示該節點的children)。

第一次呼叫的時候,patch 方法需要一個 DOM 佔位符和一個初始的虛擬 DOM,然後它會根據虛擬 DOM 建立一個對應的真實 DO樹。在隨後的的呼叫中,我們為它提供新舊兩個虛擬 DOM,然後它通過 diff 演算法比對這兩個虛擬 DOM,並找出更新的部分對真實 DOM 進行必要的修改 ,使得真實的 DOM 樹為最新的虛擬 DOM 的對映。

為了快速上手,我在 GitHub 上建立了一個倉庫,其中包含了專案的必要內容。下面讓我們來克隆這個倉庫(yelouafi/snabbdom-starter),然後執行 npm install 安裝依賴。這個倉庫使用 Browserify 作為打包工具,檔案變更後使用 Watchify 自動重新構建,並且通過 Babel 將 ES6 的程式碼轉成相容性更好的 ES5。

下面執行如下程式碼:

npm run watch
複製程式碼

這段程式碼將啟動 watchify 模組,它會在 app 資料夾內,建立一個瀏覽器能夠執行的包:build.js 。模組還將檢測我們的 js 程式碼是否發生改變,如果有修改,會自動的重新構建 build.js。(如果你想手動構建,可以使用:npm run build

在瀏覽器中開啟 app/index.html 就能執行程式,這時候你會在螢幕上看到 “Hello World”。

這篇文中的所有案例都能在特定的分支上進行實現,我會在文中連結到每個分支,同時 README.md 檔案也包含了所有分支的連結。

動態檢視

本例的原始碼在 dynamic-view branch

為了突出虛擬 DOM 動態化的優勢,接下來會構建一個很簡單的時鐘。

首先修改 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);
複製程式碼

通過單獨的函式 view 來生成虛擬 DOM,它接受一個狀態(當前日期)作為輸入。

該案例展示了虛擬 DOM 的經典使用方式,在不同的時刻構造出新的虛擬 DOM,然後將新舊虛擬 DOM 進行對比,並更新到真實 DOM 上。案例中,我們每秒都構造了一個虛擬 DOM,並用它來更新真實 DOM。

事件響應

本例的原始碼在 event-reactivity branch

下面的案例介紹了通過事件系統完成一個打招呼的應用程式:

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 物件來設定元素的屬性,props 模組會對 props 物件進行處理。類似地,我們通過 on 物件進行元素的時間繫結,eventlistener 模組會對 on 物件進行處理。

上面的案例中,update 函式執行了與前面案例中 setInterval 類似的事情:從傳入的事件物件中提取出 input 的值,構造出一個新的虛擬 DOM,然後呼叫 patch ,用新的虛擬 DOM 樹更新真實 DOM。

複雜的應用程式

使用獨立的虛擬 DOM 庫的好處是,我們在構建自己的應用時,可以按照自己喜歡的方式來做。你可以使用 MVC 的設計模式,可以使用更現代化的資料流體系,比如 Flux。

在這篇文章中,我會介紹一種不太為人所知的架構模式,是我之前在 Elm(一種可編譯成 JavaScript 的 函式式語言)中使用過的。Elm 的開發者稱這種模式為 Elm Architecture,它的主要優點是允許我們將整個應用編寫為一組純函式。

主流程

讓我們回顧一下上個案例的主流程:

  1. 通過 view 函式構造出我們初始的虛擬 DOM,在 view 函式中,給 input 輸入框新增了一個 input 事件。
  2. 通過 patch 將虛擬 DOM 渲染到真實 DOM 中,並將 input 事件繫結到真實 DOM 上。
  3. 等待使用者輸入……
  4. 使用者輸入內容,觸發 input 事件,然後呼叫 update 函式
  5. 在 update 函式中,我們更新了狀態
  6. 我們傳入了新的狀態給 view 函式,並生成新的虛擬 DOM (與步驟 1 相同)
  7. 再次呼叫 patch,重複上述過程(與步驟 2 相同)

上面的過程可以描述成一個迴圈。如果去掉實現的一些細節,我們可以建立一個抽象的函式呼叫序列。

主流程

user 是使用者互動的抽象,我們得到的是函式呼叫的迴圈序列。注意,user 函式是非同步的,否則這將是一個無限的死迴圈。

讓我們將上述過程轉換為程式碼:

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 函式反映了上述的迴圈過程:給定一個初始狀態(initState),一個 DOM 節點和一個頂層元件(view + update),main 通過當前的狀態經過 view 函式構建出新的虛擬 DOM,然後通過補丁的方式更新到真實 DOM上。

傳遞給 view 函式的引數有兩個:首先是當前狀態,其次是事件處理的回撥函式,對生成的檢視中觸發的事件進行處理。回撥函式主要負責為應用程式構建一個新的狀態,並使用新的狀態重啟 UI 迴圈。

新狀態的構造委託給頂層元件的 update 函式,該函式是一個簡單的純函式:無論何時,給定當前狀態和當前程式的輸入(事件或行為),它都會為程式返回一個新的狀態。

要注意的是,除了 patch 方法會有副作用,主函式內不會有任何改變狀態行為發生。

main 函式有點類似於低階GUI框架的 main 事件迴圈,這裡的重點是收回對 UI 事件分發流程的控制: 在實際狀態下,DOM API通過採用觀察者模式強制我們進行事件驅動,但是我們不想在這裡使用觀察者模式,下面就會講到。

Elm 架構(Elm architecture)

Elm architecture

基於 Elm-architecture 的程式中,是由一個個模組或者說元件構成的。每個元件都有兩個基本函式:updateview,以及一個特定的資料結構:元件擁有的 model 以及更新該 model 例項的 actions

  1. update 是一個純函式,接受兩個引數:元件擁有的 model 例項,表示當前的狀態(state),以及一個 action 表示需要執行的更新操作。它將返回一個新的 model 例項。
  2. view 同樣接受兩個引數:當前 model 例項和一個事件通道,它可以通過多種形式傳播資料,在我們的案例中,將使用一個簡單的回撥函式。該函式返回一個新的虛擬 DOM,該虛擬 DOM 將會渲染成真實 DOM。

如上所述,Elm architecture 擺脫了傳統的由事件進行驅動觀察者模式。相反該架構傾向於集中式的管理資料(比如 React/Flux),任何的事件行為都會有兩種方式:

  1. 冒泡到頂層元件;
  2. 通過元件樹的形式進行下發,在此階段,每個元件都可以選擇自己的處理方式,或者轉發給其他一個或所有子元件。

該架構的另一個關鍵點,就是將程式需要的整個狀態都儲存在一個物件中。樹中的每個元件都負責將它們擁有的狀態的一部分傳遞給子元件。

在我們的案例中,我們將使用與 Elm 網站相同的案例,因為它完美的展示了該模式。

案例一:計數器

本例的原始碼在 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 元件由以下屬性組成:

  1. Model: 一個簡單的 Number
  2. View:為使用者提供兩個按鈕,使用者遞增、遞減計數器,以及顯示當前數字
  3. Update:接受兩個動作:INC / DEC,增加或減少計數器的值

首先要注意的是,view/update 都是純函式,除了輸入之外,他們不依賴任何外部環境。計數器元件本身不包括任何狀態或變數,它只會從給定的狀態構造出固定的檢視,以及通過給定的狀態更新檢視。由於其純粹性,計數器元件可以輕鬆的插入任何提供依賴(state 和 action)環境。

其次需要注意 handler.bind(null, action) 表示式,每次點選按鈕,事件監聽器都會觸發該函式。我們將原始的使用者事件轉換為一個有意義的操作(遞增或遞減),使用了 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
複製程式碼

案例二:兩個計數器

本例的原始碼在 counter-2 branch

我們將和 Elm 官方教程保持同步,增加計數器的數量,現在我們會有2個計數器。此外,還有一個“重置”按鈕,將兩個計數器同時重置為“0”;

首先,我們需要修改計數器元件,讓該元件支援重置操作。為此,我們將引入一個新函式 init,其作用是為計數器構造一個新狀態 (count)。

function init() {
  return 0;
}
複製程式碼

init 在很多情況下都非常有用。例如,使用來自伺服器或本地儲存的資料初始化狀態。它通過 JavaScript 物件建立一個豐富的資料模型(例如,為一個 JavaScript 物件新增一些原型屬性或方法)。

initupdate 有一些區別:後者執行一個更新操作,然後從一個狀態派生出新的狀態;但是前者是使用一些輸入值(比如:預設值、伺服器資料等等)構造一個狀態,輸入值是可選的,而且完全不管前一個狀態是什麼。

下面我們將通過一些程式碼管理兩個計數器,我們在 towCounters.js 中實現我們的程式碼。

首先,我們需要定義模型相關的操作型別:

//{ first : counter.model, second : counter.model }

const RESET         = Symbol('reset');
const UPDATE_FIRST  = Symbol('update first');
const UPDATE_SECOND = Symbol('update second');
複製程式碼

該模型匯出兩個屬性:first 和 second 分別儲存兩個計數器的狀態。我們定義了三個操作型別:第一個用來將計數器重置為 0,另外兩個後面也會講到。

元件通過 init 方法建立 state。

function init() {
  return { first: counter.init(), second: counter.init() };
}
複製程式碼

view 函式負責展示這兩個計數器,併為使用者提供一個重置按鈕。

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 方法傳遞了兩個引數:

  1. 每個檢視都會獲得父元件的部分狀態(model.first / model.second)
  2. 動態處理函式,它會傳遞到每個子節點的 view 。比如:第一個計數器觸發了一個動作,我們會將 UPDATE_FIRST 封裝在 action 中,當父類的 update 方法被呼叫時,我們會將計數器需要的 action(儲存在 data 屬性中)轉發到正確的計數器,並呼叫計數器的 update 方法。

下面看看 update 函式的實現,並匯出元件的所有屬性。

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 函式處理3個操作:

  1. RESET 操作會呼叫 init 將每個計數器重置到預設狀態。
  2. UPDATE_FIRSTUPDATE_SECOND,會封裝一個計數器需要 action。函式將封裝好的 action 連同其 state 轉發給相關的子計數器。

{...model, prop: val}; 是 ES7 的物件擴充套件屬性(如object .assign),它總是返回一個新的物件。我們不修改引數中傳遞的 state ,而是始終返回一個相同屬性的新 state 物件,確保更新函式是一個純函式。

最後呼叫 main 方法,構造頂層元件:

main(
  twoCounters.init(), // the initial state 
  document.getElementById('placeholder'), 
  twoCounters
);
複製程式碼

“towCounters” 展示了經典的巢狀元件的使用模式:

  1. 元件通過類似於樹的層次結構進行組織。
  2. main 函式呼叫頂層元件的 view 方法,並將全域性的初始狀態和處理回撥(main handler)作為引數。
  3. 在檢視渲染的時候,父元件呼叫子元件的 view 函式,並將子元件相關的 state 傳給子元件。
  4. 檢視將使用者事件轉化為對程式更有意義的 actions。
  5. 從子元件觸發的操作會通過父元件向上傳遞,直到頂層元件。與 DOM 事件的冒泡不同,父元件不會在此階段進行操作,它能做的就是將相關資訊新增到 action 中。
  6. 在冒泡階段,父元件的 view 函式可以攔截子元件的 actions ,並擴充套件一些必要的資料。
  7. 該操作最終在主處理程式(main handler)中結束,主處理程式將通過呼叫頂部元件的 update 函式進行派發操作。
  8. 每個父元件的 update 函式負責將操作分派給其子元件的 update 函式。通常使用在冒泡階段新增了相關資訊的 action。

案例三:計數器列表

本例的原始碼在 counter-3 branch

讓我們繼續來看 Elm 的教程,我們將進一步擴充套件我們的示例,可以管理任意數量的計數器列表。此外還提供新增計數器和刪除計數器的按鈕。

“counter” 元件程式碼保持不變,我們將定義一個新元件 counterList 來管理計數器陣列。

我們先來定義模型,和一組關聯操作。

/*
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');
複製程式碼

元件的模型包括了兩個引數:

  1. 一個由物件(id,counter)組成的列表,id 屬性與前面例項的 first 和 second 屬性作用類似;它將標識每個計數器的唯一性。
  2. nextID 用來維護一個做自動遞增的基數,每個新新增的計數器都會使用 nextID + 1 來作為它的 ID。

接下來,我們定義 init 方法,它將構造一個預設的 state。

function init() {
  return  { nextID: 1, counters: [] };
}
複製程式碼

下面定義一個 view 函式。

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)))
    
  ]); 
}
複製程式碼

檢視提供了兩個按鈕來觸發“新增”和“重置”操作。每個計數器的都通過 counterItemView 函式來生成虛擬 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')
  ]);
}
複製程式碼

該函式新增了一個 remove 按鈕在檢視中,並引用了計數器的 id 新增到 remove 的 action 中。

接下來看看 update 函式。

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 資訊,將子節點的 actions 轉發到頂層元件。下面是 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)
          }
    )
  };
}
複製程式碼

上面這種模式可以應用於任何樹結構巢狀的元件結構中,通過這種模式,我們讓整個應用程式的結構進行了統一。

在 actions 中使用 union 型別

在前面的示例中,我們使用 ES6 的 Symbols 型別來表示操作型別。在檢視內部,我們建立了帶有操作型別和附加資訊(id,子節點的 action)的物件。

在真實的場景中,我們必須將 action 的建立邏輯移動到一個單獨的工廠函式中(類似於React/Flux中的 Action Creators)。在這篇文章的剩餘部分,我將提出一個更符合 FP 精神的替代方案:union 型別。它是 FP 語言(如Haskell)中使用的 代數資料型別 的子集,您可以將它們看作具有更強大功能的列舉。

union型別可以為我們提供以下特性:

  1. 定義一個可描述所有可能的 actions 的型別。
  2. 為每個可能的值提供一個工廠函式。
  3. 提供一個可控的流來處理所有可能的變數。

union 型別在 JavaScript 中不是原生的,但是我們可以使用一個庫來模擬它。在我們的示例中,我們使用 union-type (github/union-type) ,這是 snabbdom 作者編寫的一個小而美的庫。

先讓我們安裝這個庫:

npm install --save union-type
複製程式碼

下面我們來定義計數器的 actions:

import Type from 'union-type';

const Action = Type({
  Increment : [],
  Decrement : []
});
複製程式碼

Type 是該庫匯出的唯一函式。我們使用它來定義 union 型別 Action,其中包含兩個可能的 actions。

返回的 Action 具有一組工廠函式,用於建立所有可能的操作。

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 建立遞增和遞減兩種 action。update 函式展示了 uinon 如何對不同型別的 action 進行模式匹配。

function update(count, action) {
  return  Action.case({
    Increment : () => count + 1,
    Decrement : () => count - 1
  }, action);
}
複製程式碼

Action 具有一個 case 方法,該方法接受兩個引數:

  • 一個物件(變數名和一個回撥函式)
  • 要匹配的值

然後,case方法將提供的 action 與所有指定的變數名相匹配,並呼叫相應的處理函式。返回值是匹配的回撥函式的返回值。

類似地,我們看看如何定義 counterList 的 actions

const Action = Type({
  Add     : [],
  Remove  : [Number],
  Reset   : [],
  Update  : [Number, counter.Action],
});
複製程式碼

AddReset是空陣列(即它們沒有任何欄位),Remove只有一個欄位(計數器的 id)。最後,Update 操作有兩個欄位:計數器的 id 和計數器觸發時的 action。

與之前一樣,我們在 update 函式中進行模式匹配。

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);
}
複製程式碼

注意,RemoveUpdate 都會接受引數。如果匹配成功,case 方法將從 case 例項中提取欄位並將它們傳遞給對應的回撥函式。

所以典型的模式是:

  • 將 actions 建模為union型別。
  • 在 view 函式中,使用 union 型別提供的工廠函式建立 action (如果建立的邏輯更復雜,還可以將操作建立委託給單獨的函式)。
  • 在 update 函式中,使用 case 方法來匹配 union 型別的可能值。

TodoMVC例子

在這個倉庫中(github/yelouafi/snabbdom-todomvc),使用本文提到的規範進行了 todoMVC 應用的實現。應用程式由2個模組組成:

  • task.js 定義一個呈現單個任務並更新其狀態的元件
  • todos.js,它管理任務列表以及過濾和更新

總結

我們已經瞭解瞭如何使用小而美的虛 擬DOM 庫編寫應用程式。當我們不想被迫選擇使用React框架(尤其是 class),或者當我們需要一個小型 JavaScript 庫時,這將非常有用。

Elm architecture 提供了一個簡單的模式來編寫複雜的虛擬DOM應用,具有純函式的所有優點。這為我們的程式碼提供了一個簡單而規範的結構。使用標準的模式使得應用程式更容易維護,特別是在成員頻繁更改的團隊中。新成員可以快速掌握程式碼的總體架構。

由於完全用純函式實現的,我確信只要元件程式碼遵守其約定,更改元件就不會產生不良的副作用。


想檢視更多前端技術相關文章可以逛逛我的部落格:自然醒的部落格

相關文章