從零開始編寫一個(gè) MVVM 框架(二)

憑空來寫一個(gè)框架是不現(xiàn)實(shí)的,所以在這篇文章中我們會嘗試寫一些簡單的應(yīng)用代碼十艾。
我們一開始先寫基礎(chǔ)的 JavaScript,然后將它重構(gòu)成基于 MVVM 的項(xiàng)目查剖。
我在這篇文章中所有代碼都是基于 JSbin 用 babel/ES-6 語法寫的飒泻。如果你對哪一行代碼有疑惑鞭光,可以在那里嘗試一下。

用 MVVM 方式編寫你的代碼

基礎(chǔ) js 方式寫的學(xué)生信息

我們繼續(xù)上一篇文章中提到的學(xué)生信息應(yīng)用泞遗。
如果我們想要完成這樣一個(gè)應(yīng)用惰许,我們可能從下面的代碼開始:

const student = {
  'first-name': 'Tracy',
  'last-name': 'Kent',
  'height': 170,
  'weight': 50,
}

const root = document.createElement('ul')

const nameLi = document.createElement('li')
const nameLabel = document.createElement('span')
nameLabel.textContent = 'Name: '
const name_ = document.createElement('span')
name_.textContent = student['first-name'] + ' ' + student['last-name']
nameLi.appendChild(nameLabel)
nameLi.appendChild(name_)

const heightLi = document.createElement('li')
const heightLabel = document.createElement('span')
heightLabel.textContent = 'Height: '
const height = document.createElement('span')
height.textContent = '' + student['height'] / 100 + 'm'
heightLi.appendChild(heightLabel)
heightLi.appendChild(height)

const weightLi = document.createElement('li')
const weightLabel = document.createElement('span')
weightLabel.textContent = 'Weight: '
const weight = document.createElement('span')
weight.textContent = '' + student['weight'] + 'kg'
weightLi.appendChild(weightLabel)
weightLi.appendChild(weight)

root.appendChild(nameLi)
root.appendChild(heightLi)
root.appendChild(weightLi)

document.body.appendChild(root)

輸出內(nèi)容就是一個(gè)像下面的列表:

  • Name: Tracy Kent
  • Height: 1.7m
  • Weight: 50kg

一個(gè)三行的列表,卻花費(fèi)了這么多的代碼刹孔,有點(diǎn)恐怖吧啡省?

為了復(fù)用而重構(gòu)

為啥說有的程序員都沉迷于各種各樣的最佳實(shí)踐娜睛?
那是因?yàn)樗麄兊膽卸琛?br> 懶惰于程序員們而言,卻是一種美德卦睹。
復(fù)用畦戒,是這個(gè)行業(yè)中最棒的想法之一。我們現(xiàn)在的應(yīng)用中重復(fù)了很多行代碼结序,然而程序設(shè)計(jì)中一個(gè)為人們所普遍接受的觀點(diǎn)是 “DRY”:
別重復(fù)你自己的工作(Do not Repeat Yourself).
現(xiàn)在障斋,讓我們減少這個(gè)應(yīng)用重復(fù)的代碼。
我們可以發(fā)現(xiàn)徐鹤,我們執(zhí)行了很多次 document.createElement 來為列表創(chuàng)建 HTML 節(jié)點(diǎn)垃环。事實(shí)上,我們不需要這樣做返敬,因?yàn)樗械牧斜碓毓蚕硗瑯拥慕Y(jié)構(gòu)遂庄。
所以,那應(yīng)該是一個(gè)共享函數(shù)劲赠。
首先涛目,我們先為函數(shù)復(fù)制行的部分:

const createListItem = function (label, content) {
  const nameLi = document.createElement('li')
  const nameLabel = document.createElement('span')
  nameLabel.textContent = 'Name: '
  const name_ = document.createElement('span')
  name_.textContent = student['first-name'] + ' ' + student['last-name']
  nameLi.appendChild(nameLabel)
  nameLi.appendChild(name_)
}

光是這樣好像并不正常運(yùn)行,于是我們再修復(fù)一下:

const createListItem = function (label, content) {
  const li = document.createElement('li')
  const labelSpan = document.createElement('span')
  labelSpan.textContent = label
  const contentSpan = document.createElement('span')
  contentSpan.textContent = content
  li.appendChild(labelSpan)
  li.appendChild(contentSpan)
  return li
}

于是凛澎,整個(gè)應(yīng)用就變成了:

const student = {
  'first-name': 'Tracy',
  'last-name': 'Kent',
  'height': 170,
  'weight': 50,
}

const createListItem = function (label, content) {
  const li = document.createElement('li')
  const labelSpan = document.createElement('span')
  labelSpan.textContent = label
  const contentSpan = document.createElement('span')
  contentSpan.textContent = content
  li.appendChild(labelSpan)
  li.appendChild(contentSpan)
  return li
}

const root = document.createElement('ul')

const nameLi = createListItem('Name: ', student['first-name'] + ' ' + student['last-name'])

const heightLi = createListItem('Height: ', student['height'] / 100 + 'm')

const weightLi = createListItem('Weight: ', student['weight'] + 'kg')

root.appendChild(nameLi)
root.appendChild(heightLi)
root.appendChild(weightLi)

document.body.appendChild(root)

變得更簡短且易讀了吧霹肝。
(老版本里,)你一眼看不出在在一堆節(jié)點(diǎn)創(chuàng)建的代碼里我到底在干什么塑煎,但是新版本里沫换,很明顯我在創(chuàng)建一個(gè)列表和它的元素。
對于閱讀里代碼的人最铁,可能他們并不在意你如何創(chuàng)建列表元素讯赏,他們知道你在創(chuàng)建一個(gè)列表元素就夠了;對于那些在意列表元素的人炭晒,他們可以只查閱 createListItem 函數(shù)待逞,而不考慮你如何創(chuàng)建你的列表。于是网严,應(yīng)用就變成了:

const student = {
  'first-name': 'Tracy',
  'last-name': 'Kent',
  'height': 170,
  'weight': 50,
}

// The list creation util
const createList = function(kvPairs){
  const createListItem = function (label, content) {
    const li = document.createElement('li')
    const labelSpan = document.createElement('span')
    labelSpan.textContent = label
    const contentSpan = document.createElement('span')
    contentSpan.textContent = content
    li.appendChild(labelSpan)
    li.appendChild(contentSpan)
    return li
  }

  const root = document.createElement('ul')
  kvPairs.forEach(function (x) {
    root.appendChild(createListItem(x.key, x.value))
  })
  return root
}

//The business logic
const ul = createList([
  {
    key: 'Name: ',
    value: student['first-name'] + ' ' + student['last-name']
  },
  {
    key: 'Height: ',
    value: student['height'] / 100 + 'm'
  },
  {
    key: 'Weight: ',
    value: student['weight'] + 'kg'
  }])

document.body.appendChild(ul)

朝 MVVM 更進(jìn)一步

說真的识樱,現(xiàn)在我們的應(yīng)用已經(jīng)多少有了點(diǎn) MVVM 的風(fēng)格。
student 對象是我們的原始數(shù)據(jù)震束,在我們的重構(gòu)中怜庸,它永遠(yuǎn)不曾變動,我們可以稱之為“模型”垢村。createList 函數(shù)返回了我們需要展示的 DOM 樹割疾,我認(rèn)為它理論上可以被稱為“視圖”。而 “View-Model” 呢嘉栓?不幸的是宏榕,到目前為止拓诸,我們還有沒獨(dú)立出一個(gè)“View-Model”。是的麻昼,我是說奠支,“View-Model”沒有被獨(dú)立出來,但事實(shí)上抚芦,它是存在的倍谜。我們傳遞到 createList 中的參數(shù)是“模型”轉(zhuǎn)換之后的結(jié)果,換句話說叉抡,我們通過手動創(chuàng)建的數(shù)組來使得“模型”和“視圖”相適配尔崔。
下面來將其獨(dú)立出來:

//Model
const tk = {
  'first-name': 'Tracy',
  'last-name': 'Kent',
  'height': 170,
  'weight': 50,
}

//View
const createList = function(kvPairs){
  const createListItem = function (label, content) {
    const li = document.createElement('li')
    const labelSpan = document.createElement('span')
    labelSpan.textContent = label
    const contentSpan = document.createElement('span')
    contentSpan.textContent = content
    li.appendChild(labelSpan)
    li.appendChild(contentSpan)
    return li
  }

  const root = document.createElement('ul')
  kvPairs.forEach(function (x) {
    root.appendChild(createListItem(x.key, x.value))
  })
  return root
}

//View-Model
const formatStudent = function (student) {
  return [
    {
      key: 'Name: ',
      value: student['first-name'] + ' ' + student['last-name']
    },
    {
      key: 'Height: ',
      value: student['height'] / 100 + 'm'
    },
    {
      key: 'Weight: ',
      value: student['weight'] + 'kg'
    }]
}

const ul = createList(formatStudent(tk))

document.body.appendChild(ul)

這看起來好多了,除了最后兩行……
好吧褥民,再把它們封裝一下:

const run = function (root, {model, view, vm}) {
  const rendered = view(vm(model))
  root.appendChild(rendered)
}

run(document.body, {
      model: tk, 
      view: createList, 
      vm: formatStudent
})

需求變更:BMI(Body Mass Index季春,身體質(zhì)量指數(shù))

比如說,我們的產(chǎn)品經(jīng)理要求我們要為學(xué)生信息添加一個(gè)新的字段 BMI轴捎。在原來的代碼基礎(chǔ)之上要實(shí)現(xiàn)這樣的功能缺失讓人惱火鹤盒。至少我不會那樣做,我討厭頻繁的復(fù)制和粘貼 document.createElement侦副。
相較而言,在 MVVM 版本中驼鞭,它卻變得輕松了:我們只需要修改 “View-Model”秦驯,因?yàn)?BMI 可以通過 heightweight 字段計(jì)算得出。

const formatStudent = function (student) {
  return [
    {
      key: 'Name: ',
      value: student['first-name'] + ' ' + student['last-name']
    },
    {
      key: 'Height: ',
      value: student['height'] / 100 + 'm'
    },
    {
      key: 'Weight: ',
      value: student['weight'] + 'kg'
    },
    {
      key: 'BMI: ',
      value:  student['weight'] / (student['height'] * student['height'] / 10000)
    }]
}

我們可以選擇像這樣簡單的完成它挣棕,又或者在函數(shù)內(nèi)部做一些優(yōu)化译隘,但這不是我們這里要討論的問題。
我想表達(dá)的是:
為啥我們要選擇修改 “View-Model”洛心?
在 MVVM 模式中固耘,當(dāng)需要作出修改的時(shí)候,我們的首選總是傾向于修改“View-Model”词身。我認(rèn)為這不難理解:
視圖可能被用于展示其它的數(shù)據(jù)集厅目;它只關(guān)注數(shù)據(jù)該被如何展示。
模型可能被展示成其它模式法严;它只關(guān)注業(yè)務(wù)中發(fā)生了什么损敷。
它們都有被復(fù)用的潛力。所以我們最好要保持它們的通用性深啤。
“View-Model”你是基本上沒有辦法復(fù)用的拗馒;它相當(dāng)于是一個(gè)專門的針對于一個(gè)特定視圖和一個(gè)特定模型的適配器。
因?yàn)樗菍iT的溯街,修改它將不會讓你面臨程序其它地方崩潰的風(fēng)險(xiǎn)诱桂。但是如果你想修改視圖或者模型洋丐,你需要檢查所有它們被使用過的地方。

轉(zhuǎn)換 height 的度量單位

在中國挥等,有一個(gè)笑話是友绝,一個(gè)程序員可以和除了產(chǎn)品經(jīng)理之外的任何人稱為朋友,因?yàn)楫a(chǎn)品經(jīng)理總是變更他們的需求:)触菜。
假設(shè)產(chǎn)品經(jīng)理告訴你九榔,要添加一個(gè)轉(zhuǎn)化 height 字段單位的功能……
事實(shí)上,
我并不想解釋大量關(guān)于如何管理用戶輸入的東西涡相,這會很復(fù)雜哲泊,所以我計(jì)劃在后面的文章中討論它。但是在用戶界面開發(fā)的時(shí)候催蝗,用戶輸入又是如此的重要切威,所以我覺得還是有必要在這個(gè)問題上多說兩句。
為了添加一個(gè)按鈕丙号,我們需要修改我們的視圖先朦,而我們的視圖又可能在其它地方被復(fù)用犬缨,所以我們不應(yīng)該草率地修改我們當(dāng)前的視圖。
這里我們將通過我們將舊的視圖與一些新代碼相結(jié)合來復(fù)用它怀薛。
首先,我們需要一些新的代碼來負(fù)責(zé)現(xiàn)在的量度枝恋,所以我們引入了一個(gè)新的模型创倔。

const tk = {
  'first-name': 'Tracy',
  'last-name': 'Kent',
  'height': 170,
  'weight': 50
}

const measurement = 'cm'

我們添加了一條測量數(shù)據(jù)畦攘,而非修改 tk:所以 tk 仍舊能被其它模塊所復(fù)用。
對于視圖部分十电,我們可以將之前的列表視圖作為我們新視圖的一個(gè)部分:

const createList = function(kvPairs){
  const createListItem = function (label, content) {
    const li = document.createElement('li')
    const labelSpan = document.createElement('span')
    labelSpan.textContent = label
    const contentSpan = document.createElement('span')
    contentSpan.textContent = content
    li.appendChild(labelSpan)
    li.appendChild(contentSpan)
    return li
  }

  const root = document.createElement('ul')
  kvPairs.forEach(function (x) {
    root.appendChild(createListItem(x.key, x.value))
  })
  return root
}

const createToggle = function (options) {
  const createRadio = function (name, opt){
    const radio = document.createElement('input')
    radio.name = name
    radio.value = opt.value
    radio.type = 'radio'
    radio.textContent = opt.value
    radio.addEventListener('click', opt.onclick)
    radio.checked = opt.checked

    return radio
  }

  const root = document.createElement('form')
  options.opts.forEach(function (x) {
    root.appendChild(createRadio(options.name, x))
    root.appendChild(document.createTextNode(x.value))
  })

  return root
}

const createToggleableList = function(vm){
  const listView = createList(vm.kvPairs)
  const toggle = createToggle(vm.options)

  const root = document.createElement('div')
  root.appendChild(toggle)
  root.appendChild(listView)

  return root
}

我們的 createToggle 函數(shù)返回一個(gè)帶有一系列單選按鈕的表單。但是從目前的代碼來看摆出,我們無法得知它在我們的應(yīng)用中將扮演的角色。換句話說偎漫,它是同業(yè)務(wù)解耦的爷恳。
最后象踊,View-Model 部分:
如你所見温亲,createToggleableList 函數(shù)需要一個(gè)和我們之前的 createList 函數(shù)不同的參數(shù)棚壁。
所以在 View-Model 上重構(gòu)是必須的。

const createVm = function (model) {
  const calcHeight = function (measurement, cms) {
    if (measurement === 'm'){
      return cms / 100 + 'm'
    }else{
      return cms + 'cm'
    }
  }

  const options = {
    name: 'measurement',
    opts: [
      {
        value: 'cm',
        checked: model.measurement === 'cm',
        onclick: () => model.measurement = 'cm'
      },
      {
        value: 'm',
        checked: model.measurement === 'm',
        onclick: () => model.measurement = 'm'
      }
    ]
  }

  const kvPairs = [
    {
      key: 'Name: ',
      value: model.student['first-name'] + ' ' + model.student['last-name']
    },
    {
      key: 'Height: ',
      value: calcHeight(model.measurement, model.student['height'])
    },
    {
      key: 'Weight: ',
      value: model.student['weight'] + 'kg'
    },
    {
      key: 'BMI: ',
      value:  model.student['weight'] / (model.student['height'] * model.student['height'] / 10000)
    }]
  return {kvPairs, options}
}

我們?yōu)? createToggle 添加了 opt 參數(shù)栈虚,我們使用不同的公式來計(jì)算 height袖外;當(dāng)仍和一個(gè)單選按鈕被點(diǎn)擊的時(shí)候,這個(gè)模型的度量單位將發(fā)生變化魂务。

看起來很完美曼验,但實(shí)際上當(dāng)你點(diǎn)擊單選按鈕的時(shí)候,它并不會生效粘姜。因?yàn)槲覀儾]有數(shù)據(jù)變化時(shí)的更新機(jī)制鬓照。
這一部分,關(guān)于一個(gè) MVVM 框架如何處理模型更新孤紧,有一點(diǎn)糾結(jié)(思路并不難)豺裆。我將把它留到后面的文章中。
這里号显,我們將使用一種幾乎最簡單的方式實(shí)現(xiàn)它臭猜。

const run = function (root, {model, view, vm}) {
  let m = {...model}
  let m_old = {}

  setInterval( function (){
    if(!_.isEqual(m, m_old)){
      const rendered = view(vm(m))
      root.innerHTML = ''
      root.appendChild(rendered)

      m_old = {...m}
    }
  },1000)
}

run(document.body, {
      model: {student:tk, measurement}, 
      view: createToggleableList, 
      vm: createVm 
})

這種機(jī)制在計(jì)算機(jī)領(lǐng)域被稱為“輪詢”。在你的應(yīng)用運(yùn)行在瀏覽器中時(shí)押蚤,使用它并不是一個(gè)好的想法蔑歌。雖然它被瀏覽器廣泛地使用:)。
這里揽碘,我們引入一個(gè)國外的庫丐膝。我懶得去自己實(shí)現(xiàn)一個(gè) isEqual 函數(shù)。所以我使用 lodash 來檢查模型的更新钾菊。

每秒鐘,run 函數(shù)都將檢查是否有模型更新:如果更新了偎肃,我們將重新渲染整個(gè)視圖(當(dāng)你有大量的 DOM
節(jié)點(diǎn)的時(shí)候煞烫,這將導(dǎo)致性能問題);否則累颂,我們什么也不做滞详,靜候下一秒。
這是一個(gè)簡單的 MVVM 風(fēng)格的應(yīng)用的示例紊馏,下一篇文章料饥,我們將基于它創(chuàng)建一個(gè) MVVM 框架的示例。

< 上一篇?????????????????????????????????????????????????????????????????????????????????????????????????????????下一篇 >


原文地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末朱监,一起剝皮案震驚了整個(gè)濱河市岸啡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赫编,老刑警劉巖巡蘸,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悦荒,死亡現(xiàn)場離奇詭異,居然都是意外死亡境氢,警方通過查閱死者的電腦和手機(jī)碰纬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門嘀趟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牛隅,你說我怎么就攤上這事酌泰。” “怎么了默伍?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵也糊,是天一觀的道長羡宙。 經(jīng)常有香客問我,道長狗热,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮熟丸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隙弛。我一直安慰自己,他們只是感情好叉寂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布屏鳍。 她就那樣靜靜地躺著,像睡著了一般钓瞭。 火紅的嫁衣襯著肌膚如雪淫奔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天鸭丛,我揣著相機(jī)與錄音唐责,去河邊找鬼鼠哥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛朴恳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贞绵,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谴垫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乳怎,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蚪缀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后询枚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刷后,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了护桦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贪染,死狀恐怖抑进,靈堂內(nèi)的尸體忽然破棺而出睡陪,到底是詐尸還是另有隱情,我是刑警寧澤兰迫,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布汁果,位于F島的核電站,受9級特大地震影響鳄乏,放射性物質(zhì)發(fā)生泄漏棘利。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一水援、第九天 我趴在偏房一處隱蔽的房頂上張望蜗元。 院中可真熱鬧,春花似錦奕扣、人聲如沸成畦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽离斩。三九已至瘪匿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間核偿,已是汗流浹背顽染。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尼荆,地道東北人捅儒。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓振亮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坊秸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,144評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件荸恕、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,104評論 4 62
  • 自然界是奇妙的融求,一花一草生宛,都存在著它成長的軌跡。而觀察陷舅,便是對于奇妙事物最好的闡述莱睁,讓我們豎起耳朵芒澜,聆聽大自...
    燕子_2f24閱讀 294評論 0 0
  • 兒時(shí),您扶我蹣跚學(xué)步南吮,晚年誊酌,我是否可以攙您夕陽漫步? 兒時(shí)砚尽,您逗我樂哄我笑辉词,晚年,我是否可以陪您生活喜樂敷搪? 兒時(shí)幢哨,...
    波斯貓123閱讀 235評論 1 1