MVVM是什么
MVVM是Model-View-ViewModel的簡寫捧存。它模式是MVC—>MVP—>MVVM的進化版愁铺。
Model負(fù)責(zé)用JavaScript對象表示,View負(fù)責(zé)UI界面顯示,兩者做到了最大限度的分離远搪。
而把Model和View關(guān)聯(lián)起來的就是ViewModel峡捡。ViewModel負(fù)責(zé)把Model的數(shù)據(jù)同步到View顯示出來击碗,還負(fù)責(zé)把View的界面修改同步回Model更新數(shù)據(jù)。
主流MVVM框架和實現(xiàn)做法
- 臟值檢查(angular.js)
- 發(fā)布者-訂閱者模式+數(shù)據(jù)劫持(vue.js)
臟值檢查: angular.js 是通過臟值檢測的方式來比對數(shù)據(jù)是否有變更而決定是否更新視圖棋返。
原理是延都,拷貝一份copy_viewModel
在內(nèi)存中,用戶操作導(dǎo)致viewModel
發(fā)生改變的行為時,框架都會把copy_viewModel
和最新的viewModel
進行深度比較睛竣,一旦發(fā)現(xiàn)有屬性發(fā)生變化晰房,則重新渲染與之綁定的DOM節(jié)點。
最簡單的方式就是通過setInterval()
定時輪詢檢測數(shù)據(jù)變動射沟,angular觸發(fā)時進入臟值檢測殊者。但只限 指定的事件 (如:用戶點擊,輸入操作验夯,ajax請求猖吴,setInterval,setTimeout等...)挥转,否則需手動調(diào)用apply
函數(shù)去強制執(zhí)行一次臟檢查海蔽。
數(shù)據(jù)劫持: vue.js 則是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式共屈,通過Object.defineProperty()
來劫持各個屬性的setter
,getter
在數(shù)據(jù)變動時發(fā)布消息給訂閱者党窜,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)拗引,而產(chǎn)生更新數(shù)據(jù)和視圖。
vue數(shù)據(jù)雙向綁定原理
原理圖告訴我們幌衣,data屬性定義了getter矾削、setter對屬性進行劫持,當(dāng)屬性值改變是就會notify通知watch對象豁护,而watch對象則會重新觸發(fā)組件呈現(xiàn)功能哼凯,繼而更新view上的DOM節(jié)點樹。
反之楚里,view上輸入數(shù)據(jù)時断部,也會觸發(fā)data變更,也會觸發(fā)訂閱者watch更新,這樣子model數(shù)據(jù)就可以實時更新view上的數(shù)據(jù)變化腻豌。這樣一個過程就是vue的數(shù)據(jù)雙向綁定了家坎。
vue是通過數(shù)據(jù)劫持的方式來做數(shù)據(jù)綁定的,其中最核心的方法便是通過Object.defineProperty()
來實現(xiàn)對屬性的劫持吝梅,達(dá)到監(jiān)聽數(shù)據(jù)變動的目的虱疏。
Object.defineProperty
Object.defineProperty
是ES5一個方法,可以直接在一個對象上定義一個新屬性苏携,或者修改一個已經(jīng)存在的屬性做瞪,并返回這個對象,對象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符和 存取描述符右冻。
數(shù)據(jù)描述符是一個擁有可寫或不可寫值的屬性装蓬。
存取描述符是由一對getter-setter函數(shù)功能來描述的屬性。
描述符必須是兩種形式之一牍帚;不能同時是兩者。即:有值和可寫乳蛾,或者可get和set
屬性描述符包括:
- Configurable(可配置性相當(dāng)于屬性的總開關(guān)暗赶,只有為true時才能設(shè)置,而且不可逆)肃叶、
- Enumerable(是否可枚舉蹂随,為false時for..in以及Object.keys()將不能枚舉出該屬性)、
- Writable(是否可寫因惭,為false時將不能夠修改屬性的值)岳锁、
- Value(屬性的值,默認(rèn)為undefined)、
- Get(一個給屬性提供getter的方法)蹦魔、
- Set(一個給屬性提供setter的方法)激率、
var Book = {}
Object.defineProperty(Book, 'name', {
get: function () {
return '《' + name + '》'
},
set: function (value) {
name = value;
console.log('你取了一個書名叫做' + value);
}
})
console.log(Book.name); // 《》
Book.name = 'vue權(quán)威指南'; // 你取了一個書名叫做vue權(quán)威指南
console.log(Book.name); // 《vue權(quán)威指南》
實現(xiàn)過程
我們已經(jīng)知道怎么實現(xiàn)數(shù)據(jù)的雙向綁定咳燕,首先要對數(shù)據(jù)進行劫持監(jiān)聽,所以我們需要設(shè)置一個監(jiān)聽器Observer
柱搜,用來監(jiān)聽所有屬性迟郎。如果屬性發(fā)上變化了,就需要告訴訂閱者Watcher
看是否需要更新聪蘸。因為訂閱者是有很多個,所以我們需要有一個消息訂閱器Dep
來專門收集這些訂閱者表制,然后在監(jiān)聽器Observer
和訂閱者Watcher
之間進行統(tǒng)一管理的健爬。接著,我們還需要有一個指令解析器Compile
么介,對每個節(jié)點元素進行掃描和解析娜遵,將相關(guān)指令對應(yīng)初始化成一個訂閱者Watcher
,并替換模板數(shù)據(jù)或者綁定相應(yīng)的函數(shù)壤短,此時當(dāng)訂閱者Watcher
接收到相應(yīng)屬性的變化设拟,就會執(zhí)行對應(yīng)的更新函數(shù),從而更新視圖久脯。
因此接下去我們執(zhí)行以下4個步驟纳胧,實現(xiàn)數(shù)據(jù)的雙向綁定:
- 實現(xiàn)一個監(jiān)聽器
Observer
,用來劫持并監(jiān)聽所有屬性帘撰,如果有變動的跑慕,就拿到最新值并通知訂閱者。 - 實現(xiàn)一個訂閱者
Watcher
摧找,連接Observer
和Compile
核行。可以訂閱并收到每個屬性的變化通知并執(zhí)行指令綁定的相應(yīng)函數(shù)蹬耘,從而更新視圖芝雪。 - 實現(xiàn)一個解析器
Compile
,可以掃描和解析每個節(jié)點的相關(guān)指令综苔,并根據(jù)初始化模板替換數(shù)據(jù)惩系,以及綁定相應(yīng)的更新函數(shù)。 - mvvm入口函數(shù)休里,整合以上三者蛆挫。
<!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>Document</title>
</head>
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<button v-on:click="clickMe">click me!</button>
<p>aaaa{{xxx}}zzzz</p>
</div>
<!-- <h1 id="name">{{name}}</h1> -->
</body>
</html>
<script>
/****
* Observer
*
* */
//初始化數(shù)據(jù)監(jiān)聽器
function observe(data) {
//驗證傳入的參數(shù)格式
if (!data || typeof data !== 'object') {
return;
}
// var dep = new Dep(); //創(chuàng)建訂閱器Dep
// console.log(dep)
//遍歷所有屬性
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key])//所有數(shù)據(jù),單個鍵妙黍,單個值
console.log(data)
console.log(key)
console.log(data[key])
})
console.log(Object.keys(data))
}
//監(jiān)聽所有屬性
function defineReactive(data, key, val) {
observe(val); // 遞歸遍歷所有子屬性
var dep = new Dep();//創(chuàng)建訂閱器Dep
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉
configurable: false, // 可配置
get: function () {//返回它本身
console.log(Dep)
console.log(Dep.target)
if (Dep.target) { // 判斷是否需要添加訂閱者
dep.addSub(Dep.target); // 在這里添加一個訂閱者
}
return val;
},
set: function (newVal) {//返回更新值
val = newVal;
console.log('屬性' + key + '已經(jīng)被監(jiān)聽了悴侵,現(xiàn)在值為:“' + newVal.toString() + '”');
console.log(dep)
dep.notify(); // 如果數(shù)據(jù)變化,通知所有訂閱者
}
})
}
console.log(Dep)
Dep.target = null;
//訂閱器容器
function Dep() {
this.subs = [];
}
//訂閱器原型方法
Dep.prototype = {
//添加進訂閱器容器
addSub: function (sub) {
this.subs.push(sub);
},
//通知所有訂閱者
notify: function () {
this.subs.forEach(function (sub) {
console.log(sub)
sub.update();
});
}
};
/****
* Watcher
*
* */
//初始化Watcher訂閱者
function Watcher(vm, exp, cb) {//實例本身拭嫁, 模板鍵值可免,模板值重新賦值方法
console.log(vm)
console.log(exp)
console.log(cb)
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 將自己添加到訂閱器的操作
}
Watcher.prototype = {
update: function () {
this.run();
},
run: function () {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);//實例的賦值方法call到訂閱者
}
},
//讓實例設(shè)置的屬性強制映射到結(jié)構(gòu)樹上
get: function () {
console.log(Dep.target)
console.log(Dep)
Dep.target = this; // 緩存自己
var value = this.vm.data[this.exp] // 強制執(zhí)行監(jiān)聽器里的get函數(shù)
Dep.target = null; // 釋放自己
return value;
}
};
/****
* Compile
*
* */
function Compile(el, vm) {//dom節(jié)點抓于,實例對象
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
Compile.prototype = {
// 初始化
init: function () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);//掛載點載入模板碎片
} else {
console.log('Dom元素不存在');
}
},
//創(chuàng)建一個fragment片段,用于解析的dom節(jié)點
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();//創(chuàng)建fragment-DOM模板碎片
var child = el.firstChild;
while (child) {
// 將Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
//獲取起始節(jié)點下所有節(jié)點并且遞歸遍歷所有符合{{}}的指令
compileElement: function (el) {
var childNodes = el.childNodes;
var self = this;
//數(shù)組分割的方法作用于起始節(jié)點下所有節(jié)點并遍歷每個節(jié)點執(zhí)行對應(yīng)方法
[].slice.call(childNodes).forEach(function (node) {
var reg = /\{\{(.*)\}\}/;//{{}}指令的正則
var text = node.textContent;//節(jié)點的內(nèi)容
//v-model指令和事件指令的解析編譯
if (self.isElementNode(node)) {
self.compile(node);
} else if (self.isTextNode(node) && reg.test(text)) { // 判斷是否是符合這種形式{{}}的指令
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node); // 繼續(xù)遞歸遍歷子節(jié)點
}
});
},
// 執(zhí)行v-model指令和事件指令的解析編譯
compile: function (node) {
var nodeAttrs = node.attributes;//獲取該元素上的長度
var self = this;
//遍歷該元素上的所有屬性
Array.prototype.forEach.call(nodeAttrs, function (attr) {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;//指定model的value值
var dir = attrName.substring(2);
if (self.isEventDirective(dir)) { // 事件指令
self.compileEvent(node, self.vm, exp, dir);
} else { // v-model 指令
self.compileModel(node, self.vm, exp, dir);
}
node.removeAttribute(attrName);
}
});
},
//執(zhí)行{{}}的節(jié)點的值
compileText: function (node, exp) {//每個符合{{}}的節(jié)點浇借,{{}}里面的內(nèi)容值
var self = this;
var initText = this.vm[exp];
this.updateText(node, initText); // 將初始化的數(shù)據(jù)初始化到視圖中
new Watcher(this.vm, exp, function (value) { // 生成訂閱器并綁定更新函數(shù)
self.updateText(node, value);
});
},
//執(zhí)行事件的節(jié)點的值
compileEvent: function (node, vm, exp, dir) {
var eventType = dir.split(':')[1];
var cb = vm.methods && vm.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false);
}
},
//執(zhí)行模塊的節(jié)點的值
compileModel: function (node, vm, exp, dir) {
var self = this;
var val = this.vm[exp];
this.modelUpdater(node, val);
new Watcher(this.vm, exp, function (value) {
self.modelUpdater(node, value);
});
node.addEventListener('input', function (e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[exp] = newValue;
val = newValue;
});
},
//更新文本
updateText: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
//更新模塊
modelUpdater: function (node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
},
// 判斷是是不是v-指令
isDirective: function (attr) {
return attr.indexOf('v-') == 0;
},
// 判斷是是不是on:事件指令
isEventDirective: function (dir) {
return dir.indexOf('on:') === 0;
},
// 判斷元素節(jié)點 元素類型等于1
isElementNode: function (node) {
return node.nodeType == 1;
},
// 判斷文本節(jié)點
isTextNode: function (node) {
return node.nodeType == 3;
}
}
/****
* Observer和Watcher
*
* */
function SelfVue(options) {// 整個實例對象 //data, el, exp 所有數(shù)據(jù)捉撮,選中元素,模板鍵值
var self = this;
this.vm = this;
this.data = options.data;
this.methods = options.methods;
//賦值時妇垢,屬性的綁定做一層封裝
Object.keys(this.data).forEach(function (key) {
self.proxyKeys(key); // 綁定代理屬性
});
//劫持并監(jiān)聽所有屬性
observe(this.data);
//解析器解析掛載點的指令
new Compile(options.el, this.vm)//掛載點巾遭,實例對象
options.mounted.call(this); // 所有事情處理好后執(zhí)行mounted函數(shù)
// el.innerHTML = this.data[exp]; // 初始化模板數(shù)據(jù)的值 // 內(nèi)容為設(shè)置的鍵值
// console.log(el.innerHTML)
// console.log(this)
// new Watcher(this, exp, function (value) {//selfvue本身,模板鍵值闯估,模板值為監(jiān)聽的新值
// el.innerHTML = value;
// });
return this;
}
//讓selfVue的屬性代理為訪問selfVue.data的屬性
SelfVue.prototype = {
proxyKeys: function (key) {
var self = this;
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function proxyGetter() {
return self.data[key];
},
set: function proxySetter(newVal) {
self.data[key] = newVal;
}
});
}
}
/****
* 實例
*
* */
var selfVue = new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: 'null',
xxx: 'cjh'
},
methods: {
clickMe: function () {
this.title = 'hello world';
}
},
mounted: function () {
window.setTimeout(() => {
this.title = '你好';
}, 2000);
}
});
// window.setTimeout(function () {
// selfVue.title = '你好';
// }, 2000);
// window.setTimeout(function () {
// selfVue.name = 'canfoo';
// }, 2500);
// //實例
// var ele = document.querySelector('#name');
// var selfVue = new SelfVue({
// name: 'hello world'
// }, ele, 'name');
// console.log(ele)
// console.log('name')
// window.setTimeout(function () {
// console.log('name值改變了');
// selfVue.name = 'canfoo';
// }, 2000);
// //實例
// var library = {
// book1: {
// name: ''
// },
// book2: ''
// };
// observe(library);
// library.book1.name = 'vue權(quán)威指南'; // 屬性name已經(jīng)被監(jiān)聽了灼舍,現(xiàn)在值為:“vue權(quán)威指南”
// library.book2 = '沒有此書籍'; // 屬性book2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“沒有此書籍”
// console.log(library)
</script>
參考鏈接:
深入響應(yīng)式原理
剖析Vue原理&實現(xiàn)雙向綁定MVVM
《響應(yīng)式系統(tǒng)的基本原理》.js
JavaScript實現(xiàn)MVVM之我就是想監(jiān)測一個普通對象的變化