摘要:為了將對(duì)象和視圖綁定在一起,我們需要設(shè)置一個(gè)回調(diào)函數(shù),當(dāng)對(duì)象的屬性發(fā)生改變時(shí)發(fā)送一個(gè)更新視圖的通知。下面是值發(fā)生變化的時(shí)候調(diào)用的回調(diào)函數(shù),當(dāng)然現(xiàn)在我們可以使用更簡單的進(jìn)行數(shù)據(jù)的監(jiān)聽,這在我們后面的將會(huì)講到。
MVC
MVC是一種設(shè)計(jì)模式,它將應(yīng)用劃分為3個(gè)部分:數(shù)據(jù)(模型)、展示層(視圖)和用戶交互層。結(jié)合一下下圖,更能理解三者之間的關(guān)系。
換句話說,一個(gè)事件的發(fā)生是這樣的過程
用戶和應(yīng)用交互
控制器的事件處理器被觸發(fā)
控制器從模型中請求數(shù)據(jù),并將其交給視圖
視圖將數(shù)據(jù)呈現(xiàn)給用戶
模型:用來存放應(yīng)用的所有數(shù)據(jù)對(duì)象。模型不必知曉視圖和控制器的細(xì)節(jié),模型只需包含數(shù)據(jù)及直接和這些數(shù)據(jù)相關(guān)的邏輯。任何事件處理代碼、視圖模版,以及那些和模型無關(guān)的邏輯都應(yīng)當(dāng)隔離在模型之外。
視圖:視圖層是呈現(xiàn)給用戶的,用戶與之產(chǎn)生交互。在javaScript應(yīng)用中,視圖大都是由html、css和JavaScript模版組成的。除了模版中簡單的條件語句之外,視圖不應(yīng)當(dāng)包含任何其他邏輯。事實(shí)上和模型類似,視圖也應(yīng)該從應(yīng)用的其他部分中解耦出來
控制器:控制器是模型和視圖的紐帶。控制器從視圖獲得事件和輸入,對(duì)它們進(jìn)行處理,并相應(yīng)地更新視圖。當(dāng)頁面加載時(shí),控制器會(huì)給視圖添加事件監(jiān)聽,比如監(jiān)聽表單提交和按鈕單擊。然后當(dāng)用戶和應(yīng)用產(chǎn)生交互時(shí),控制器中的事件觸發(fā)器就開始工作。
例如JavaScript框架早期框架backbone就是采用的MVC模式。
上面的例子似乎太過空洞,下面講一個(gè)生活中的例子進(jìn)行講解:
1、用戶提交一個(gè)新的聊天信息
2、控制器的事件處理器被觸發(fā)
3、控制器創(chuàng)建了一個(gè)新的聊天模型
4、然后控制器更新視圖
5、用戶在聊天窗口看到新的聊天信息
講了一個(gè)生活的例子,我們用代碼的方式更加深入了解MVC。
MVC中M表示model,與數(shù)據(jù)操作和行為相關(guān)的邏輯都應(yīng)當(dāng)放入模型中。例如我們創(chuàng)建一個(gè)Model對(duì)象,所有數(shù)據(jù)的操作都應(yīng)該都放在這個(gè)命名空間中。下面是一些簡化的代碼,首先創(chuàng)建新模型和實(shí)例
var Model = { create: function() { this.records = {} var object = Object.create(this) object.prototype = Object.create(this.prototype) return object } }
create用于創(chuàng)建一個(gè)以Model為原型的對(duì)象,然后就是一些包括數(shù)據(jù)操作的一些函數(shù)包括查找,存儲(chǔ)
var Model = { /*---代碼片段--*/ find: function () { return this.records[this.id] }, save: function () { this.records[this.id] = this } }
下面我們就可以使用這個(gè)Model了:
user = Model.create() user.id = 1 user.save() asset = Model.create() asset.id = 2 asset.save() Model.find(1) => {id:1}
可以看到我們就已經(jīng)查找到了這個(gè)對(duì)象。模型也就是數(shù)據(jù)的部分我們也就完成了。
Control下面來講講mvc中的控制器。當(dāng)加載頁面的時(shí)候,控制器將事件處理程序綁定在視圖中,并適時(shí)地處理回調(diào),以及和模型必要的對(duì)接。下面是控制器的簡單例子:
var ToggleView = { init: function (view) { this.view = $(view) this.view.mouseover(this.toggleClass, true) this.view.mouseout(this.toggleClass, false) }, this.toggleClass: function () { this.view.toggleClass("over", e.data) } }
這樣我們就實(shí)現(xiàn)了對(duì)一個(gè)視圖的簡單控制,鼠標(biāo)移入元素添加over class,移除就移除over class。然后在添加一些簡單的樣式例如
ex: .over {color: red} p{color: black} 這樣控制器就和視圖建立起了連接。在MVC中有一個(gè)特性就是一個(gè)控制器控制一個(gè)視圖,隨著項(xiàng)目體積的增大,就需要一個(gè)狀態(tài)機(jī)用于管理這些控制器。先來創(chuàng)建一個(gè)狀態(tài)機(jī) var StateMachine = function() {} SateMachine.add = function (controller) { this.bind("change", function (e, current) { if (controller == current) { controller.activate() } else { controller.deactivate() } }) controller.active = function () { this.trigger("change", controller) } } // 創(chuàng)建兩個(gè)控制器 var con1 = { activate: funtion() { $("#con1").addClass("active") }, deactivate: function () { $("#con1").removeClass("active") } } var con2 = { activate: funtion() { $("#con2").addClass("active") }, deactivate: function () { $("#con2").removeClass("active") } } // 創(chuàng)建狀態(tài)機(jī),添加狀態(tài) var sm = new StateMachine sm.add(con1) sm.add(con2) // 激活第一個(gè)狀態(tài) con1.active()
這樣就實(shí)現(xiàn)了簡單的控制器管理,最后我們在添加一些css樣式。
#con1, #con2 { display: none } #con2.active, #con2.active { display: block }
當(dāng)con1激活的時(shí)候樣式就發(fā)生了變化,也就是視圖發(fā)生了變化。
控制器也就講到了這里,下面來看看MVC中的View部分,也就是視圖
視圖是應(yīng)用的接口,它為用戶提供視覺呈現(xiàn)并與用戶產(chǎn)生交互。在javaScript種,視圖是無邏輯的HTML片段,又應(yīng)用的控制器來管理,視圖處理事件回調(diào)以及內(nèi)嵌數(shù)據(jù)。簡單來說就是在javaScript中寫HTML代碼,然后將HTML片段插入到HTML頁面中,這里講兩種方法:
動(dòng)態(tài)渲染視圖使用document.createElement創(chuàng)建DOM元素,設(shè)置他們的內(nèi)容然后追加到頁面中,例如
var views = documents.getElementById("views")
views.innerHTML = "" // 元素清空
var wapper = document.createElement("p")
wrapper.innerText = "add to views"
views.appendChild(wrapper)
這樣就完成了用createElement創(chuàng)建元素,然后添加到HTML頁面中。
如果以前有過后端開發(fā)經(jīng)驗(yàn),那么對(duì)模版應(yīng)該比較熟悉。例如在nodejs中常用的就是ejs,下面是ejs的一個(gè)小例子,可以看到的是ejs將javascript直接渲染為HTML
str = "
title: "ejs"
});
那么這個(gè)渲染后的結(jié)果就是
var addChange = function (ob) { ob.change = function (callback) { if (callback) { if (!this._change) this._change = {} this._change.push(callback) } else { if (!this._change) return for (var i = this._change.length - 1; i >= 0; i--) { this._change[i].apply(this) } } } }
我們來看看一個(gè)實(shí)際的例子
var addChange = function (ob) { ob.change = function (callback) { if (callback) { if (!this._change) this._change = {} this._change.push(callback) } else { if (!this._change) return for (var i = this._change.length - 1; i >= 0; i--) { this._change[i].apply(this) } } } } var object = {} object.name = "Foo" addChange(object) object.change(function () { console.log("Changed!", this) // 更新視圖的代碼 }) obejct.change() object.name = "Bar" object.change()
這樣就實(shí)現(xiàn)了執(zhí)行和觸發(fā)change事件了。
我相信大家對(duì)MVC有了比較深刻的理解,下面來學(xué)習(xí)MVVM模式。
如今主流的web框架基本都采用的是MVVM模式,為什么放棄了MVC模式,轉(zhuǎn)而投向了MVVM模式呢。在之前的MVC中我們提到一個(gè)控制器對(duì)應(yīng)一個(gè)視圖,控制器用狀態(tài)機(jī)進(jìn)行管理,這里就存在一個(gè)問題,如果項(xiàng)目足夠大的時(shí)候,狀態(tài)機(jī)的代碼量就變得非常臃腫,難以維護(hù)。還有一個(gè)就是性能問題,在MVC中我們大量的操作了DOM,而大量操作DOM會(huì)讓頁面渲染性能降低,加載速度變慢,影響用戶體驗(yàn)。最后就是當(dāng)Model頻繁變化的時(shí)候,開發(fā)者就主動(dòng)更新View,那么數(shù)據(jù)的維護(hù)就變得困難。世界是懶人創(chuàng)造的,為了減小工作量,節(jié)約時(shí)間,一個(gè)更適合前端開發(fā)的架構(gòu)模式就顯得非常重要。這時(shí)候MVVM模式在前端中的應(yīng)用就應(yīng)運(yùn)而生。
MVVM讓用戶界面和邏輯分離更加清晰。下面是MVVM的示意圖,可以看到它由Model、ViewModel、View這三個(gè)部分組成。
下面分別來講講他們的作用
View是作為視圖模板,用于定義結(jié)構(gòu)、布局。它自己不處理數(shù)據(jù),只是將ViewModel中的數(shù)據(jù)展現(xiàn)出來。此外為了和ViewModel產(chǎn)生關(guān)聯(lián),那么還需要做的就是數(shù)據(jù)綁定的聲明,指令的聲明,事件綁定的聲明。這在當(dāng)今流行的MVVM開發(fā)框架中體現(xiàn)的淋淋盡致。在示例圖中,我們可以看到ViewModel和View之間是雙向綁定,意思就是說ViewModel的變化能夠反映到View中,View的變化也能夠改變ViewModel的數(shù)據(jù)值。那如何實(shí)現(xiàn)雙向綁定呢,例如有這個(gè)input元素:
隨著用戶在Input中輸入值的變化,在ViewModel中的message也會(huì)發(fā)生改變,這樣就實(shí)現(xiàn)了View到ViewModel的單向數(shù)據(jù)綁定。下面是一些思路:
掃描看哪些節(jié)點(diǎn)有yg-xxx屬性
自動(dòng)給這些節(jié)點(diǎn)加上onchange這種事件
更新ViewModel中的數(shù)據(jù),例如ViewModel.message = xx.innerText
那么ViewModel到View的綁定可以是下面例子:
渲染后p中顯示的值就是ViewModel中的message變量值。下面是一些思路:
首先注冊ViewModel
掃描整個(gè)DOM Tree 看哪些節(jié)點(diǎn)有yg-xxx這中屬性
記錄這些被單向綁定的DOM節(jié)點(diǎn)和ViewModel之間的隱射關(guān)系
使用innerText,innerHTML = ViewModel.message進(jìn)行賦值
ViewModelViewModel起著連接View和Model的作用,同時(shí)用于處理View中的邏輯。在MVC框架中,視圖模型通過調(diào)用模型中的方法與模型進(jìn)行交互,然而在MVVM中View和Model并沒有直接的關(guān)系,在MVVM中,ViewModel從Model獲取數(shù)據(jù),然后應(yīng)用到View中。相對(duì)MVC的眾多的控制器,很明顯這種模式更能夠輕松管理數(shù)據(jù),不至于這么混亂。還有的就是處理View中的事件,例如用戶在點(diǎn)擊某個(gè)按鈕的時(shí)候,這個(gè)行動(dòng)就會(huì)觸發(fā)ViewModel的行為,進(jìn)行相應(yīng)的操作。行為就可能包括更改Model,重新渲染View。
ModelModel 層,對(duì)應(yīng)數(shù)據(jù)層的域模型,它主要做域模型的同步。通過 Ajax/fetch 等 API 完成客戶端和服務(wù)端業(yè)務(wù) Model 的同步。在層間關(guān)系里,它主要用于抽象出 ViewModel 中視圖的 Model。
MVVM簡單實(shí)現(xiàn)實(shí)現(xiàn)效果:
{{message}}
這里為了簡單,借鑒了Vue的一些方法
ObserverMVVM為我們省去了手動(dòng)更新視圖的步驟,一旦值發(fā)生變化,視圖就重新渲染,那么就需要對(duì)數(shù)據(jù)的改變就行檢測。例如有這么一個(gè)例子:
hero = { name: "A" }
這時(shí)候但我們訪問hero.name 的時(shí)候,就會(huì)打印出一些信息:
hero.name // I"m A
當(dāng)我們對(duì)hero.name 進(jìn)行更改的時(shí)候,也會(huì)打印出一些信息:
hero.name = "B" // the name has change
這樣我們是不是就實(shí)現(xiàn)了數(shù)據(jù)的觀測了呢。
在Angular中實(shí)現(xiàn)數(shù)據(jù)的觀測使用的是臟檢查,就是在用戶進(jìn)行可能改變ViewModel的操作的時(shí)候,對(duì)比以前老的ViewModel然后做出改變。
而在Vue中,采取的是數(shù)據(jù)劫持,就是當(dāng)數(shù)據(jù)獲取或者設(shè)置的時(shí)候,會(huì)觸發(fā)Object.defineProperty()。
這里我們采取的是Vue數(shù)據(jù)觀測的方法,簡單一些。下面是具體的代碼
function observer (obj) { let keys = Object.keys(obj) if (typeof obj === "object" && !Array.isArray(obj)) { keys.forEach(key => { defineReactive(obj, key, obj[key]) }) } } function defineReactive (obj, key, val) { observer(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { console.log("I am A") return val }, set: function (newval) { console.log("the name has change") observer(val) val = newval } }) }
把hero帶入observe方法中,結(jié)果正如先前預(yù)料的一樣的結(jié)果。這樣數(shù)據(jù)的檢測也就實(shí)現(xiàn)了,然后在通知訂閱者。如何通知訂閱者呢,我們需要實(shí)現(xiàn)一個(gè)消息訂閱器,維護(hù)一個(gè)數(shù)組用來收集訂閱者,數(shù)據(jù)變動(dòng)觸發(fā)notify(),然后訂閱者觸發(fā)update()方法,改善后的代碼長這樣:
function defineReactive (obj) { dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { console.log("I am A") Dep.target || dep.depend() return val }, set: function (newval) { console.log("the name has change") dep.notify() observer(val) val = newval } }) } var Dep = function Dep () { this.subs = [] } Dep.prototype.notify = function(){ var subs = this.subs.slice() for (var i = 0, l = subs.length; i < l; i++) { subs[i].update() } } Dep.prototype.addSub = function(sub){ this.subs.push(sub) } Dep.prototype.depend = function(){ if (Dep.target) { Dep.target.addDep(this) } }
這跟Vue源碼差不多,就完成了往訂閱器里邊添加訂閱者,和通知訂閱者。這里以前我看Vue源碼的時(shí)候,困擾了很久的問題,就是在get方法中Dep是哪兒來的。這里說一下他是一個(gè)全局變量,添加target變量是用于向訂閱器中添加訂閱者。這里的訂閱者是Wacther,Watcher就可以連接視圖更新視圖。下面是Watcher的一部分代碼
Watcher.prototype.get = function(key){ Dep.target = this this.value = obj[key] // 觸發(fā)get從而向訂閱器中添加訂閱者 Dep.target = null // 重置 };Compile
在講MVVM概念的時(shí)候,在View -> ViewModel的過程中有一個(gè)步驟就是在DOM tree中尋找哪個(gè)具有yg-xx的元素。這一節(jié)就是講解析模板,讓View和ViewModel連接起來。遍歷DOM tree是非常消耗性能的,所以會(huì)先把節(jié)點(diǎn)el轉(zhuǎn)換為文檔碎片fragment進(jìn)行解析編譯操作。操作完成后,在將fragment添加到原來的真實(shí)DOM節(jié)點(diǎn)中。下面是它的代碼
function Compile (el) { this.el = document.querySelector(el) this.fragment = this.init() this.compileElement() } Compile.prototype.init = function(){ var fragment = document.createDocumentFragment(), chid while (child.el.firstChild) { fragment.appendChild(child) } return fragment }; Compile.prototype.compileElement = function(){ fragment = this.fragment me = this var childNodes = el.childNodes [].slice.call(childNodes).forEach(function (node) { var text = node.textContent var reg = /{{(.*)}}/ // 獲取{{}}中的值 if (reg.test(text)) { me.compileText(node, RegExp.$1) } if (node.childNodes && node.childNodes.length) { me.compileElement(node) } }) } Compile.prototype.compileText = function (node, vm, exp) { updateFn && updateFn(node, vm[exp]) new Watcher(vm, exp, function (value, oldValue) { // 一旦屬性值有變化,就會(huì)收到通知執(zhí)行此更新函數(shù),更新視圖 updateFn() && updateFn(node, val) }) } // 更新視圖 function updateFn (node, value) { node.textContent = value }
這樣編譯fragment就成功了,并且ViewModel中值的改變就能夠引起View層的改變。接下來是Watcher的實(shí)現(xiàn),get方法已經(jīng)講了,我們來看看其他的方法。
WatcherWatcher是連接Observer和Compile之間的橋梁。可以看到在Observer中,往訂閱器中添加了自己。dep.notice()發(fā)生的時(shí)候,調(diào)用了sub.update(),所以需要一個(gè)update()方法,值發(fā)生變化后,就能夠觸發(fā)Compile中的回調(diào)更新視圖。下面是Watcher的具體實(shí)現(xiàn)
var Watcher = function Watcher (vm, exp, cb) { this.vm = vm this.cb = cb this.exp = exp // 觸發(fā)getter,向訂閱器中添加自己 this.value = this.get() } Watcher.prototype = { update: function () { this.run() }, addDep: function (dep) { dep.addSub(this) }, run: function () { var value = this.get() var oldVal = this.value if (value !== oldValue) { this.value = value this.cb.call(this.vm, value, oldValue) // 執(zhí)行Compile中的回調(diào) } }, get: function () { Dep.target = this value = this.vm[exp] // 觸發(fā)getter Dep.target = null return value } }
在上面的代碼中Watcher就起到了連接Observer和Compile的作用,值發(fā)生改變的時(shí)候通知Watcher,然后Watcher調(diào)用update方法,因?yàn)樵贑ompile中定義的Watcher,所以值發(fā)生改變的時(shí)候,就會(huì)調(diào)用Watcher()中的回調(diào),從而更新視圖。最重要的部分也就完成了。在加一個(gè)MVVM的構(gòu)造器就ok了。推薦一篇文章自己實(shí)現(xiàn)MVVM,這里邊講的更加詳細(xì)。
總結(jié)ok,本篇文章就結(jié)束了,通過對(duì)比希望讀者能夠?qū)η岸水?dāng)前框架能夠更清晰的認(rèn)識(shí)。謝謝大家
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/92869.html
摘要:它由微軟架構(gòu)師和開發(fā),通過利用微軟圖形系統(tǒng)和的互聯(lián)網(wǎng)應(yīng)用派生品的特性來簡化用戶界面的事件驅(qū)動(dòng)程序設(shè)計(jì)。微軟的和架構(gòu)師之一于年在他的博客上發(fā)表了。更改時(shí)會(huì)得到提醒這個(gè)情況是一個(gè)單向流。 前言 記得四個(gè)月前有一次面試,面試官問我 MVVM 是什么,MVVM 的本質(zhì)是什么。我大腦一片混亂,那時(shí)我對(duì) MVVM 的認(rèn)知就只是雙向綁定和Vue,以這個(gè)關(guān)鍵字簡單回答了幾句,我反問 MVVM 的本質(zhì)是...
摘要:所以我查了很多的材料,希望能從自己的角度上用通俗的語言闡述前端框架的演變。現(xiàn)在,前端頁面會(huì)有很多復(fù)雜的交互邏輯和用戶體驗(yàn),如果還使用之前老的框架,對(duì)層的操作就會(huì)難以維護(hù),這就是前端框架要不斷演變的主要原因。 說實(shí)在的,我不覺得MVC,MVVM這些框架有什么難的,直到我想寫一篇文章去系統(tǒng)的闡述它們。我遇到了以下幾個(gè)問題,1.不同的文章說的南轅北轍 2.沒有一個(gè)清晰的大綱和框架分類。所以我...
摘要:所以我查了很多的材料,希望能從自己的角度上用通俗的語言闡述前端框架的演變。現(xiàn)在,前端頁面會(huì)有很多復(fù)雜的交互邏輯和用戶體驗(yàn),如果還使用之前老的框架,對(duì)層的操作就會(huì)難以維護(hù),這就是前端框架要不斷演變的主要原因。 說實(shí)在的,我不覺得MVC,MVVM這些框架有什么難的,直到我想寫一篇文章去系統(tǒng)的闡述它們。我遇到了以下幾個(gè)問題,1.不同的文章說的南轅北轍 2.沒有一個(gè)清晰的大綱和框架分類。所以我...
摘要:架構(gòu)模式的文章很多,好理解的沒有幾個(gè)。沒有明確的目的理解架構(gòu)模式的真正意義是什么虛擬和組件化在中的位置題目開的太大,一定有很多疏忽錯(cuò)誤的地方,也懇請大家指出。因此,模式出現(xiàn)了。然后通過雙向數(shù)據(jù)綁定使中的狀態(tài)數(shù)據(jù)與中的顯示狀態(tài)保持一致。 架構(gòu)模式的文章很多,好理解的沒有幾個(gè)。大部分文章出現(xiàn)的主要問題有: 沒有設(shè)定好作用域:前端MVC是改造過的MVC,和后臺(tái)MVC有明顯的區(qū)別,不能一概...
摘要:架構(gòu)模式的文章很多,好理解的沒有幾個(gè)。沒有明確的目的理解架構(gòu)模式的真正意義是什么虛擬和組件化在中的位置題目開的太大,一定有很多疏忽錯(cuò)誤的地方,也懇請大家指出。因此,模式出現(xiàn)了。然后通過雙向數(shù)據(jù)綁定使中的狀態(tài)數(shù)據(jù)與中的顯示狀態(tài)保持一致。 架構(gòu)模式的文章很多,好理解的沒有幾個(gè)。大部分文章出現(xiàn)的主要問題有: 沒有設(shè)定好作用域:前端MVC是改造過的MVC,和后臺(tái)MVC有明顯的區(qū)別,不能一概...
閱讀 1860·2021-09-29 09:35
閱讀 2721·2021-09-22 15:25
閱讀 1979·2021-08-23 09:43
閱讀 2056·2019-08-30 15:54
閱讀 3357·2019-08-30 15:53
閱讀 2394·2019-08-30 13:50
閱讀 2406·2019-08-30 11:24
閱讀 2277·2019-08-29 15:37