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