代碼實現來源於珠峯公開課mvvm
原理的講解。此文在此記錄一下,經過手寫幾遍代碼加深一下本身對mvvm
理解。javascript
model-view-viewModel,經過數據劫持+發佈訂閱模式來實現。html
mvvm是一種設計思想。Model表明數據模型,能夠在model中定義數據修改和操做的業務邏輯;view表示ui組件,負責將數據模型轉換爲ui展示出來,它作的是數據綁定的聲明、 指令的聲明、 事件綁定的聲明。;而viewModel是一個同步view和model的對象。在mvvm框架中,view和model之間沒有直接的關係,它們是經過viewModel來進行交互的。mvvm不須要手動操做dom,只須要關注業務邏輯就能夠了。 mvvm和mvc的區別在於:mvvm是數據驅動的,而MVC是dom驅動的。mvvm的優勢在於不用操做大量的dom,不須要關注model和view之間的關係,而MVC須要在model發生改變時,須要手動的去更新view。大量操做dom使頁面渲染性能下降,使加載速度變慢,影響用戶體驗。vue
mvvm的核心是數據劫持、數據代理、數據編譯和"發佈訂閱模式"。java
一、數據劫持——就是給對象屬性添加get,set鉤子函數。node
//經過set、get鉤子函數進行數據劫持
function defineReactive(data){
Object.keys(data).forEach(key=>{
const dep=new Dep();
let val=data[key];
this.observe(val);//深層次的監聽
Object.defineProperty(data,key,{
get(){
//添加訂閱者watcher(爲每個數據屬性添加訂閱者,以便實時監聽數據屬性的變化——訂閱)
Dep.target&&dep.addSub(Dep.target);
//返回初始值
return val;
},set(newVal){
if(val!==newVal){
val=newVal;
//通知訂閱者,數據變化了(發佈)
dep.notify();
return newVal;
}
}
})
})
}
複製代碼
二、數據代理數組
將data,methods,compted
上的數據掛載到vm
實例上。讓咱們不用每次獲取數據時,都經過mvvm._data.a.b這種方式,而能夠直接經過mvvm.b.a來獲取。緩存
class MVVM{
constructor(options){
this.$options=options;
this.$data=options.data;
this.$el=options.el;
this.$computed=options.computed;
this.$methods=options.methods;
//劫持數據,監聽數據的變化
new Observer(this.$data);
//將數據掛載到vm實例上
this._proxy(this.$data);
//將方法也掛載到vm上
this._proxy(this.$methods);
//將數據屬性掛載到vm實例上
Object.keys(this.$computed).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return this.$computed[key].call(this);//將vm傳入computed中
}
})
})
//編譯數據
new Compile(this.$el,this)
};
//私有方法,用於數據劫持
_proxy(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
}
})
})
}
}
複製代碼
三、數據編譯mvc
把{{}},v-model,v-html,v-on
,裏面的對應的變量用data裏面的數據進行替換。app
class Compile{
constructor(el,vm){
this.el=this.isElementNode(el)?el:document.querySelector(el);
this.vm=vm;
let fragment=this.nodeToFragment(this.el);
//編譯節點
this.compile(fragment);
//將編譯後的代碼添加到頁面
this.el.appendChild(fragment);
};
//核心編譯方法
compile(node){
const childNodes=node.childNodes;
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){
this.compileElementNode(child);
//若是是元素節點就還得遞歸編譯
this.compile(child);
}else{
this.compileTextNode(child);
}
})
};
//編譯元素節點
compileElementNode(node){
const attrs=node.attributes;
[...attrs].forEach(attr=>{
//attr是一個對象
let {name,value:expr}=attr;
if(this.isDirective(name)){
//只考慮到v-html和v-model的狀況
let [,directive]=name.split("-");
//考慮v-on:click的狀況
let [directiveName,eventName]=directive.split(":");
//調用不一樣的指令來進行編譯
CompileUtil[directiveName](node,this.vm,expr,eventName);
}
})
};
//編譯文本節點
compileTextNode(node){
const textContent=node.textContent;
if(/\{\{(.+?)\}\}/.test(textContent)){
CompileUtil["text"](node,this.vm,textContent)
}
};
//將元素節點轉化爲文檔碎片
nodeToFragment(node){
//將元素節點緩存起來,統一編譯完後再拿出來進行替換
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
};
//判斷是不是元素節點
isElementNode(node){
return node.nodeType===1;
};
//判斷是不是指令
isDirective(attr){
return attr.includes("v-");
}
}
//存放編譯方法的對象
CompileUtil={
//根據data中的屬性獲取值,觸發觀察者的get鉤子
getVal(vm,expr){
const data= expr.split(".").reduce((initData,curProp)=>{
//會觸發觀察者的get鉤子
return initData[curProp];
},vm)
return data;
},
//觸發觀察者的set鉤子
setVal(vm,expr,value){
expr.split(".").reduce((initData,curProp,index,arr)=>{
if(index===arr.length-1){
initData[curProp]=value;
return;
}
return initData[curProp];
},vm)
},
getContentValue(vm,expr){
const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1]);
});
return data;
},
model(node,vm,expr){
const value=this.getVal(vm,expr);
const fn=this.updater["modelUpdater"];
fn(node,value);
//監聽input的輸入事件,實現數據響應式
node.addEventListener('input',e=>{
const value=e.target.value;
this.setVal(vm,expr,value);
})
//觀察數據(expr)的變化,並將watcher添加到訂閱者隊列中
new Watcher(vm,expr,newVal=>{
fn(node,newVal);
});
},
text(node,vm,expr){
const fn=this.updater["textUpdater"];
//將{{person.name}}中的person.james替換成james
const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
//觀察數據的變化
new Watcher(vm,args[1],()=>{
// this.getContentValue(vm,expr)獲取textContent被編譯後的值
fn(node,this.getContentValue(vm,expr))
})
return this.getVal(vm,args[1]);
})
fn(node,content);
},
html(node,vm,expr){
const value=this.getVal(vm,expr);
const fn=this.updater["htmlUpdater"];
fn(node,value);
new Watcher(vm,expr,newVal=>{
//數據改變後,再次替換數據
fn(node,newVal);
})
},
on(node,vm,expr,eventName){
node.addEventListener(eventName,e=>{
//調用call將vm實例(this)傳到方法中去
vm[expr].call(vm,e);
})
},
updater:{
modelUpdater(node,value){
node.value=value
},
htmlUpdater(node,value){
node.innerHTML=value;
},
textUpdater(node,value){
node.textContent=value;
}
}
}
複製代碼
四、發佈訂閱框架
發佈訂閱主要靠的是數組關係,訂閱就是放入函數(就是將訂閱者添加到訂閱隊列中),發佈就是讓數組裏的函數執行(在數據發生改變的時候,通知訂閱者執行相應的操做)。消息的發佈和訂閱是在觀察者的數據綁定中進行數據的——在get鉤子函數被調用時進行數據的訂閱(在數據編譯時經過 new Watcher()來對數據進行訂閱
),在set鉤子函數被調用時進行數據的發佈。
//消息管理者(發佈者),在數據發生變化時,通知訂閱者執行相應的操做
class Dep{
constructor(){
this.subs=[];
};
//訂閱
addSub(watcher){
this.subs.push(watcher);
};
//發佈
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
//訂閱者,主要是觀察數據的變化
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
this.oldValue=this.get();
};
get(){
Dep.target=this;
const value=CompileUtil.getVal(this.vm,this.expr);
Dep.target=null;
return value;
};
update(){
const newVal=CompileUtil.getVal(this.vm,this.expr);
if(this.oldValue!==newVal){
this.cb(newVal);
}
}
}
//觀察者
class Observer{
constructor(data){
this.observe(data);
};
//使數據可響應
observe(data){
if(data&&typeof data==="object"){
this.defineReactive(data)
}
};
defineReactive(data){
Object.keys(data).forEach(key=>{
const dep=new Dep();
let val=data[key];
this.observe(val);//深層次的監聽
Object.defineProperty(data,key,{
get(){
//添加訂閱者watcher(爲每個數據屬性添加訂閱者,以便實時監聽數據屬性的變化——訂閱)
Dep.target&&dep.addSub(Dep.target);
//返回初始值
return val;
},set(newVal){
if(val!==newVal){
val=newVal;
//通知訂閱者,數據變化了(發佈)
dep.notify();
return newVal;
}
}
})
})
}
}
複製代碼