mvvm 原理

看完這篇關於MVVM的文章,面試通過率提升了80%

來看看目前最火的MVVM

  • 今天面試又被問到什麼是MVVM?
  • 光靠說理論已經糊弄不過去了?
  • 什麼!MVVM的實現不止一種啊?
  • 往下看~ 親手帶你剖析MVVM原理!

先來總結下MVVM的實現方式

  • 傳統的MVC中通過發佈訂閱來進行數據和視圖的綁定監聽
  • angular1.x中通過髒值檢測來實現MVVM模式
  • 目前主流Vue的模式:數據劫持 Object.defineProperty、發佈訂閱
  • ES6中的新特性Proxy和Reflect

談談現代版的框架

直接從主流的說起!
vue的特點不必多說(簡單易用)。修改數據方便不需要記憶api方法,這都歸功於Object.defineProperty,它可以在數據的設置和獲取時增加我們自己的功能!(像牆一樣)

總結下實現MVVM都要掌握哪些!

  • 模板編譯(Compile)
  • 數據劫持(Observer)
  • 發佈的訂閱(Dep)
  • 觀察者(Watcher)

MVVM模式就要將這些板塊進行整合,實現模板和數據的綁定!

看看我畫圖的功底,有個印象就好!

 

 

先簡單來說說MVVM

 

 

  • 數據就是簡單的javascript對象,需要將數據綁定到模板上
  • 監聽視圖的變化,視圖變化後通知數據更新,數據更新會再次導致視圖的變化!

Vue基礎案例

看段大衆代碼,接下來我們就基於這段代碼搞一下MVVM的實現

<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庫,這個庫是用來整合所有板塊的!

MVVM構建

直接用ES6來打造我們的MVVM

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) {}
}
複製代碼

接下來一個個的方法來搞

node2fragment

node2fragment(el) { // 需要將el中的內容全部放到內存中
    // 文檔碎片 內存中的dom節點
    let fragment = document.createDocumentFragment();
    let firstChild;
    while (firstChild = el.firstChild) {
        fragment.appendChild(firstChild);
        // appendChild具有移動性
    }
    return fragment; // 內存中的節點
}
複製代碼

compile

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來專門處理對應的邏輯

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);
    }
}
複製代碼

CompileUtil

我們要實現一個專門用來配合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;
                }
            }
        });
    }
}
複製代碼

來再看看效果^_^

 

 

Watcher實現

觀察者的目的就是給需要變化的那個元素增加一個觀察者,用新值和老值進行比對,如果數據變化就執行對應的方法

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
                }
            })
        })
    }
}
複製代碼

看看最終效果!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章