摘要:框架的誕生以上便是一個(gè)簡(jiǎn)短精簡(jiǎn)的風(fēng)格的學(xué)生信息的示例。至此,一個(gè)精簡(jiǎn)的框架其實(shí)已經(jīng)出來(lái)了什么你確定不是在開(kāi)玩笑一個(gè)只有十行的框架請(qǐng)記住框架是對(duì)如何組織代碼和整個(gè)項(xiàng)目如何通用運(yùn)作的抽象。
前言
MVVM模式相信做前端的人都不陌生,去網(wǎng)上搜MVVM,會(huì)出現(xiàn)一大堆關(guān)于MVVM模式的博文,但是這些博文大多都只是用圖片和文字來(lái)進(jìn)行抽象的概念講解,對(duì)于剛接觸MVVM模式的新手來(lái)說(shuō),這些概念雖然能夠讀懂,但是也很難做到理解透徹。因此,我寫(xiě)了這篇文章。
這篇文章旨在通過(guò)代碼的形式讓大家更好的理解MVVM模式,相信大多數(shù)人讀了這篇文章之后再去看其他諸如regular、vue等基于MVVM模式框架的源碼,會(huì)容易很多。
如果你對(duì)MVVM模式已經(jīng)很熟悉并且也已經(jīng)研讀過(guò)并深刻理解了當(dāng)下主流的前端框架,可以忽略下面的內(nèi)容。如果你沒(méi)有一點(diǎn)JavaScript基礎(chǔ),也請(qǐng)先去學(xué)習(xí)下再來(lái)閱讀讀此文。
引子來(lái)張圖來(lái)鎮(zhèn)壓此文:
MVVM是Model-View-ViewModel的縮寫(xiě)。簡(jiǎn)單的講,它將View與Model層分隔開(kāi),利用ViewModel層將Model層的數(shù)據(jù)經(jīng)過(guò)一定的處理變成適用于View層的數(shù)據(jù)結(jié)構(gòu)并傳送到View層渲染界面,同時(shí)View層的視圖更新也會(huì)告知ViewModel層,然后ViewModel層再更新Model層的數(shù)據(jù)。
我們用一段學(xué)生信息的代碼作為引子,然后一步步再重構(gòu)成MVVM模式的樣子。
編寫(xiě)類(lèi)似下面結(jié)構(gòu)的學(xué)生信息:
Name: Jessica Bre
Height: 1.8m
Weight: 70kg
用常規(guī)的js代碼是這樣的:
const student = { "first-name": "Jessica", "last-name": "Bre", "height": 180, "weight": 70, } 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)
好長(zhǎng)的一堆代碼呀!別急,下面我們一步步優(yōu)化!
DRY一下如何程序設(shè)計(jì)中最廣泛接受的規(guī)則之一就是“DRY”: "Do not Repeat Yourself"。很顯然,上面的一段代碼有很多重復(fù)的部分,不僅與這個(gè)準(zhǔn)則相違背,而且給人一種不舒服的感覺(jué)。是時(shí)候做下處理,來(lái)讓這段學(xué)生信息更"Drier"。
可以發(fā)現(xiàn),代碼里寫(xiě)了很多遍document.createElement來(lái)創(chuàng)建節(jié)點(diǎn),但是由于列表項(xiàng)都是相似的結(jié)構(gòu),所以我們沒(méi)有必要一遍一遍的寫(xiě)。因此,進(jìn)行如下封裝:
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 }
經(jīng)過(guò)這步轉(zhuǎn)化之后,整個(gè)學(xué)生信息應(yīng)用就變成了這樣:
const student = { "first-name": "Jessica", "last-name": "Bre", "height": 180, "weight": 70, } 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)
是不是變得更短了,也更易讀了?即使你不看createListItem函數(shù)的實(shí)現(xiàn),光看const nameLi = createListItem("Name: ", student["first-name"] + " " + student["last-name"])也能大致明白這段代碼時(shí)干什么的。
但是上面的代碼封裝的還不夠,因?yàn)槊看蝿?chuàng)建一個(gè)列表項(xiàng),我們都要多調(diào)用一遍createListItem,上面的代碼為了創(chuàng)建name,height,weight標(biāo)簽,調(diào)用了三遍createListItem,這里顯然還有精簡(jiǎn)的空間。因此,我們?cè)龠M(jìn)一步封裝:
const student = { "first-name": "Jessica", "last-name": "Bre", "height": 180, "weight": 70, } 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 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)
有沒(méi)有看到MVVM風(fēng)格的影子?student對(duì)象是原始數(shù)據(jù),相當(dāng)于Model層;createList創(chuàng)建了dom樹(shù),相當(dāng)于View層,那么ViewModel層呢?仔細(xì)觀(guān)察,其實(shí)我們傳給createList函數(shù)的參數(shù)就是Model的數(shù)據(jù)的改造,為了讓Model的數(shù)據(jù)符合View的結(jié)構(gòu),我們做了這樣的改造,因此雖然這段函數(shù)里面沒(méi)有獨(dú)立的ViewModel層,但是它確實(shí)是存在的!聰明的同學(xué)應(yīng)該想到了,下一步就是來(lái)獨(dú)立出ViewModel層了吧~
// Model const tk = { "first-name": "Jessica", "last-name": "Bre", "height": 180, "weight": 70, } //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 } //ViewModel 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 })
這種寫(xiě)法,熟悉vue或者regular的同學(xué),應(yīng)該會(huì)覺(jué)得似曾相識(shí)吧?
讓我們來(lái)加點(diǎn)互動(dòng)前面學(xué)生信息的身高的單位都是默認(rèn)m,如果新增一個(gè)需求,要求學(xué)生的身高的單位可以在m和cm之間切換呢?
首先需要一個(gè)變量來(lái)保存度量單位,因此這里必須用一個(gè)新的Model:
const tk = { "first-name": "Jessica", "last-name": "Bre", "height": 180, "weight": 70, } const measurement = "cm"
為了讓tk更方便的被其他模塊重用,這里選擇增加一個(gè)measurement數(shù)據(jù)源,而不是直接修改tk。
在視圖部分要增加一個(gè)radio單選表單,用來(lái)切換身高單位。
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 }
接下來(lái)是ViewModel部分,createToggleableList函數(shù)需要與之前的createList函數(shù)不同的參數(shù)。因此,對(duì)View-Model結(jié)構(gòu)重構(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} }
這里為createToggle添加了ops,并且將ops封裝成了一個(gè)對(duì)象。根據(jù)度量單位,使用不同的方式去計(jì)算身高。當(dāng)任何一個(gè)radio被點(diǎn)擊,數(shù)據(jù)的度量單位將會(huì)改變。
看上去很完美,但是當(dāng)你點(diǎn)擊radio標(biāo)簽的時(shí)候,視圖不會(huì)有任何改變。因?yàn)檫@里還沒(méi)有為視圖做更新算法。有關(guān)MVVM如何處理視圖更新,那是一個(gè)比較大的課題,需要另辟一個(gè)博文來(lái)講,由于本文寫(xiě)的是一個(gè)精簡(jiǎn)的MVVM框架,這里就不再贅述,并用最簡(jiǎn)單的方式實(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 })
上述代碼引用了一個(gè)外部庫(kù)lodash的isEqual方法來(lái)比較數(shù)據(jù)模型是否有更新。此段代碼應(yīng)用了輪詢(xún),每秒都會(huì)檢測(cè)數(shù)據(jù)是否發(fā)生變化,有變化了再更新視圖。這是最笨的方法,并且在DOM結(jié)構(gòu)比較復(fù)雜時(shí),性能也會(huì)受到很大的影響。還是同樣的話(huà),本文的主題是一個(gè)精簡(jiǎn)的MVVM框架,因此略去了很多細(xì)節(jié)性的東西,只把主要的東西提煉出來(lái),以達(dá)到更好的理解MVVM模式的目的。
MVVM框架的誕生以上便是一個(gè)簡(jiǎn)短精簡(jiǎn)的MVVM風(fēng)格的學(xué)生信息的示例。至此,一個(gè)精簡(jiǎn)的MVVM框架其實(shí)已經(jīng)出來(lái)了:
/** * @param {Node} root * @param {Object} model * @param {Function} view * @param {Function} vm */ 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) }
什么?你確定不是在開(kāi)玩笑?一個(gè)只有十行的框架?請(qǐng)記住:
框架是對(duì)如何組織代碼和整個(gè)項(xiàng)目如何通用運(yùn)作的抽象。
這并不意味著你應(yīng)該有一堆代碼或混亂的類(lèi),盡管企業(yè)可用的API列表經(jīng)常都很可怕的長(zhǎng)。但是如果你研讀一個(gè)框架倉(cāng)庫(kù)的核心文件夾,你可能發(fā)現(xiàn)它會(huì)出乎意料的?。ㄏ啾扔谡麄€(gè)項(xiàng)目來(lái)說(shuō))。其核心代碼包含主要工作進(jìn)程,而其他部分只是幫助開(kāi)發(fā)人員以更加舒適的方式構(gòu)建應(yīng)用程序的附件。有興趣的同學(xué)可以去看看cycle.js,這個(gè)框架只有124行(包含注釋和空格)。
總結(jié)此時(shí)用一張圖來(lái)作為總結(jié)再好不過(guò)了!
當(dāng)然這里還有很多細(xì)節(jié)需要進(jìn)一步探討,比如如何選擇或設(shè)計(jì)一個(gè)更加友好的View層的視圖工具,如何更新和何時(shí)更新視圖比較合適等等。如果把這些問(wèn)題都解決了,相信這種MVVM框架會(huì)更加健壯。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/91828.html
摘要:前端日?qǐng)?bào)精選深入理解綁定請(qǐng)使用千位分隔符逗號(hào)表示網(wǎng)頁(yè)中的大數(shù)字跨頁(yè)面通信的各種姿勢(shì)你所不知道的濾鏡技巧與細(xì)節(jié)代碼質(zhì)量管控復(fù)雜度檢測(cè)中文翻譯基于與的三種代碼分割范式掘金系列如何構(gòu)建應(yīng)用程序冷星的前端雜貨鋪第期美團(tuán)旅行前端技術(shù)體系 2017-09-16 前端日?qǐng)?bào) 精選 深入理解 js this 綁定請(qǐng)使用千位分隔符(逗號(hào))表示web網(wǎng)頁(yè)中的大數(shù)字跨頁(yè)面通信的各種姿勢(shì)你所不知道的 CSS 濾...
摘要:在模式中一般把層算在層中,只有在理想的雙向綁定模式下,才會(huì)完全的消失。層將通過(guò)特定的展示出來(lái),并在控件上綁定視圖交互事件,一般由框架自動(dòng)生成在瀏覽器中。三大框架的異同三大框架都是數(shù)據(jù)驅(qū)動(dòng)型的框架及是雙向數(shù)據(jù)綁定是單向數(shù)據(jù)綁定。 MVVM相關(guān)概念 1) MVVM典型特點(diǎn)是有四個(gè)概念:Model、View、ViewModel、綁定器。MVVM可以是單向綁定也可以是雙向綁定甚至是不綁...
摘要:關(guān)于雙向數(shù)據(jù)綁定當(dāng)我們?cè)谇岸碎_(kāi)發(fā)中采用的模式時(shí),,指的是模型,也就是數(shù)據(jù),,指的是視圖,也就是頁(yè)面展現(xiàn)的部分。參考沉思錄一數(shù)據(jù)綁定雙向數(shù)據(jù)綁定實(shí)現(xiàn)數(shù)據(jù)與視圖的綁定與同步,最終體現(xiàn)在對(duì)數(shù)據(jù)的讀寫(xiě)處理過(guò)程中,也就是定義的數(shù)據(jù)函數(shù)中。 關(guān)于雙向數(shù)據(jù)綁定 當(dāng)我們?cè)谇岸碎_(kāi)發(fā)中采用MV*的模式時(shí),M - model,指的是模型,也就是數(shù)據(jù),V - view,指的是視圖,也就是頁(yè)面展現(xiàn)的部分。通常,...
閱讀 2886·2023-04-25 23:08
閱讀 1700·2021-11-23 09:51
閱讀 1698·2021-10-27 14:18
閱讀 3175·2019-08-29 13:25
閱讀 2896·2019-08-29 13:14
閱讀 3040·2019-08-26 18:36
閱讀 2261·2019-08-26 12:11
閱讀 875·2019-08-26 11:29