MVVM框架原理淺談

MVVM基本原理

MVVM(Model-View-ViewModel)本質上就是MVC 的改進版,MVVM 就是將其中的View 的狀態和行爲抽象化,讓我們將視圖 UI 和業務邏輯分開。

數據修改通知ViewModel
通知Model數據修改
View數據修改通知ViewModel
ViewModel數據修改通知View
Model
ViewModel
View

MVVM相比與MVC模式主要是分離了試圖和模型,所有的交互都通過ViewModel進行了數據的通知與交互,從而達到了低耦合的優點,View的修改可獨立於Model的修改,可提高重用性,可在View中重用獨立的視圖邏輯。歸根結底可總結爲數據的雙向綁定的過程,即ViewModel的數據與Model的綁定,ViewModel的數據與View中的數據綁定,從而達到數據低耦合高重用性的過程。

MVVM的原理的基礎

MVVM的基礎模式其實不復雜,通過該原理主要就是需要實現ViewModel層到Model層與View層的交互過程。在Vue的官方示例圖中,給出瞭如下圖示;

在這裏插入圖片描述

從圖示所致,DOM就是視圖,Model就是JS對象,Vue就是作爲ViewModel存在將雙向的數據進行綁定,那如何來實現一個簡單的雙向數據綁定呢?

MVVM的流程梳理

根據js的相關內容來實現的思考,基本流程如下;
在這裏插入圖片描述
大致的思路邏輯如上所示,其中在Model修改數據的時候主要就是通過addEventListener事件來註冊回調函數,ViewModel到View的事件或者View到ViewModel的事件主要就是通過Object.defineProperty屬性來設置值。

假如數據格式如下;

視圖(View)
<input v-model="c" type="text">  

MVVM框架(ViewModel)
將視圖的內容和數據當做參數傳入Mvvm中生成一個實例

數據(Model)
{c: 2}
MVVM實例初始化過程
  1. 首先,根據傳入的Model,調用Object.defineProperty來劫持數據,並設置set和get方法,並註冊回調watch更新視圖函數
  2. 通過傳入的視圖信息編譯初始化一個視圖,並根據傳入的編譯後的視圖添加監聽函數,監聽html中輸入輸入的響應事件,如果觸發則回調設置到Mvvm中的對應的實例值
  3. 如果是通過其他事件修改了Model的值則執行註冊的watch方法,重新生成對應的視圖內容,渲染出新的頁面值,從而完成從數據到視圖的更新

以上,就基本是MVVM框架中從Model層到View層,和View層到Model層的數據的交流的概述流程。

實現簡易的MVVM框架
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        <h1>{{singer}}</h1>                       <!-- 模板渲染 -->
        <h1>{{song.first}}</h1>                   <!--  遞歸嵌套 -->
        <input v-model="singer" type="text">      <!--  雙向綁定 -->
    </div>
</body>
<script >

    function Mvvm(options = {}) {
        // vm.$options Vue上是將所有屬性掛載到上面
        this.$options = options;
        let data = this._data = this.$options.data;

        this.init()                     // 初始化設置熟悉

        // 數據劫持
        observe(data);

        new Compile(options.el, this)   // 編譯內容
    }

    // Dep 訂閱發佈模式實現類
    function Dep() {
        // 一個數組(存放函數的事件池)
        this.subs = [];
    }
    Dep.prototype.addSub = function(sub){
        this.subs.push(sub);
    },

    Dep.prototype.notify = function(val) {
        // 綁定的方法,都有一個update方法
        this.subs.forEach(sub => sub.update(val));
    }

    // 數據劫持
    function observe(data){
        // 如果傳入爲空或者不是object則返回
        if (!data || typeof data !== "object"){
            return
        }

        for (let key in data){
                let dep = new Dep()
                let val = data[key]
                observe(val)      // 嵌套調用
                Object.defineProperty(data, key, {
                    configurable: true,
                    get() {
                        // 通過該函數註冊回調函數到dep
                        if (Dep.target){
                            dep.addSub(Dep.target)
                        }
                        return val;
                    },
                    set(newVal) {
                        if (val === newVal) {
                            return;
                        }
                        // 更新回調視圖
                        val = newVal;
                        dep.notify(newVal)
                    }
                })
            }
    }

    Mvvm.prototype = {
        init() {
            for (let key in this._data){
                Object.defineProperty(this, key, {
                    configurable: true,
                    get() {
                        return this._data[key];
                    },
                    set(newVal) {
                        this._data[key] = newVal;
                    }
                })
            }
        }
    }

    function Compile(el, vm){
        vm.$el = document.querySelector(el);
        // 在el範圍裏將內容都拿到,當然不能一個一個的拿
        let fragment = document.createDocumentFragment();

        while (child = vm.$el.firstChild) {
            fragment.appendChild(child);    // 此時將el中的內容放入內存中
        }
        // 對el裏面的內容進行替換
        function replace(frag) {
            Array.from(frag.childNodes).forEach(node => {
                let txt = node.textContent;
                let reg = /\{\{(.*?)\}\}/g;   // 正則匹配{{}}
                if (node.nodeType === 3 && reg.test(txt)) { // 即是文本節點又有大括號的情況{{}}
                    let arr = RegExp.$1.split('.');   // 匹配到的第一個分組 如: a.b, c
                    let val = vm;
                    arr.forEach(key => {
                        val = val[key];
                    });

                    let w = new Watcher(vm, RegExp.$1, function(newVal) {
                        node.textContent = newVal;   // 當watcher觸發時會自動將內容放進輸入框中
                    });
                    node.textContent = txt.replace(reg, val).trim();
                }

                if (node.nodeType == 1) {
                    // 檢查是否是v-model屬性值
                    let nodeAttr = node.attributes
                    Array.from(nodeAttr).forEach(attr => {
                        let name = attr.name;   // v-model  type
                        let exp = attr.value;   // data key
                        if (name == "v-model"){
                            node.value = vm[exp]
                            let w = new Watcher(vm, exp, function(newVal) {
                                node.value = vm[exp];   // 當watcher觸發時會自動將內容放進輸入框中
                            });
                        }

                        node.addEventListener('input', e => {
                            let newVal = e.target.value;
                            // 而值的改變會調用set,set中又會調用notify,notify中調用watcher的update方法實現了更新
                            vm[exp] = newVal;
                        });
                    })
                }
                if (node.childNodes && node.childNodes.length) {
                    replace(node);
                }
            });
        }

        replace(fragment);  // 替換內容

        vm.$el.appendChild(fragment);   // 再將文檔碎片放入el中

    }


    // 監聽函數
    function Watcher(vm, exp, fn) {
        this.vm = vm
        this.exp = exp
        this.fn = fn;   // 將fn放到實例上
        let arr = exp.split('.');   // 檢查是否是深層次遞歸嵌套
        let data = this.vm._data
        if (arr.length > 1){
            let length = arr.length
            for(i=0;i<length-1;i++){
                data = data[arr[i]]
            }
            // 只註冊最後一個回調函數
            Dep.target = this
            const value = data[arr[length-1]]
        } else {
            Dep.target = this
            const value = data[exp]
        }
        Dep.target = null
    }
    Watcher.prototype.update = function(val){
            this.fn(val)
    };

    let app = new Mvvm({el: "#app",data: {"singer": "singer1", "song": {"first": "contry", "second": "home"}}})

</script>
</html>

該段代碼運行在瀏覽器之後,在輸入框中輸入數據,可看到第一行的數據也會跟着改變,此時打開瀏覽器調試窗口的終端,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5SBlO8AR-1578382678865)(/Users/wuzi/Desktop/blog/mvvm_test.png)]

從終端打印信息可看出,基本都是實現了set get方法,並且會根據數據的改變從而改變視圖。

本段代碼參考了網上一些實現的代碼,並做了修改,代碼的實現思路跟上圖中的流程圖相似。在實現的過程中也借用了閉包的原理,爲每一個屬性值在defineProperty的時候,創建一個訂閱數組dep,然後當數據有更改之後,就通知所有訂閱該dep的所有接受着,這樣確保每次的數據修改都只修改到訂閱的局部的訂閱者。

劫持數據
get
set
編譯文件
編譯到匹配的key
newMvvm
defineProperty
添加watcher中的訂閱者到dep
調用dep中的通知函數去更新視圖
編譯
生成一個watcher實例並調用該屬性的get方法註冊到dep中

主要做的兩件事情就是將數據進行雙向的綁定,具體的實現大家可自行深入學習,本文的示例代碼基於網上實例進行修改,原文來自不好意思!耽誤你的十分鐘,讓MVVM原理還給你

總結

本文主要就是簡單的探究了一下MVVM框架的基本原理,僅僅是作爲在使用vue框架的時候的基礎回顧,原理並不算複雜,主要就是通過數據的雙向綁定從而提高開發效率與代碼複用率。由於本人才疏學淺,如有錯誤請批評指正。

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