MVVM實現類似Vue的基本功能
html檔案中引入自定義的MVVM.js
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div id="app"> <input type="text" v-model="school.name" /> {{school.name}} <div>{{school.name}}</div> <div>{{school.age}}</div> <div v-html="message"></div> <ul> <li>1</li> <li>2</li> </ul> {{getMyLove}} <button v-on:click="change">+</button> </div> <script src="./自定義mvvm.js"></script> <script> let vm=new Vue({ el:"#app", data:{ school:{ name: "宋楊", age:22 }, message:"<h1>大家好</h1>" }, computed:{ getMyLove(){ return this.school.name+"喜歡琳琳"; } }, methods:{ change(){ this.school.age=this.school.age+1; //為什麼這樣寫不行 this.school.age=this.school.age++; } } }) </script> </body> </html>
我們需要先定義幾個關鍵的類:
Vue:整個框架的入口。
Observer:把$data資料全部轉化成Object.defineProperty()來定義。資料劫持。
Watcher:觀察者,每個使用到資料的地方都要建立觀察者,並且繫結到對應的被觀察者身上,該過程就是訂 閱。
Dep:是一個包含釋出和訂閱的功能,data裡面的每個資料都需要有一個Dep例項。
Compiler:用來編譯模版的,簡單實現了虛擬DOM的功能。
2.簡述一下大致的流程。
首先,進入的是vue例項,在裡面獲取對應的el、data、computed、method等資料。先把data的資料全部轉化成Object.defineProperty()定義。然後把computed、method這些方法新增代理,例如,用this.XXX就能指代this.computed.XXX()。再把資料獲取操作vm.$data上的取值操作 都代理到 vm上。最後就可以建立Compiler例項編譯模版了。
注意:代理操作其實是更方便新手入門,更簡單的操作資料。
Observer類裡面會將data物件裡面的屬性全部用Object.defineProperty()定義,若屬性也是物件,就把該屬性裡面的子屬性也重定義,使用了遞迴的思想。同時,在每一個的get方法裡面都有訂閱觀察者的功能,在每一個set方法裡面都有釋出功能。
Dep就是在Observer類中使用訂閱和釋出功能時候需要呼叫的類。
Compiler模版編譯中,先獲取到對應的根節點,用document。createDocumentFragment()建立文件碎片,將根結點的child都用append移動到文件碎片中。這樣的話就相當於是虛擬Dom,最後使用的時候把該文件碎片塞回到頁面中。
同時分別對元素和文字編譯,獲取到對應的指令或{{}},對v-model和{{}}都要新增觀察者,並且把對應的值傳到頁面中。
MVVM.js
class Compiler { constructor(el, vm) { // 判斷el是字串還是元素。如果是字串,通過字串獲取相應元素 this.el = this.isElementNode(el) ? el : document.querySelector(el); // 把當前節點中的元素 獲取到 放到記憶體儲中 this.vm = vm; let fragment = this.createFragment(this.el); // 把節點中的內容替換 // 用資料編譯模版 this.compiler(fragment); // 把內容塞到頁面中 this.el.appendChild(fragment); } isDirective(attrName) { //判斷是不是指令 return attrName.startsWith('v-'); } compilerElement(node) { //編譯元素 let attributes = node.attributes; [...attributes].forEach(attr => { let {name,value:expr} = attr; if (this.isDirective(name)) { let [,directive]=name.split('-'); let [directiveName,eventName]=directive.split(':'); //需要呼叫不同的指令來處理 CompilerUtil[directiveName](node,expr,this.vm,eventName); } }) } compilerText(node) { //編譯文字,判斷文字中是否有{{}} let content = node.textContent; if(/\{\{(.+?)\}\}/.test(content)){ //文字節點 CompilerUtil['text'](node,content,this.vm);//{{a}} {{b}} } } //核心編譯方法 compiler(node) { //編譯記憶體中dom let childNodes = node.childNodes; [...childNodes].forEach(child => { if (this.isElementNode(child)) { this.compilerElement(child); //如果是元素的話 需要把自己傳進去 再去遍歷子節點 this.compiler(child); } else { this.compilerText(child); } }) } createFragment(node) { //把節點移動到記憶體中 // 建立文件碎片 let fragment = document.createDocumentFragment(); let firstChild; while (firstChild = node.firstChild) { // appendChild()具有移動性 fragment.appendChild(firstChild); } return fragment; } isElementNode(node) { //判斷是否為元素節點 return node.nodeType === 1; } } CompilerUtil={ getValue(vm,expr){ return expr.split('.').reduce((data,current)=>{ return data[current]; },vm.$data) }, setValue(vm,expr,value){ expr.split('.').reduce((data,current,index,arr)=>{ if(index==arr.length-1){ return data[current]=value; } return data[current]; },vm.$data) }, on(node,expr,vm,eventName){ node.addEventListener(eventName,(e)=>{ vm[expr].call(vm,e); }); }, model(node,expr,vm){ let fn=this.updater['modeUpdater']; new Watcher(vm,expr,(newValue)=>{//新增觀察者 fn(node,newValue); }); node.addEventListener('input',(e)=>{ let value=e.target.value; this.setValue(vm,expr,value); }); let value=this.getValue(vm,expr); fn(node,value) }, html(node,expr,vm){ let fn=this.updater['htmlUpdater']; new Watcher(vm,expr,(newValue)=>{//新增觀察者 fn(node,newValue); }); let value=this.getValue(vm,expr); fn(node,value) }, getContentValue(vm,expr){ return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ return this.getValue(vm,args[1]); }); }, text(node,expr,vm){ let fn=this.updater['textUpdater']; let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ new Watcher(vm,args[1],()=>{//新增觀察者 fn(node,this.getContentValue(vm,expr));//返回一個全的字串 }); return this.getValue(vm,args[1]); }); fn(node,content); }, updater:{ modeUpdater(node,value){ node.value=value; }, htmlUpdater(node,value){ node.innerHTML=value; }, textUpdater(node,value){ node.textContent=value; } } } //觀察者 (釋出訂閱) 觀察者(多) 被觀察者(一) class Dep { constructor() { this.subs=[];//存放所有的watcher } //訂閱 addSub(watcher){//新增watcher方法 this.subs.push(watcher); } //釋出 notify(){ this.subs.forEach(watcher=>watcher.updata()); } } // vm.$watch(vm,'school.name',(newValue)=>{}) class Watcher{ constructor(vm,expr,cb){ this.vm=vm; this.expr=expr; this.cb=cb; this.oldValue=this.get(); } get(){ //取資料之前吧當前的觀察者放到Dep.target裡面 Dep.target=this; //由於getValue()會觸發Object.defineProperty的get方法,在get裡面會將此watcher新增到所屬的Dep例項裡面。 let value=CompilerUtil.getValue(this.vm,this.expr); //取完資料之後必須賦空,否則後面其他地方會無法更新Dep.target的值,意味著後面的訂閱都會出錯。 Dep.target=null; return value; } updata(){ let newValue =CompilerUtil.getValue(this.vm,this.expr); if(newValue != this.oldValue){ this.cb(newValue); } } } 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,{ get(){ Dep.target && dep.addSub(Dep.target); return value; }, set:(newValue)=>{ if(value != newValue){ // 賦的新值,需要把它再監控下 this.observer(newValue); value=newValue; dep.notify(); } } }); } } 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); for(let key in computed){ Object.defineProperty(this.$data,key,{ get:()=>{ return computed[key].call(this) } }); } for(let key in methods){ Object.defineProperty(this,key,{ get(){ return methods[key]; } }); } // 把資料獲取操作 vm上的取值操作 都代理到vm.$data this.proxyVm(this.$data); new Compiler(this.$el, this); } } proxyVm(data){ for(let key in data){ Object.defineProperty(this,key,{ get(){ return data[key]; //進行轉化 }, set(newValue){ data[key]=newValue; } }); } } }