這篇文章給大家介紹怎么在vue中實(shí)現(xiàn)一個(gè)MVVM框架,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

目前成都創(chuàng)新互聯(lián)已為千余家的企業(yè)提供了網(wǎng)站建設(shè)、域名、雅安服務(wù)器托管、網(wǎng)站運(yùn)營、企業(yè)網(wǎng)站設(shè)計(jì)、瀘水網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
<body>
<div id="mvvm-app">
{{name}}
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
let vm = new MVVM({
el: "#mvvm-app",
data: {
name: "hello world"
},
})
</script>
</body>數(shù)據(jù)代理
1、什么是數(shù)據(jù)代理
在vue里面,我們將數(shù)據(jù)寫在data對象中。但是我們在訪問data里的數(shù)據(jù)時(shí),既可以通過vm.data.name訪問,也可以通過vm.name訪問。這就是數(shù)據(jù)代理:在一個(gè)對象中,可以動(dòng)態(tài)的訪問和設(shè)置另一個(gè)對象的屬性。
2、實(shí)現(xiàn)原理
我們知道靜態(tài)綁定(如vm.name = vm.data.name)可以一次性的將結(jié)果賦給變量,而使用Object.defineProperty()方法來綁定則可以通過set和get函數(shù)實(shí)現(xiàn)賦值的中間過程,從而實(shí)現(xiàn)數(shù)據(jù)的動(dòng)態(tài)綁定。具體實(shí)現(xiàn)如下:
let obj = {};
let obj1 = {
name: 'xiaoyu',
age: 18,
}
//實(shí)現(xiàn)origin對象代理target對象
function proxyData(origin,target){
Object.keys(target).forEach(function(key){
Object.defineProperty(origin,key,{//定義origin對象的key屬性
enumerable: false,
configurable: true,
get: function getter(){
return target[key];//origin[key] = target[key];
},
set: function setter(newValue){
target[key] = newValue;
}
})
})
}vue中的數(shù)據(jù)代理也是通過這種方式來實(shí)現(xiàn)的。
function MVVM(options) {
this.$options = options || {};
var data = this._data = this.$options.data;
var _this = this;//當(dāng)前實(shí)例vm
// 數(shù)據(jù)代理
// 實(shí)現(xiàn) vm._data.xxx -> vm.xxx
Object.keys(data).forEach(function(key) {
_this._proxyData(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this);
}
MVVM.prototype = {
_proxyData: function(key) {
var _this = this;
if (typeof key == 'object' && !(key instanceof Array)){//這里只實(shí)現(xiàn)了對對象的監(jiān)聽,沒有實(shí)現(xiàn)數(shù)組的
this._proxyData(key);
}
Object.defineProperty(_this, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return _this._data[key];
},
set: function proxySetter(newVal) {
_this._data[key] = newVal;
}
});
},
};實(shí)現(xiàn)Observe
1、雙向數(shù)據(jù)綁定
數(shù)據(jù)變動(dòng) ---> 視圖更新
視圖更新 ---> 數(shù)據(jù)變動(dòng)
要想實(shí)現(xiàn)當(dāng)數(shù)據(jù)變動(dòng)時(shí)視圖更新,首先要做的就是如何知道數(shù)據(jù)變動(dòng)了,可以通過Object.defineProperty()函數(shù)監(jiān)聽data對象里的數(shù)據(jù),當(dāng)數(shù)據(jù)變動(dòng)了就會觸發(fā)set()方法。所以我們需要實(shí)現(xiàn)一個(gè)數(shù)據(jù)監(jiān)聽器Observe,來對數(shù)據(jù)對象中的所有屬性進(jìn)行監(jiān)聽,當(dāng)某一屬性數(shù)據(jù)發(fā)生變化時(shí),拿到最新的數(shù)據(jù)通知綁定了該屬性的訂閱器,訂閱器再執(zhí)行相應(yīng)的數(shù)據(jù)更新回調(diào)函數(shù),從而實(shí)現(xiàn)視圖的刷新。
當(dāng)設(shè)置this.name = 'hello vue'時(shí),就會執(zhí)行set函數(shù),通知訂閱器里的訂閱者執(zhí)行相應(yīng)的回調(diào)函數(shù),實(shí)現(xiàn)數(shù)據(jù)變動(dòng),對應(yīng)視圖更新。
function observe(data){
if (typeof data != 'object') {
return ;
}
return new Observe(data);
}
function Observe(data){
this.data = data;
this.walk(data);
}
Observe.prototype = {
walk: function(data){
let _this = this;
for (key in data) {
if (data.hasOwnProperty(key)){
let value = data[key];
if (typeof value == 'object'){
observe(value);
}
_this.defineReactive(data,key,data[key]);
}
}
},
defineReactive: function(data,key,value){
Object.defineProperty(data,key,{
enumerable: true,//可枚舉
configurable: false,//不能再define
get: function(){
console.log('你訪問了' + key);return value;
},
set: function(newValue){
console.log('你設(shè)置了' + key);
if (newValue == value) return;
value = newValue;
observe(newValue);//監(jiān)聽新設(shè)置的值
}
})
}
}2、實(shí)現(xiàn)一個(gè)訂閱器
要想通知訂閱者,首先得要有一個(gè)訂閱器(統(tǒng)一管理所有的訂閱者)。為了方便管理,我們會為每一個(gè)data對象的屬性都添加一個(gè)訂閱器(new Dep)。
訂閱器里存著的是訂閱者Watcher(后面會講到),由于訂閱者可能會有多個(gè),我們需要建立一個(gè)數(shù)組來維護(hù)。一旦數(shù)據(jù)變化,就會觸發(fā)訂閱器的notify()方法,訂閱者就會調(diào)用自身的update方法實(shí)現(xiàn)視圖更新。
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub) {
sub.update();
})
}
}每次響應(yīng)屬性的set()函數(shù)調(diào)用的時(shí)候,都會觸發(fā)訂閱器,所以代碼補(bǔ)充完整。
Observe.prototype = {
//省略的代碼未作更改
defineReactive: function(data,key,value){
let dep = new Dep();//創(chuàng)建一個(gè)訂閱器,會被閉包在key屬性的get/set函數(shù)內(nèi),因此每個(gè)屬性對應(yīng)唯一一個(gè)訂閱器dep實(shí)例
Object.defineProperty(data,key,{
enumerable: true,//可枚舉
configurable: false,//不能再define
get: function(){
console.log('你訪問了' + key);
return value;
},
set: function(newValue){
console.log('你設(shè)置了' + key);
if (newValue == value) return;
value = newValue;
observe(newValue);//監(jiān)聽新設(shè)置的值
dep.notify();//通知所有的訂閱者
}
})
}
}實(shí)現(xiàn)Complie
compile主要做的事情是解析模板指令,將模板中的data屬性替換成data屬性對應(yīng)的值(比如將{{name}}替換成data.name值),然后初始化渲染頁面視圖,并且為每個(gè)data屬性添加一個(gè)監(jiān)聽數(shù)據(jù)的訂閱者(new Watcher),一旦數(shù)據(jù)有變動(dòng),收到通知,更新視圖。
遍歷解析需要替換的根元素el下的HTML標(biāo)簽必然會涉及到多次的DOM節(jié)點(diǎn)操作,因此不可避免的會引發(fā)頁面的重排或重繪,為了提高性能和效率,我們把根元素el下的所有節(jié)點(diǎn)轉(zhuǎn)換為文檔碎片fragment進(jìn)行解析編譯操作,解析完成,再將fragment添加回原來的真實(shí)dom節(jié)點(diǎn)中。
注:文檔碎片本身也是一個(gè)節(jié)點(diǎn),但是當(dāng)將該節(jié)點(diǎn)append進(jìn)頁面時(shí),該節(jié)點(diǎn)標(biāo)簽作為根節(jié)點(diǎn)不會顯示html文檔中,其里面的子節(jié)點(diǎn)則可以完全顯示。
Compile解析模板,將模板內(nèi)的子元素#text添加進(jìn)文檔碎片節(jié)點(diǎn)fragment。
function Compile(el,vm){
this.$vm = vm;//vm為當(dāng)前實(shí)例
this.$el = document.querySelector(el);//獲得要解析的根元素
if (this.$el){
this.$fragment = this.nodeToFragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
nodeToFragment: function(el){
let fragment = document.createDocumentFragment();
let child;
while (child = el.firstChild){
fragment.appendChild(child);//append相當(dāng)于剪切的功能
}
return fragment;
},
};compileElement方法將遍歷所有節(jié)點(diǎn)及其子節(jié)點(diǎn),進(jìn)行掃描解析編譯,調(diào)用對應(yīng)的指令渲染函數(shù)進(jìn)行數(shù)據(jù)渲染,并調(diào)用對應(yīng)的指令更新函數(shù)進(jìn)行綁定,詳看代碼及注釋說明:
因?yàn)槲覀兊哪0逯缓幸粋€(gè)文本節(jié)點(diǎn)#text,因此compileElement方法執(zhí)行后會進(jìn)入_this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'
Compile.prototype = {
nodeToFragment: function(el){
let fragment = document.createDocumentFragment();
let child;
while (child = el.firstChild){
fragment.appendChild(child);//append相當(dāng)于剪切的功能
}
return fragment;
},
init: function(){
this.compileElement(this.$fragment);
},
compileElement: function(node){
let childNodes = node.childNodes;
const _this = this;
let reg = /\{\{(.*)\}\}/g;
[].slice.call(childNodes).forEach(function(node){
if (_this.isElementNode(node)){//如果為元素節(jié)點(diǎn),則進(jìn)行相應(yīng)操作
_this.compile(node);
} else if (_this.isTextNode(node) && reg.test(node.textContent)){
//如果為文本節(jié)點(diǎn),并且包含data屬性(如{{name}}),則進(jìn)行相應(yīng)操作
_this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'
}
if (node.childNodes && node.childNodes.length){
//如果節(jié)點(diǎn)內(nèi)還有子節(jié)點(diǎn),則遞歸繼續(xù)解析節(jié)點(diǎn)
_this.compileElement(node);
}
})
},
compileText: function(node,exp){//#text,'name'
compileUtil.text(node,this.$vm,exp);//#text,vm,'name'
},};CompileText()函數(shù)實(shí)現(xiàn)初始化渲染頁面視圖(將data.name的值通過#text.textContent = data.name顯示在頁面上),并且為每個(gè)DOM節(jié)點(diǎn)添加一個(gè)監(jiān)聽數(shù)據(jù)的訂閱者(這里是為#text節(jié)點(diǎn)新增一個(gè)Wather)。
let updater = {
textUpdater: function(node,value){
node.textContent = typeof value == 'undefined' ? '' : value;
},
}
let compileUtil = {
text: function(node,vm,exp){//#text,vm,'name'
this.bind(node,vm,exp,'text');
},
bind: function(node,vm,exp,dir){//#text,vm,'name','text'
let updaterFn = updater[dir + 'Updater'];
updaterFn && updaterFn(node,this._getVMVal(vm,exp));
new Watcher(vm,exp,function(value){
updaterFn && updaterFn(node,value)
});
console.log('加進(jìn)去了');
}
};現(xiàn)在我們完成了一個(gè)能實(shí)現(xiàn)文本節(jié)點(diǎn)解析的Compile()函數(shù),接下來我們實(shí)現(xiàn)一個(gè)Watcher()函數(shù)。
實(shí)現(xiàn)Watcher
我們前面講過,Observe()函數(shù)實(shí)現(xiàn)data對象的屬性劫持,并在屬性值改變時(shí)觸發(fā)訂閱器的notify()通知訂閱者Watcher,訂閱者就會調(diào)用自身的update方法實(shí)現(xiàn)視圖更新。
Compile()函數(shù)負(fù)責(zé)解析模板,初始化頁面,并且為每個(gè)data屬性新增一個(gè)監(jiān)聽數(shù)據(jù)的訂閱者(new Watcher)。
Watcher訂閱者作為Observer和Compile之間通信的橋梁,所以我們可以大致知道Watcher的作用是什么。
主要做的事情是:
在自身實(shí)例化時(shí)往訂閱器(dep)里面添加自己。
自身必須有一個(gè)update()方法 。
待屬性變動(dòng)dep.notice()通知時(shí),能調(diào)用自身的update()方法,并觸發(fā)Compile中綁定的回調(diào)。
先給出全部代碼,再分析具體的功能。
//Watcher
function Watcher(vm, exp, cb) {
this.vm = vm;
this.cb = cb;
this.exp = exp;
this.value = this.get();//初始化時(shí)將自己添加進(jìn)訂閱器
};
Watcher.prototype = {
update: function(){
this.run();
},
run: function(){
const value = this.vm[this.exp];
//console.log('me:'+value);
if (value != this.value){
this.value = value;
this.cb.call(this.vm,value);
}
},
get: function() {
Dep.target = this; // 緩存自己
var value = this.vm[this.exp] // 訪問自己,執(zhí)行defineProperty里的get函數(shù)
Dep.target = null; // 釋放自己
return value;
}
}
//這里列出Observe和Dep,方便理解
Observe.prototype = {
defineReactive: function(data,key,value){
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable: true,//可枚舉
configurable: false,//不能再define
get: function(){
console.log('你訪問了' + key);
//說明這是實(shí)例化Watcher時(shí)引起的,則添加進(jìn)訂閱器
if (Dep.target){
//console.log('訪問了Dep.target');
dep.addSub(Dep.target);
}
return value;
},
})
}
}
Dep.prototype = {
addSub: function(sub){this.subs.push(sub);
},
}我們知道在Observe()函數(shù)執(zhí)行時(shí),我們?yōu)槊總€(gè)屬性都添加了一個(gè)訂閱器dep,而這個(gè)dep被閉包在屬性的get/set函數(shù)內(nèi)。所以,我們可以在實(shí)例化Watcher時(shí)調(diào)用this.get()函數(shù)訪問data.name屬性,這會觸發(fā)defineProperty()函數(shù)內(nèi)的get函數(shù),get方法執(zhí)行的時(shí)候,就會在屬性的訂閱器dep添加當(dāng)前watcher實(shí)例,從而在屬性值有變化的時(shí)候,watcher實(shí)例就能收到更新通知。
那么Watcher()函數(shù)中的get()函數(shù)內(nèi)Dep.taeger = this又有什么特殊的含義呢?我們希望的是在實(shí)例化Watcher時(shí)將相應(yīng)的Watcher實(shí)例添加一次進(jìn)dep訂閱器即可,而不希望在以后每次訪問data.name屬性時(shí)都加入一次dep訂閱器。所以我們在實(shí)例化執(zhí)行this.get()函數(shù)時(shí)用Dep.target = this來標(biāo)識當(dāng)前Watcher實(shí)例,當(dāng)添加進(jìn)dep訂閱器后設(shè)置Dep.target=null。
實(shí)現(xiàn)VMVM
MVVM作為數(shù)據(jù)綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監(jiān)聽自己的model數(shù)據(jù)變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達(dá)到數(shù)據(jù)變化 -> 視圖更新;視圖交互變化(input) -> 數(shù)據(jù)model變更的雙向綁定效果。
function MVVM(options) {
this.$options = options || {};
var data = this._data = this.$options.data;
var _this = this;
// 數(shù)據(jù)代理
// 實(shí)現(xiàn) vm._data.xxx -> vm.xxx
Object.keys(data).forEach(function(key) {
_this._proxyData(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this);
}關(guān)于怎么在vue中實(shí)現(xiàn)一個(gè)MVVM框架就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
新聞名稱:怎么在vue中實(shí)現(xiàn)一個(gè)MVVM框架
當(dāng)前鏈接:http://chinadenli.net/article34/gspcse.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁設(shè)計(jì)公司、網(wǎng)站導(dǎo)航、動(dòng)態(tài)網(wǎng)站、電子商務(wù)、品牌網(wǎng)站設(shè)計(jì)、網(wǎng)站策劃
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)