MVVM機制淺析

易小星發表於2018-07-20

來公司一年多了,接觸了angular,vue這兩種主流的MVVM框架,之前一直沒有去思考其實現的原理,但框架其實也是用基礎的js方法實現的,所以還是很有必要去了解下其原理的,在這裡簡單的總結一下。

1,幾種實現雙向繫結的方法

實現資料繫結的大致幾種做法:

  • 釋出者-訂閱者模式(backbone.js)
  • 髒值檢查(angular.js)
  • 資料劫持(vue.js)

釋出者-訂閱者模式: 一般通過sub,pub的方式實現資料和檢視的繫結監聽,更新資料方式通常做法是 vm.set('property', value),現在用的較少,這裡不再闡述其原理了,有興趣的同學可以點選這裡

髒值檢查: ng是通過髒檢查的檢測機制來對比資料是否發生了變更,來決定是否更新檢視。那麼髒檢查是怎麼觸發的呢,大致有一下幾種情況:

  • UI事件,如ng-click
  • ajax請求
  • timeout延遲
  • 執行$apply()(非ng事件時需我們手動觸發髒檢查)

這裡我們就髒值檢查舉個小栗子:

// html
<div ng-app="app">
    <div ng-controller="myController">
        <span>{{count}}</span>
        <button ng-click="counter=counter+1">increase</button>
    </div>
</div>


// js
var app = angular.module('app', []);
app.controller('myController', function($scope) {
    $scope.count = 0;
})
複製程式碼

看下這時的效果

MVVM機制淺析
我們發下這時通過ng-click形成的資料更改是實時響應的。來,讓我們換種實現方式:

<div ng-app="app">
    <div ng-controller="CounterCtrl">
        <span>{{data}}</span>
        <button my-click>click</button>
    </div>
</div>

app.controller('CounterCtrl', function($scope) {
    $scope.data = 0;
})
app.directive('myClick', function() {
    return function(scope, element, attr) {
        element.on('click', function() {
            scope.data++;
            console.log(scope.data);
        })
    }
})
複製程式碼

這時的效果

MVVM機制淺析
我們發現雖然data的值在更改,但是view卻並沒有更新,這是因為我沒有用ng事件來觸發UI事件,所以沒有執行髒檢查,這個時候我們可以手動加下$apply試下效果。

資料劫持: vue.js 則是採用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。

2,資料劫持的程式碼實現

這裡我們用資料劫持的方式來實現一個簡易的資料繫結,我們來寫一下:

簡單的寫下html

<div id='app'>
    <h3>姓名</h3>
    <p>{{name}}</p>
    <h3>年齡</h3>
    <p>{{age}}</p>
</div>
複製程式碼

接下來準備我們要展示資料

document.addEventListener('DOMContentLoaded', function() {
    let opt = {
        el:'#app', 
        data:{
            name:'準備中...', 
            age:30
        }
    }
    let vm = new MVVM(opt);
    setTimeout(() => {
        opt.data.name = '小明';
    }, 2000);
})
複製程式碼

好,到這裡準備工作都做好了,接下來是重頭戲,不多說,直接上程式碼:

class MVVM {
    constructor(opt) {
        this.opt = opt;
        this.observe(opt.data);
        let root = document.querySelector(opt.el);
        this.complie(root);
    }
    
    // 繫結觀察者物件
    observe(data) {
        Object.keys(data).forEach(key => {
            // 宣告一個觀察者物件
            let obv = new Observer();
            
            // 記錄data[key]的值,防止set的時候遞迴呼叫報錯
            data['_' + key] = data[key];
            
            Object.defineProperty(data, key, {
                get() {
                    Observer.target && obv.addSubNode(Observer.target);
                    return data['_' + key];
                },
                set(newVal) {
                    obv.update(newVal);
                    data['_' + key] = newVal;
                }
            })
        })
    }
    
    // 初始化頁面,遍歷 DOM,收集每一個key變化時,隨之調整的位置,以觀察者方法存放起來    
    complie(node) {
        [].forEach.call(node.childNodes, child =>{
            if(!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)){
                let key = RegExp.$1.trim()
                child.innerHTML = child.innerHTML.replace(new RegExp('\\{\\{\\s*'+ key +'\\s*\\}\\}', 'gm'),this.opt.data[key]) 
                
                // 記錄當前的DOM節點
                Observer.target = child;
                
                // 呼叫set函式
                this.opt.data[key];
                
                Observer.target = null;
            }
            else if (child.firstElementChild) {
                this.compile(child);
            }
        })
    }
}

// 常規觀察者類
class Observer{
    constructor() {
        this.subNode = []    
    }
    addSubNode(node){
        this.subNode.push(node)
    }
    update(newVal){
        this.subNode.forEach(node=>{
            node.innerHTML = newVal
        })
    }
}

複製程式碼

好了至此就是所有的程式碼,我們可以把opt宣告為全域性變數,這樣我們就可以在控制檯修改相關資料,然後實時觀察修改結果。

具體的實現程式碼參照了這篇文章50行程式碼的MVVM,感受閉包的藝術,當時看到的時候有些地方比較困惑,理解了之後這裡將其貼上來,註釋上我自己的理解,僅供學習參考吧。(感覺data['_' + key] 這種記錄方式不太好,引入了無關的值)

相關文章