直接從主流的提及!
vue的特色沒必要多說(簡單易用)。修改數據方便不須要記憶api方法,這都歸功於Object.defineProperty,它能夠在數據的設置和獲取時增長咱們本身的功能!(像牆同樣)javascript
MVVM模式就要將這些板塊進行整合,實現模板和數據的綁定!vue
看看我畫圖的功底,有個印象就好!java
看段大衆代碼,接下來咱們就基於這段代碼搞一下MVVM的實現node
<div id="app"> <!-- 雙向數據綁定 靠的是表單 --> <input type="text" v-model="message.a"> <div>我很帥</div> {{message.a}} {{b}} </div> <script src="watcher.js"></script> <script src="observer.js"></script> <script src="compile.js"></script> <script src="MVVM.JS"></script> <script> // 咱們的數據通常都掛載在vm上 let vm = new MVVM({ el:'#app', data:{ message:{a:'jw'}, b:'MVVM' } }) </script> 複製代碼
這裏咱們用了本身的MVVM庫,這個庫是用來整合全部板塊的!面試
直接用ES6來打造咱們的MVVMapi
class MVVM{ constructor(options){ // 一上來 先把可用的東西掛載在實例上 this.$el = options.el; this.$data = options.data; // 若是有要編譯的模板我就開始編譯 if(this.$el){ // 用數據和元素進行編譯 new Compile(this.$el, this); } } } 複製代碼
MVVM中調用了Compile類來編譯咱們的頁面,開始來實現模板編譯數組
先來個基礎的架子性能優化
class Compile { constructor(el, vm) { // 看看傳遞的元素是否是DOM,不是DOM我就來獲取一下~ this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; if (this.el) { // 若是這個元素能獲取到 咱們纔開始編譯 // 1.先把這些真實的DOM移入到內存中 fragment (性能優化) let fragment = this.node2fragment(this.el); // 2.編譯 => 提取想要的元素節點 v-model 和文本節點 {{}} this.compile(fragment); // 3.把編譯號的fragment在塞回到頁面裏去 this.el.appendChild(fragment); } } /* 專門寫一些輔助的方法 */ isElementNode(node) { return node.nodeType === 1; } /* 核心的方法 */ compileElement(node) {} compileText(node) {} compile(fragment) {} node2fragment(el) {} } 複製代碼
接下來一個個的方法來搞app
node2fragment(el) { // 須要將el中的內容所有放到內存中 // 文檔碎片 內存中的dom節點 let fragment = document.createDocumentFragment(); let firstChild; while (firstChild = el.firstChild) { fragment.appendChild(firstChild); // appendChild具備移動性 } return fragment; // 內存中的節點 } 複製代碼
compile(fragment) { // 須要遞歸 每次拿子元素 let childNodes = fragment.childNodes; Array.from(childNodes).forEach(node => { if (this.isElementNode(node)) { // 是元素節點,還須要繼續深刻的檢查 // 這裏須要編譯元素 this.compileElement(node); this.compile(node) } else { // 文本節點 // 這裏須要編譯文本 this.compileText(node); } }); } 複製代碼
咱們在弄出兩個方法compileElement,compileText來專門處理對應的邏輯框架
/*輔助的方法*/ // 是否是指令 isDirective(name) { return name.includes('v-'); } ---------------------------- compileElement(node) { // 帶v-model v-text let attrs = node.attributes; // 取出當前節點的屬性 Array.from(attrs).forEach(attr => { // 判斷屬性名字是否是包含v-model let attrName = attr.name; if (this.isDirective(attrName)) { // 取到對應的值放到節點中 let expr = attr.value; let [, type] = attrName.split('-'); // // 調用對應的編譯方法 編譯哪一個節點,用數據替換掉表達式 CompileUtil[type](node, this.vm, expr); } }) } compileText(node) { let expr = node.textContent; // 取文本中的內容 let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}} if (reg.test(expr)) { // 調用編譯文本的方法 編譯哪一個節點,用數據替換掉表達式 CompileUtil['text'](node, this.vm, expr); } } 複製代碼
咱們要實現一個專門用來配合Complie類的工具對象
先只處理文本和輸入框的狀況
CompileUtil = { text(node, vm, expr) { // 文本處理 let updateFn = this.updater['textUpdater']; // 用處理好的節點和內容進行編譯 updateFn && updateFn(node, value) }, model(node, vm, expr) { // 輸入框處理 let updateFn = this.updater['modelUpdater']; // 用處理好的節點和內容進行編譯 updateFn && updateFn(node, value); }, updater: { // 文本更新 textUpdater(node, value) { node.textContent = value }, // 輸入框更新 modelUpdater(node, value) { node.value = value; } } } 複製代碼
實現text方法
text(node, vm, expr) { // 文本處理 let updateFn = this.updater['textUpdater']; // 文本比較特殊 expr多是'{{message.a}} {{b}}' // 調用getTextVal方法去取到對應的結果 let value = this.getTextVal(vm, expr); updateFn && updateFn(node, value) }, getTextVal(vm, expr) { // 獲取編譯文本後的結果 return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => { // 依次去去數據對應的值 return this.getVal(vm, arguments[1]); }) }, getVal(vm, expr) { // 獲取實例上對應的數據 expr = expr.split('.'); // {{message.a}} [message,a] 實現依次取值 // vm.$data.message => vm.$data.message.a return expr.reduce((prev, next) => { return prev[next]; }, vm.$data); } 複製代碼
實現Model方法
model(node, vm, expr) { // 輸入框處理 let updateFn = this.updater['modelUpdater']; // 這裏應該加一個監控 數據變化了 應該調用這個watch的callback updateFn && updateFn(node, this.getVal(vm, expr)); } 複製代碼
看下編譯後的效果^_^
咱們一直說Object.defineProperty有劫持功能咱就看看這個是怎樣劫持的
默認狀況下定義屬性給屬性設置的操做是這樣的
let school = {name:''} school.name = 'jw'; // 當我給屬性設置時但願作一些操做 console.log(school.name); // 當我獲取屬性時也但願對應有寫操做 複製代碼
這時候Object.defineProperty登場
let school = {name:''} let val; Object.defineProperty(school, 'name', { enumerable: true, // 可枚舉, configurable: true, // 可配置 get() { // todo return val; }, set(newVal) { // todo val = newVal } }); school.name = 'jw'; console.log(school.name); 複製代碼
這樣咱們能夠在設置值和獲取值時作咱們想要作的操做了
接下來咱們就來寫下一個類Observer
// 在MVVM加上Observe的邏輯 if(this.$el){ // 數據劫持 就是把對想的全部屬性 改爲get和set方法 new Observer(this.$data); // 用數據和元素進行編譯 new Compile(this.$el, this); } -------------------------------------- class Observer{ constructor(data){ this.observe(data); } observe(data){ // 要對這個data數據將原有的屬性改爲set和get的形式 // defineProperty針對的是對象 if(!data || typeof data !== 'object'){ return; } // 要將數據 一一劫持 先獲取取到data的key和value Object.keys(data).forEach(key=>{ // 定義響應式變化 this.defineReactive(data,key,data[key]); this.observe(data[key]);// 深度遞歸劫持 }); } // 定義響應式 defineReactive(obj,key,value){ // 在獲取某個值的適合 想彈個框 let that = this; Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ // 當取值時調用的方法 return value; }, set(newValue){ // 當給data屬性中設置值的適合 更改獲取的屬性的值 if(newValue!=value){ // 這裏的this不是實例 that.observe(newValue);// 若是是設置的是對象繼續劫持 value = newValue; } } }); } } 複製代碼
來再看看效果^_^
觀察者的目的就是給須要變化的那個元素增長一個觀察者,用新值和老值進行比對,若是數據變化就執行對應的方法
class Watcher{ // 由於要獲取老值 因此須要 "數據" 和 "表達式" constructor(vm,expr,cb){ this.vm = vm; this.expr = expr; this.cb = cb; // 先獲取一下老的值 保留起來 this.value = this.get(); } // 老套路獲取值的方法,這裏先不進行封裝 getVal(vm, expr) { expr = expr.split('.'); return expr.reduce((prev, next) => { return prev[next]; }, vm.$data); } get(){ let value = this.getVal(this.vm,this.expr); return value; } // 對外暴露的方法,若是值改變就能夠調用這個方法來更新 update(){ let newValue = this.getVal(this.vm, this.expr); let oldValue = this.value; if(newValue != oldValue){ this.cb(newValue); // 對應watch的callback } } } 複製代碼
在哪裏使用watcher?答案確定是compile呀,給須要從新編譯的DOM增長watcher
text(node, vm, expr) { // 文本處理 let updateFn = this.updater['textUpdater']; let value = this.getTextVal(vm, expr); + expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => { + new Watcher(vm, arguments[1],(newValue)=>{ + // 若是數據變化了,文本節點須要從新獲取依賴的屬性更新文本中的內容 + updateFn && updateFn(node,this.getTextVal(vm,expr)); + }); + }) updateFn && updateFn(node, value) }, model(node, vm, expr) { // 輸入框處理 let updateFn = this.updater['modelUpdater']; + new Watcher(vm,expr,(newValue)=>{ + // 當值變化後會調用cb 將新的值傳遞過來 + updateFn && updateFn(node, newValue); + }); updateFn && updateFn(node, this.getVal(vm, expr)); } 複製代碼
如何將視圖和數據關聯起來呢?就是將每一個數據和對應的watcher關聯起來。當數據變化時讓對應的watcher執行update方法便可!再想一想在哪作操做呢?就是咱們的set和get!
Dep實現
class Dep{ constructor(){ // 訂閱的數組 this.subs = [] } addSub(watcher){ this.subs.push(watcher); } notify(){ this.subs.forEach(watcher=>watcher.update()); } } 複製代碼
關聯dep和watcher
watcher中有個重要的邏輯就是this.get();每一個watcher被實例化時都會獲取數據從而會調用當前屬性的get方法
// watcher中的get方法 get(){ // 在取值前先將watcher保存到Dep上 Dep.target = this; let value = this.getVal(this.vm,this.expr); // 會調用屬性對應的get方法 Dep.target = null; return value; } // 更新Observer中的defineReactive defineReactive(obj,key,value){ let that = this; + let dep = new Dep(); // 每一個變化的數據 都會對應一個數組,這個數組是存放全部更新的操做 Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ // 當取值時調用的方法 Dep.target && dep.addSub(Dep.target); return value; }, set(newValue){ if(newValue!=value){ that.observe(newValue); value = newValue; dep.notify(); // 通知全部人 數據更新了 } } }); } 複製代碼
到此數據和視圖就關聯起來了!^_^
setVal(vm,expr,value){ expr = expr.split('.'); return expr.reduce((prev,next,currentIndex)=>{ if(currentIndex === expr.length-1){ return prev[next] = value; } return prev[next]; },vm.$data); }, model(node, vm, expr) { let updateFn = this.updater['modelUpdater']; new Watcher(vm,expr,(newValue)=>{ // 當值變化後會調用cb 將新的值傳遞過來 () updateFn && updateFn(node, this.getVal(vm, expr)); }); + node.addEventListener('input',(e)=>{ + let newValue = e.target.value; + // 監聽輸入事件將輸入的內容設置到對應數據上 + this.setVal(vm,expr,newValue) + }); updateFn && updateFn(node, this.getVal(vm, expr)); } 複製代碼
class MVVM{ constructor(options){ this.$el = options.el; this.$data = options.data; if(this.$el){ new Observer(this.$data); // 將數據代理到實例上直接操做實例便可,不須要經過vm.$data來進行操做 this.proxyData(this.$data); new Compile(this.$el, this); } } proxyData(data){ Object.keys(data).forEach(key=>{ Object.defineProperty(this,key,{ get(){ return data[key] }, set(newValue){ data[key] = newValue } }) }) } } 複製代碼
看看最終效果!