MVVM是Model-View-ViewModel的縮寫,它是一種基於前端開發的架構模式,View和Model之間並無直接的聯繫,而是經過ViewModel進行交互,其核心是ViewModel經過雙向數據綁定將View和Model鏈接起來了,這使得View數據的變化會同步到Model中,而Model數據的變化也會當即反應到View上javascript
在Vue中使用數據劫持,採用Object.defineProperty的getter和setter,並結合觀察者模式來實現數據綁定。當把一個js對象傳給Vue實例來做爲它的data屬性時,Vue會遍歷它的屬性,用Object.defineProperty將它們賦予set和get,在內部它們讓Vue追蹤依賴,當屬性被訪問和修改時通知變化。 總體以下圖: html
具體分析如圖: 前端
Observer:數據監聽器,可以對數據對象的全部屬性進行監聽,若有變更可拿到最新值並通知訂閱者,內部採用Object.defineProperty的getter和Setter來實現的。vue
Compile:模板編譯,它的做用對每一個元素節點的指令和文本節點進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數。java
Watcher:訂閱者,做爲鏈接Observer和Compile的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令綁定的相應回調函數。node
Dep:消息訂閱器,內部定義了一個數組,用來收集訂閱者(Watcher),數據變更觸發notify函數,再調用訂閱者的update方法。數組
分析上圖:當執行new Vue()時,Vue就進入了初始化階段,一方面Vue會遍歷data選項中的屬性,並用Object.defineProperty將它們轉換爲getter/setter,實現數據變化監聽功能;另外一方面,Vue的模板編譯Compile對元素節點的指令和文本節點進行掃描和解析,初始化視圖,Object.defineProperty在get鉤子中addSub訂閱Watcher並添加到消息訂閱器(Dep)中,初始化完成。 當數據發生變化時,Observer中的setter方法被觸發,setter會當即調用Dep.notify(),Dep開始遍歷全部的訂閱者,並調用訂閱者的update方法,訂閱者收到通知後對視圖進行相應的更新。架構
在入口Vue類中調用Observer進行數據劫持,將數據變成響應式數據;調用Compile模板編譯,找到須要替換數據的元素,進行編譯及初始化;最後進行數據代理,實現vm.school而不用使用vm.$data.school調用數據app
具體代碼以下:dom
class Vue{
constructor(options){
this.$el=options.el;
this.$data=options.data;
// 若是$el存在,那麼能夠找到上面的HTML模塊
if(this.$el){
// 把數據變成響應式 當 new Observer後,school就變成了響應式數據
new Observer(this.$data)
// 如今也須要讓vm代理this.$data
this.proxyVm(this.$data)
// console.log(this.$data)
// 須要找到模塊中須要替換數據的元素,編譯模板
new Compiler(this.$el,this)
}
}
// 讓vm代理data
proxyVm(data){
for(let key in data){ //data: {school:{name:beida,age:100}}
// console.log(this) this vm實例
Object.defineProperty(this,key,{
get(){
return data[key]
}
})
}
}
}
複製代碼
主要分三步:
把真實的dom利用文檔碎片(fragment)移入到內存中,減少內存消耗,操做dom速度加快; 補充:將el中的內容移入到文檔碎片fragment中是一個進出棧的過程,el的子元素被移到fragment,出棧後,el的下一個子元素就會變成firstChild
編譯--遍歷元素節點和文本節點v-model...,{{}},而後執行相應的操做。 具體操做:
把編譯好的fragment放回到原頁面中
具體代碼實現:
class Compiler{
constructor(el,vm){
this.el=this.isElementNode(el)?el:document.querySelector(el);
this.vm=vm;
// console.log(this.el)
let fragment=this.node2fragment(this.el);
// console.log(fragment);
// 替換操做 (編譯模板) 用數據來編譯
this.compile(fragment);
// 把替換完的數據從新給網頁
this.el.appendChild(fragment)
}
// 判斷一個屬性是不是一個指令
isDirective(attrName){
return attrName.startsWith("v-"); //返回的是boolean值
}
// 編譯元素節點
compileElement(node){
let attributes=node.attributes; //獲得某個元素的屬性節點 是個僞數組
// console.log(attributes)
[...attributes].forEach(attr=>{
let {name,value:expr}=attr; //解構賦值
// console.log(expr)
if(this.isDirective(name)){
// console.log(name+"是一個指令"); //v-model
let [,directive]=name.split('-');
// console.log(directive) //model,將v-去掉
ComplierUtil[directive](node,expr,this.vm);
}
})
}
// 編譯文本節點
compileText(node){
let content=node.textContent;
let reg=/\{\{(.+?)\}\}/;
//reg.test(content) 若是content知足咱們寫的正則,返回true,不然false
if(reg.test(content)){
ComplierUtil["text"](node,content,this.vm);
}
}
// 編譯
compile(node){
// childNodes並不包含li獲得的僅僅是子節點
// console.log(node.childNodes) [text, input, text, div, text, div, text, ul, text]
let childNodes=node.childNodes;
// console.log(Array.isArray(childNodes)) //獲得的childNodes是一個僞數組
[...childNodes].forEach(child=>{ //[...childNodes]將僞數組childNodes轉變爲真正數組
if(this.isElementNode(child)){
// console.log(child+"是一個元素節點")
this.compileElement(child);
// 可能一個元素節點中嵌套其餘的元素節點,還可能嵌套文本節點
// 若是child內部還有其餘節點,須要利用遞歸從新編譯
this.compile(child);
}else{
// console.log(child+"獲得的是文本節點")
this.compileText(child);
}
})
}
// 判斷一個節點是不是元素節點
isElementNode(node){
return node.nodeType===1;
}
// 將網頁的HTML移到文檔碎片中
node2fragment(node){
// 建立一個文檔碎片
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
}
// 寫一個對象{},包含了不一樣的指令對應不一樣的處理方法
ComplierUtil={
getVal(vm,expr){
// console.log(expr.split(".")) // ["school","name"]
// 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school
// 第二次data是school,current是name 即return data[current]==> school[current]
return expr.split(".").reduce((data,current)=>{
return data[current];
},vm.$data);
},
setVal(vm,expr,value){
// console.log(expr.split(".")) // ["school","name"]
// 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school,index是0,arr是["school","name"]
// 第二次data是undefined(沒有處理累加,默認是undefined),current是name,index是1,arr是["school","name"]
expr.split(".").reduce((data,current,index,arr)=>{
// console.log(data)
if(index==arr.length-1){
// console.log(current)
// console.log(data)
return data[current]=value;
// console.log(data[current])
// console.log(111)
}
return data[current];
},vm.$data)
},
model(node,expr,vm){ //node是帶指令的元素節點,expr是表達式,vm是vue對象
let value=this.getVal(vm,expr)
let fn=this.updater["modelUpdater"]
// 給輸入框添加一個觀察者,若是後面數據發生改變了,就通知觀察者
new Watcher(vm,expr,(newVal)=>{
fn(node,newVal);
})
// 給input添加一個input事件,
node.addEventListener("input",(e)=>{
let value=e.target.value;
this.setVal(vm,expr,value);
})
fn(node,value)
},
html(){
},
// 獲得新的內容
getContentValue(vm,expr){
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1])
});
},
text(node,expr,vm){
// console.log(node) //"{{school.name}}"
// console.log(expr) //{{school.name}} {{school.age}}
// console.log(vm)
let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
// console.log(vm)
// console.log(args)
new Watcher(vm,args[1],()=>{
fn(node,this.getContentValue(vm,expr));
})
return this.getVal(vm,args[1]) //baida 100
})
let fn=this.updater["textUpdater"];
fn(node,content)
},
// 更新數據
updater:{
modelUpdater(node,value){
node.value=value;
},
htmlUpdater(){
},
// 處理文本節點
textUpdater(node,value){
// textContent獲得文本節點中內容
node.textContent=value
}
}
}
複製代碼
// 實現數據的響應式--->數據劫持,當獲取修改數據時,須要感應到(set和get)
class Observer{
constructor(data){
this.observer(data)
}
// 把上面的數據變成響應式數據,把一個對象數據作出響應式
observer(data){
if(data&& typeof data=='object'){
// console.log(data) //{school: {name: "beida", age: 100}}
// for in 循環一個js對象
for(let key in data){
// console.log(key) //school
// console.log(data[key]) //{name: "beida", age: 100}
this.defindReactive(data,key,data[key])
}
}
}
defindReactive(obj,key,value){
this.observer(value) //若是一個數據是一個對象,也須要將其變成響應式
// Object.defineProperty(obj,prop,descriptor)函數會直接在obj上定義一個新屬性或修改一個新屬性
// obj要在其上定義屬性或修改的對象,prop要定義或修改的屬性名稱,descriptor 將被定義或修改的屬性描述符
// 這是要修改obj對象的school屬性
let dep=new Dep(); //不一樣的watcher放到不一樣的dep中
Object.defineProperty(obj,key,{
// 修改以下 當獲取school時,會調用get
get(){
Dep.target&&dep.subs.push(Dep.target)
// console.log("get...")
return value
},
// 當設置school時,會調用set
set:(newVal)=>{
if(newVal!=value){
// console.log("set...")
this.observer(newVal)
value=newVal;
// 值改變時,通知觀察者
dep.notify();
}
}
})
}
}
複製代碼
// 觀察者
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
// 剛開始須要一個老的狀態
this.oldValue=this.get();
}
get(){
Dep.target=this;
let value=ComplierUtil.getVal(this.vm,this.expr);
Dep.target=null;
return value;
}
// 當狀態改變後,會調用觀察者的update
update(){
let newVal=ComplierUtil.getVal(this.vm,this.expr);
if(newVal!=this.oldValue){
this.cb(newVal)
}
}
}
複製代碼
// 存儲觀察者的類Dep
class Dep{
constructor(){
this.subs=[]; //在subs中存放因此的watcher
}
// 添加watcher即訂閱
addSub(watcher){
this.subs.push(watcher)
}
// 通知 發佈 通知subs容器中的全部觀察者
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
複製代碼
完整代碼以下:
// 存儲觀察者的類Dep
class Dep{
constructor(){
this.subs=[]; //在subs中存放因此的watcher
}
// 添加watcher即訂閱
addSub(watcher){
this.subs.push(watcher)
}
// 通知 發佈 通知subs容器中的全部觀察者
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;
let value=ComplierUtil.getVal(this.vm,this.expr);
Dep.target=null;
return value;
}
// 當狀態改變後,會調用觀察者的update
update(){
let newVal=ComplierUtil.getVal(this.vm,this.expr);
if(newVal!=this.oldValue){
this.cb(newVal)
}
}
}
// 實現數據的響應式--->數據劫持,當獲取修改數據時,須要感應到(set和get)
class Observer{
constructor(data){
this.observer(data)
}
// 把上面的數據變成響應式數據,把一個對象數據作出響應式
observer(data){
if(data&& typeof data=='object'){
// console.log(data) //{school: {name: "beida", age: 100}}
// for in 循環一個js對象
for(let key in data){
// console.log(key) //school
// console.log(data[key]) //{name: "beida", age: 100}
this.defindReactive(data,key,data[key])
}
}
}
defindReactive(obj,key,value){
this.observer(value) //若是一個數據是一個對象,也須要將其變成響應式
// Object.defineProperty(obj,prop,descriptor)函數會直接在obj上定義一個新屬性或修改一個新屬性
// obj要在其上定義屬性或修改的對象,prop要定義或修改的屬性名稱,descriptor 將被定義或修改的屬性描述符
// 這是要修改obj對象的school屬性
let dep=new Dep(); //不一樣的watcher放到不一樣的dep中
Object.defineProperty(obj,key,{
// 修改以下 當獲取school時,會調用get
get(){
Dep.target&&dep.subs.push(Dep.target)
// console.log("get...")
return value
},
// 當設置school時,會調用set
set:(newVal)=>{
if(newVal!=value){
// console.log("set...")
this.observer(newVal)
value=newVal;
// 值改變時,通知觀察者
dep.notify();
}
}
})
}
}
class Compiler{
constructor(el,vm){
this.el=this.isElementNode(el)?el:document.querySelector(el);
this.vm=vm;
// console.log(this.el)
let fragment=this.node2fragment(this.el);
// console.log(fragment);
// 替換操做 (編譯模板) 用數據來編譯
this.compile(fragment);
// 把替換完的數據從新給網頁
this.el.appendChild(fragment)
}
// 判斷一個屬性是不是一個指令
isDirective(attrName){
return attrName.startsWith("v-"); //返回的是boolean值
}
// 編譯元素節點
compileElement(node){
let attributes=node.attributes; //獲得某個元素的屬性節點 是個僞數組
// console.log(attributes)
[...attributes].forEach(attr=>{
let {name,value:expr}=attr; //解構賦值
// console.log(expr)
if(this.isDirective(name)){
// console.log(name+"是一個指令"); //v-model
let [,directive]=name.split('-');
// console.log(directive) //model,將v-去掉
ComplierUtil[directive](node,expr,this.vm);
}
})
}
// 編譯文本節點
compileText(node){
let content=node.textContent;
let reg=/\{\{(.+?)\}\}/;
//reg.test(content) 若是content知足咱們寫的正則,返回true,不然false
if(reg.test(content)){
ComplierUtil["text"](node,content,this.vm);
}
}
// 編譯
compile(node){
// childNodes並不包含li獲得的僅僅是子節點
// console.log(node.childNodes) [text, input, text, div, text, div, text, ul, text]
let childNodes=node.childNodes;
// console.log(Array.isArray(childNodes)) //獲得的childNodes是一個僞數組
[...childNodes].forEach(child=>{ //[...childNodes]將僞數組childNodes轉變爲真正數組
if(this.isElementNode(child)){
// console.log(child+"是一個元素節點")
this.compileElement(child);
// 可能一個元素節點中嵌套其餘的元素節點,還可能嵌套文本節點
// 若是child內部還有其餘節點,須要利用遞歸從新編譯
this.compile(child);
}else{
// console.log(child+"獲得的是文本節點")
this.compileText(child);
}
})
}
// 判斷一個節點是不是元素節點
isElementNode(node){
return node.nodeType===1;
}
// 將網頁的HTML移到文檔碎片中
node2fragment(node){
// 建立一個文檔碎片
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
}
// 寫一個對象{},包含了不一樣的指令對應不一樣的處理方法
ComplierUtil={
getVal(vm,expr){
// console.log(expr.split(".")) // ["school","name"]
// 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school
// 第二次data是school,current是name 即return data[current]==> school[current]
return expr.split(".").reduce((data,current)=>{
return data[current];
},vm.$data);
},
setVal(vm,expr,value){
// console.log(expr.split(".")) // ["school","name"]
// 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school,index是0,arr是["school","name"]
// 第二次data是undefined(沒有處理累加,默認是undefined),current是name,index是1,arr是["school","name"]
expr.split(".").reduce((data,current,index,arr)=>{
// console.log(data)
if(index==arr.length-1){
// console.log(current)
// console.log(data)
return data[current]=value;
// console.log(data[current])
// console.log(111)
}
return data[current];
},vm.$data)
},
model(node,expr,vm){ //node是帶指令的元素節點,expr是表達式,vm是vue對象
let value=this.getVal(vm,expr)
let fn=this.updater["modelUpdater"]
// 給輸入框添加一個觀察者,若是後面數據發生改變了,就通知觀察者
new Watcher(vm,expr,(newVal)=>{
fn(node,newVal);
})
// 給input添加一個input事件,
node.addEventListener("input",(e)=>{
let value=e.target.value;
this.setVal(vm,expr,value);
})
fn(node,value)
},
html(){
},
// 獲得新的內容
getContentValue(vm,expr){
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1])
});
},
text(node,expr,vm){
// console.log(node) //"{{school.name}}"
// console.log(expr) //{{school.name}} {{school.age}}
// console.log(vm)
let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
// console.log(vm)
// console.log(args)
new Watcher(vm,args[1],()=>{
fn(node,this.getContentValue(vm,expr));
})
return this.getVal(vm,args[1]) //baida 100
})
let fn=this.updater["textUpdater"];
fn(node,content)
},
// 更新數據
updater:{
modelUpdater(node,value){
node.value=value;
},
htmlUpdater(){
},
// 處理文本節點
textUpdater(node,value){
// textContent獲得文本節點中內容
node.textContent=value
}
}
}
class Vue{
constructor(options){
this.$el=options.el;
this.$data=options.data;
// 若是$el存在,那麼能夠找到上面的HTML模塊
if(this.$el){
// 把數據變成響應式 當 new Observer後,school就變成了響應式數據
new Observer(this.$data)
// 如今也須要讓vm代理this.$data
this.proxyVm(this.$data)
// console.log(this.$data)
// 須要找到模塊中須要替換數據的元素,編譯模板
new Compiler(this.$el,this)
}
}
// 讓vm代理data
proxyVm(data){
for(let key in data){ //data: {school:{name:beida,age:100}}
// console.log(this) this vm實例
Object.defineProperty(this,key,{
get(){
return data[key]
}
})
}
}
}
複製代碼