MVVM模式是什麼?你是怎麼理解MVVM原理的?理解它不僅是應付面試,對VUE、Backbone.js、angular、Ember、avalon框架的設計模式也會有更進步一步的理解,有可能下一個流行框架就是你的傑做~~本篇文章最後也會實現了一個屬於本身的簡易MVVM庫,裏面實現了一個mvvm庫應有基本功能~css
Mvvm定義MVVM是Model-View-ViewModel的簡寫。是一個軟件架構設計模式,由微軟 WPF 和 Silverlight 的架構師 Ken Cooper 和 Ted Peters 開發,是一種簡化用戶界面的事件驅動編程方式。由 John Gossman(一樣也是 WPF 和 Silverlight 的架構師)於2005年在他的博客上發表。即模型-視圖-視圖模型。html
var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
複製代碼
$('#name').text('Homer').css('color', 'red');
前端
MVVM最先由微軟提出來,它借鑑了桌面應用程序的MVC思想,在前端頁面中,把Model用純JavaScript對象表示,View負責顯示,二者作到了最大限度的分離。vue
ViewModel如何編寫?須要用JavaScript編寫一個通用的ViewModel,這樣,就能夠複用整個MVVM模型了。node
MVVM 的出現促進了 GUI 前端開發與後端業務邏輯的分離,極大地提升了前端開發效率。MVVM 的核心是 ViewModel 層,它就像是一箇中轉站(value converter),負責轉換 Model 中的數據對象來讓數據變得更容易管理和使用,該層向上與視圖層進行雙向數據綁定,向下與 Model 層經過接口請求進行數據交互,起呈上啓下做用。以下圖所示:react
# View 層angularjs
View 是視圖層,也就是用戶界面。前端主要由 HTML 和 CSS 來構建,爲了更方便地展示 ViewModel 或者 Model 層的數據,已經產生了各類各樣的先後端模板語言,好比FreeMarker、Marko、Pug、Jinja2等等,各大 MVVM 框架如 avalon,Vue,Angular 等也都有本身用來構建用戶界面的內置模板語言。面試
# Model 層數據庫
Model 是指數據模型,泛指後端進行的各類業務邏輯處理和數據操控,主要圍繞數據庫系統展開。編程
# ViewModel 層
ViewModel 是由前端開發人員組織生成和維護的視圖數據層。mvvm模式的核心,它是鏈接view和model的橋樑。在這一層,前端開發者對從後端獲取的 Model 數據進行轉換處理,作二次封裝,以生成符合 View 層使用預期的視圖數據模型。須要注意的是 ViewModel 所封裝出來的數據模型包括視圖的狀態和行爲兩部分,而 Model 層的數據模型是隻包含狀態的,好比頁面的這一塊展現什麼,那一塊展現什麼這些都屬於視圖狀態(展現),而頁面加載進來時發生什麼,點擊這一塊發生什麼,這一塊滾動時發生什麼這些都屬於視圖行爲(交互),視圖狀態和行爲都封裝在了 ViewModel 裏。這樣的封裝使得 ViewModel 能夠完整地去描述 View 層。因爲實現了雙向綁定,ViewModel 的內容會實時展示在 View 層,這是激動人心的,由於前端開發者不再必低效又麻煩地經過操縱 DOM 去更新視圖,MVVM 框架已經把最髒最累的一塊作好了,咱們開發者只須要處理和維護 ViewModel,更新數據視圖就會自動獲得相應更新,真正實現數據驅動開發。看到了吧,View 層展示的不是 Model 層的數據,而是 ViewModel 的數據,由 ViewModel 負責與 Model 層交互,這就徹底解耦了 View 層和 Model 層,這個解耦是相當重要的,它是先後端分離方案實施的重要一環。
一、固然是最主要的雙向綁定技術,單向綁定與雙向綁定。
<p>Hello, <span id="name">LEE</span>!</p><p>You are <span id="age">18</span>.</p>
用jQuery修改name和age節點的內容:
var name = '修改';
var age =100;
$('#name').text(name);
$('#age').text(age);
複製代碼
var person = {
name: 'LEEt',
age: 18
};
複製代碼
要把顯示的name從LEE改成修改,把顯示的age從18改成100,咱們並不操做DOM,而是直接修改JavaScript對象:
person.name = '修改';
person.age = 100;
複製代碼
MVVM的設計思想:關注Model的變化,讓MVVM框架去自動更新DOM的狀態,從而把發者從操做DOM的繁瑣步驟中解脫出來!
二、因爲控制器的功能大都移動到View上處理,大大的對控制器進行了瘦身。
三、能夠對View或ViewController的數據處理部分抽象出來一個函數處理model。這樣它們專職頁面佈局和頁面跳轉,它們必然更一步的簡化。
四、提升可維護性
五、可測試。界面素來是比較難於測試的,而如今測試能夠針對ViewModel來寫。
六、低耦合可重用:視圖(View)能夠獨立於Model變化和修改,一個ViewModel能夠綁定不一樣的"View"上,當View變化的時候Model不能夠不變,當Model變化的時候View也能夠不變。你能夠把一些視圖邏輯放在一個ViewModel裏面,讓不少view重用這段視圖邏輯。
Angular:Google出品,名氣大,可是學習難度有些大;適合PC,代碼結構會比較清晰;
Backbone.js:入門很是困難,由於自身API太多;
Ember:一個大而全的框架,想寫個Hello world都很困難。
Avalon:屬於輕量級的,而且對老的瀏覽器支持程度較高,最低支持到IE6,因此適合兼容老劉瀏覽器的項目;
Vue:主打輕量級,僅做爲MV*中的視圖部分使用,優勢輕量級,易學易用,缺點是大項目的時候還要配合其餘框架或者庫來使用,比較麻煩
3.發佈-訂閱模式(backbone):經過發佈消息,訂閱消息進行數據和視圖的綁定監聽。
一、實現一個Observer,對數據進行劫持,通知數據的變化(將使用的要點爲:Object.defineProperty()方法)
二、實現一個Compile,對指令進行解析,初始化視圖,而且訂閱數據的變動,綁定好更新函數
三、實現一個Watcher,將其做爲以上二者的一箇中介點,在接收數據變動的同時,讓Dep添加當前Watcher,並及時通知視圖進行update
四、實現一些VUE的其餘功能(Computed、menthods)
五、實現MVVM,整合以上幾點,做爲一個入口函數
Html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>
實現MVVM的js庫(模擬vue實現功能)</title>
<script src="./MVVM.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="person.name">
<p>
hello,{{person.name}}
</p>
<p>You are:{{person.age}}</p>
<!-- computed屬性若是數據不變化 視圖不更新 -->
<p>{{getNewName}}</p>
<button type="button" name="button" v-on:click="testToggle">
修更名字</button>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
person: {
name: 'lee',
age: 18
}
},
methods: {
testToggle(){
this.person.name = '修改後的名字:哈哈';
}
},
computed: {
getNewName(){
return this.person.name+' 是要成爲海賊王的人'
}
},
})
</script>
</body>
</html>
複製代碼
js:
// 2019-4-4
// lee
// 草履蟲的思考
// 簡單模擬vue實現MVVM
/**
* 實現一個Vue的類
* 一、實現一個Observer,對數據進行劫持,通知數據的變化(將使用的要點爲:Object.defineProperty()方法)
二、實現一個Compile,對指令進行解析,初始化視圖,而且訂閱數據的變動,綁定好更新函數ComplieUtil解析指令的公共方法
三、實現一個Watcher,將其做爲以上二者的一箇中介點,在接收數據變動的同時,讓Dep添加當前Watcher,並及時通知視圖進行update
四、實現一些VUE的其餘功能(Computed、menthods)
*/
// 觀察者模式(發佈訂閱)
class Dep {
constructor() {
this.subs = []; //存放全部watcher
}
// 訂閱 添加watcher
addSub(watcher) {
this.subs.push(watcher);
}
// 發佈
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 觀察者 vm.$watch(vm,'person.name',(newVal)=>{ })
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 默認存儲一個老值
this.oldValue = this.get();
}
get() {
Dep.target = this;
// 取值 把這個觀察者和數據關聯起來
let val = ComplieUtil.getVal(this.vm,this.expr);
Dep.target = null;
return val;
}
// 更新操做 數據變化後 會調用觀察者中的update方法
update() {
let newVal = ComplieUtil.getVal(this.vm,this.expr);
if (newVal !== this.oldValue) {
this.cb(newVal);
}
}
}
// 實現數據劫持做用
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
// 若是是對象才觀察
if (data && typeof data === 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive(obj, key, value) {
this.observer(value);
// 給每一個屬性 都加上具備發佈訂閱的功能
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true, // 可枚舉
configurable: true, // 可從新定義
get() {
// 建立watcher時 會取到對應的內容,而且把watcher放到全局上
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => { // {person:{name:'lee'}
// 數據沒有變不須要更新
if (newVal != value) {
// 須要遞歸
this.observer(newVal);
value = newVal;
dep.notify();
}
}
})
}
}
// 編譯器
class Complier {
constructor(el, vm) {
// 判斷el屬性是否是一個元素 若是不是元素 那就獲取他 (由於在vue的el中多是el:'#app'
// 或者document.getElementById('app')
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 把當前節點中的元素 獲取到 放到內存中
let fragment = this.nodeFragMent(this.el);
// 把節點中的內容進行替換
// 編譯模板 用數據編譯
this.complie(fragment);
// 把內容在塞到頁面中
this.el.appendChild(fragment);
}
isElementNode(node) { //是否是元素節點
return node.nodeType === 1;
}
// 把節點移動到內存中
nodeFragMent(node) {
let frag = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
// appendChild 具備移動性
frag.appendChild(firstChild);
}
return frag;
}
// 是否是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 編譯元素
complieElement(node) {
let attr = node.attributes;
[...attr].forEach(item => {
// item 有key = value ,type="text" v-model="person.name"
let {
name,
value: expr
} = item;
if (this.isDirective(name)) {
// v-mode v-html v-bind...
let [, directive] = name.split('-');
let [directiveName,eventName] = directive.split(':');
console.log(node, expr, this.vm, eventName);
// ComplieUtil[directive](node, expr, this.vm);
ComplieUtil[directiveName](node, expr, this.vm, eventName);
}
})
}
// 編譯文本
// 判斷當前文本節點中內容是否包括{{}}
complieText(node) {
let content = node.textContent;
var reg = /\{\{(.+?)\}\}/;
if (reg.test(content)) {
ComplieUtil['text'](node, content,this.vm); //{{}}
}
}
// 用來編譯內存中的dom節點
complie(node) {
let childNode = node.childNodes;
// childNode 是類數組 轉換爲數組
[...childNode].forEach(item => {
// 元素 查找v-開頭
if (this.isElementNode(item)) {
this.complieElement(item);
//若是是元素的話 須要把本身傳進去
// 在去遍歷子節點
this.complie(item);
// 文本 查找{{}}內容
} else {
this.complieText(item);
}
})
}
}
// 編譯工具
ComplieUtil = {
// 解析v-model指令
// node是節點 expr是表達式 vm是實例 person.name vm.$data 解析v-model
model(node, expr, vm) {
// 給輸入框賦予value屬性 node.value = xxx
let fn = this.updater['modelUpdater'];
let val = this.getVal(vm, expr);
// 給輸入框加一個觀察者 若是稍後數據更i性能了會觸發此方法,數據會更新
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
// 輸入事件
node.addEventListener('input',(e)=>{
let val = e.target.value; //獲取用戶輸入的內容
this.setVal(vm, expr, val);
});
fn(node, val);
},
html() {
},
// 返回了一個全的字符串
getContentVal(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
},
text(node, expr, vm) { //expr {{a}} {{b}} {{person.name}}
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//給表達式{{}}都加上觀察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr));
});
return this.getVal(vm, args[1]);
});
let fn = this.updater['textUpdater'];
fn(node, content);
},
on(node, expr, vm,eventName){ //v-on:click
console.log(node, expr, vm, eventName);
node.addEventListener(eventName,(e)=>{
vm[expr].call(vm,e );
});
},
updater: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater() {},
// 處理文本節點
textUpdater(node, value) {
node.textContent = value;
}
},
//根據表達式取到的對應的數據 vm.$data expr是如 'person.name'
getVal(vm, expr) {
return expr.split('.').reduce((data, cur) => {
return data[cur];
}, vm.$data);
},
setVal(vm, expr,value){
expr.split('.').reduce((data, cur,index,arr) => {
if(index == arr.length-1){ //索引是最後一項
return data[cur] = value;
}
return data[cur];
}, vm.$data);
}
}
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
// 根元素存在在編譯模板
if (this.$el) {
// 把數據 所有轉化成用Object.defineProperty來定義
new Observer(this.$data);
// 實現methods中的方法
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]; //進行了轉化操做
}
});
}
// 實現computed中的方法
for (let key in computed) { //有依賴關係
Object.defineProperty(this.$data, key, {
get() {
return computed[key].call(this); //進行了轉化操做
}
});
}
// 把數據獲取操做 都代理到vm.$data
this.proxy(this.$data);
new Complier(this.$el, this);
}
}
// 代理 去掉$data
proxy(data){
for(let key in data){
Object.defineProperty(this,key,{
get(){
return data[key]; //進行了轉化操做
}
});
}
}
}
複製代碼