基於vue實現一個簡單的MVVM框架

来源:https://www.cnblogs.com/yuliangbin/archive/2018/08/04/9302721.html
-Advertisement-
Play Games

不知不覺接觸前端的時間已經過去半年了,越來越發覺對知識的學習不應該只停留在會用的層面,這在我學jQuery的一段時間後便有這樣的體會。 雖然jQuery只是一個JS的代碼庫,只要會一些JS的基本操作學習一兩天就能很快掌握jQuery的基本語法並熟練使用,但是如果不瞭解jQUery庫背後的實現原理,相 ...


不知不覺接觸前端的時間已經過去半年了,越來越發覺對知識的學習不應該只停留在會用的層面,這在我學jQuery的一段時間後便有這樣的體會。

雖然jQuery只是一個JS的代碼庫,只要會一些JS的基本操作學習一兩天就能很快掌握jQuery的基本語法並熟練使用,但是如果不瞭解jQUery庫背後的實現原理,相信只要你一段時間不再使用jQuery的話就會把jQuery忘得一干二凈,這也許就是知其然不知其所以然的後果。

最近在學vue的時候又再一次經歷了這樣的困惑,雖然能夠比較熟練的掌握vue的基本使用,也能夠對MV*模式、數據劫持、雙向數據綁定、數據代理侃上兩句。但是要是稍微深入一點就有點吃力了。所以這幾天痛下決心研究大量技術文章(起初嘗試看早期源碼,無奈vue與jQuery不是一個層級的,相比於jQuery,vue是真正意義上的前端框架。只能無奈棄坑轉而看技術博客),對vue也算有了一個管中窺豹的認識。最後嘗試實踐一下自己學到的知識,基於數據代理、數據劫持、模板解析、雙向綁定實現了一個小型的vue框架。

-------------------------------------------------- 分割線,下麵介紹vue的具體實現。

溫馨提示:文章是按照每個模塊的實現依賴關係來進行分析的,但是在閱讀的時候可以按照vue的執行順序來分析,這樣對初學者更加的友好。推薦的閱讀順序為:實現VMVM、數據代理、實現Observe、實現Complie、實現Watcher。

源碼鏈接,由於只實現了v-model,v-on,v-bind等比較小的功能,所以更便於理解和掌握vue的實現過程。如果對您有幫助的話,希望點一下star。

功能演示如下所示:

 

數據代理

以下麵這個模板為例,要替換的根元素“#mvvm-app”內只有一個文本節點#text,#text的內容為{{name}}。我們就以下麵這個模板詳細瞭解一下VUE框架的大體實現流程

<body>
    <div id="mvvm-app">
        {{name}}
    </div>
    <script src="./js/observer.js"></script>
    <script src="./js/watcher.js"></script>
    <script src="./js/compile.js"></script>
    <script src="./js/mvvm.js"></script>
    <script>
        let vm = new MVVM({
            el: "#mvvm-app",
            data: {
                name: "hello world"
            },        
        })

    </script>
</body>

數據代理

1、什麼是數據代理

在vue裡面,我們將數據寫在data對象中。但是我們在訪問data里的數據時,既可以通過vm.data.name訪問,也可以通過vm.name訪問。這就是數據代理:在一個對象中,可以動態的訪問和設置另一個對象的屬性。

2、實現原理

我們知道靜態綁定(如vm.name = vm.data.name)可以一次性的將結果賦給變數,而使用Object.defineProperty()方法來綁定則可以通過set和get函數實現賦值的中間過程,從而實現數據的動態綁定。具體實現如下:

let obj = {};
let obj1 = {
    name: 'xiaoyu',
    age: 18,
}
//實現origin對象代理target對象
function proxyData(origin,target){
    Object.keys(target).forEach(function(key){
        Object.defineProperty(origin,key,{//定義origin對象的key屬性
            enumerable: false,
            configurable: true,
            get: function getter(){
                return target[key];//origin[key] = target[key];
            },
            set: function setter(newValue){
                target[key] = newValue;
            }
        })
    })
}

vue中的數據代理也是通過這種方式來實現的。

function MVVM(options) {
    this.$options = options || {};
    var data = this._data = this.$options.data;
    var _this = this;//當前實例vm

    // 數據代理
    // 實現 vm._data.xxx -> vm.xxx 
    Object.keys(data).forEach(function(key) {
        _this._proxyData(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this);

}

MVVM.prototype = {
_proxyData: function(key) {
    var _this = this;
    if (typeof key == 'object' && !(key instanceof Array)){//這裡只實現了對對象的監聽,沒有實現數組的
        this._proxyData(key);
    }
    Object.defineProperty(_this, key, {
        configurable: false,
        enumerable: true,
        get: function proxyGetter() {
            return _this._data[key];
        },
        set: function proxySetter(newVal) {
            _this._data[key] = newVal;
        }
    });
},
};

實現Observe

1、雙向數據綁定

  • 數據變動  --->  視圖更新
  • 視圖更新  --->  數據變動 

要想實現當數據變動時視圖更新,首先要做的就是如何知道數據變動了,可以通過Object.defineProperty()函數監聽data對象里的數據,當數據變動了就會觸發set()方法。所以我們需要實現一個數據監聽器Observe,來對數據對象中的所有屬性進行監聽,當某一屬性數據發生變化時,拿到最新的數據通知綁定了該屬性的訂閱器,訂閱器再執行相應的數據更新回調函數,從而實現視圖的刷新。

當設置this.name = 'hello vue'時,就會執行set函數,通知訂閱器里的訂閱者執行相應的回調函數,實現數據變動,對應視圖更新。

function observe(data){
    if (typeof data != 'object') {
        return ;
    }
    return new Observe(data);
}

function Observe(data){
    this.data = data;
    this.walk(data);
}

Observe.prototype = {
    walk: function(data){
        let _this  = this;
        for (key in data) {
            if (data.hasOwnProperty(key)){
                let value = data[key];
                if (typeof value == 'object'){
                    observe(value);
                }
                _this.defineReactive(data,key,data[key]);
            }
        }
    },
    defineReactive: function(data,key,value){
        Object.defineProperty(data,key,{
            enumerable: true,//可枚舉
            configurable: false,//不能再define
            get: function(){
                console.log('你訪問了' + key);return value;
            },
            set: function(newValue){
                console.log('你設置了' + key);
                if (newValue == value) return;
                value = newValue;
                observe(newValue);//監聽新設置的值
            }
        })
    }
}

2、實現一個訂閱器

要想通知訂閱者,首先得要有一個訂閱器(統一管理所有的訂閱者)。為了方便管理,我們會為每一個data對象的屬性都添加一個訂閱器(new Dep)。

訂閱器里存著的是訂閱者Watcher(後面會講到),由於訂閱者可能會有多個,我們需要建立一個數組來維護。一旦數據變化,就會觸發訂閱器的notify()方法,訂閱者就會調用自身的update方法實現視圖更新。

function Dep(){
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub){this.subs.push(sub);
    },
    notify: function(){
        this.subs.forEach(function(sub) {
            sub.update();
        })
    }
}

每次響應屬性的set()函數調用的時候,都會觸發訂閱器,所以代碼補充完整。

Observe.prototype = {
    //省略的代碼未作更改
    defineReactive: function(data,key,value){
        let dep = new Dep();//創建一個訂閱器,會被閉包在key屬性的get/set函數內,因此每個屬性對應唯一一個訂閱器dep實例
        Object.defineProperty(data,key,{
            enumerable: true,//可枚舉
            configurable: false,//不能再define
            get: function(){
                console.log('你訪問了' + key);
                return value;
            },
            set: function(newValue){
                console.log('你設置了' + key);
                if (newValue == value) return;
                value = newValue;
                observe(newValue);//監聽新設置的值
                dep.notify();//通知所有的訂閱者
            }
        })
    }
}

實現Complie

compile主要做的事情是解析模板指令,將模板中的data屬性替換成data屬性對應的值(比如將{{name}}替換成data.name值),然後初始化渲染頁面視圖,並且為每個data屬性添加一個監聽數據的訂閱者(new Watcher),一旦數據有變動,收到通知,更新視圖。

遍歷解析需要替換的根元素el下的HTML標簽必然會涉及到多次的DOM節點操作,因此不可避免的會引發頁面的重排或重繪,為了提高性能和效率,我們把根元素el下的所有節點轉換為文檔碎片fragment進行解析編譯操作,解析完成,再將fragment添加回原來的真實dom節點中。

  • 註:文檔碎片本身也是一個節點,但是當將該節點append進頁面時,該節點標簽作為根節點不會顯示html文檔中,其裡面的子節點則可以完全顯示。

Compile解析模板,將模板內的子元素#text添加進文檔碎片節點fragment。

function Compile(el,vm){
    this.$vm = vm;//vm為當前實例
    this.$el = document.querySelector(el);//獲得要解析的根元素  
    if (this.$el){
        this.$fragment = this.nodeToFragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }  
}
Compile.prototype = {
    nodeToFragment: function(el){
        let fragment = document.createDocumentFragment();
        let child;
        while (child = el.firstChild){
            fragment.appendChild(child);//append相當於剪切的功能
        }
        return fragment;
        
    },
};

compileElement方法將遍歷所有節點及其子節點,進行掃描解析編譯,調用對應的指令渲染函數進行數據渲染,並調用對應的指令更新函數進行綁定,詳看代碼及註釋說明:

因為我們的模板只含有一個文本節點#text,因此compileElement方法執行後會進入_this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'

Compile.prototype = {
    nodeToFragment: function(el){
        let fragment = document.createDocumentFragment();
        let child;
        while (child = el.firstChild){
            fragment.appendChild(child);//append相當於剪切的功能
        }
        return fragment;
        
    },
    
    init: function(){
        this.compileElement(this.$fragment);
    },
    
    compileElement: function(node){
        let childNodes = node.childNodes;
        const _this = this;
        let reg = /\{\{(.*)\}\}/g;
        [].slice.call(childNodes).forEach(function(node){
            
            if (_this.isElementNode(node)){//如果為元素節點,則進行相應操作
                _this.compile(node);
            } else if (_this.isTextNode(node) && reg.test(node.textContent)){
                //如果為文本節點,並且包含data屬性(如{{name}}),則進行相應操作
                _this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'
            }
            
            if (node.childNodes && node.childNodes.length){
                //如果節點內還有子節點,則遞歸繼續解析節點
                _this.compileElement(node);
                
            }
        })
    },
    compileText: function(node,exp){//#text,'name'
            compileUtil.text(node,this.$vm,exp);//#text,vm,'name'
    },
};

CompileText()函數實現初始化渲染頁面視圖(將data.name的值通過#text.textContent = data.name顯示在頁面上),並且為每個DOM節點添加一個監聽數據的訂閱者(這裡是為#text節點新增一個Wather)。

let updater = {
    textUpdater: function(node,value){   
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
}
    
let compileUtil = {
    text: function(node,vm,exp){//#text,vm,'name'
        this.bind(node,vm,exp,'text');
    },
    
    bind: function(node,vm,exp,dir){//#text,vm,'name','text'
        let updaterFn = updater[dir + 'Updater'];
        updaterFn && updaterFn(node,this._getVMVal(vm,exp));
        new Watcher(vm,exp,function(value){
            updaterFn && updaterFn(node,value)
        });
        console.log('加進去了');
    }
};

現在我們完成了一個能實現文本節點解析的Compile()函數,接下來我們實現一個Watcher()函數。

實現Watcher

 我們前面講過,Observe()函數實現data對象的屬性劫持,併在屬性值改變時觸發訂閱器的notify()通知訂閱者Watcher,訂閱者就會調用自身的update方法實現視圖更新。

Compile()函數負責解析模板,初始化頁面,並且為每個data屬性新增一個監聽數據的訂閱者(new Watcher)。

Watcher訂閱者作為Observer和Compile之間通信的橋梁,所以我們可以大致知道Watcher的作用是什麼。

主要做的事情是:

  • 在自身實例化時往訂閱器(dep)裡面添加自己。
  • 自身必須有一個update()方法 。
  • 待屬性變動dep.notice()通知時,能調用自身的update()方法,並觸發Compile中綁定的回調。

 先給出全部代碼,再分析具體的功能。

//Watcher
function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.cb = cb;
    this.exp = exp;
    this.value = this.get();//初始化時將自己添加進訂閱器
};

Watcher.prototype = {
    update: function(){
        this.run();
    },
    run: function(){
        const value = this.vm[this.exp];
        //console.log('me:'+value);
        if (value != this.value){
            this.value = value;
            this.cb.call(this.vm,value);
        }
    },
    get: function() { 
        Dep.target = this;  // 緩存自己
        var value = this.vm[this.exp]  // 訪問自己,執行defineProperty里的get函數         
        Dep.target = null;  // 釋放自己
        return value;
    }
}

//這裡列出Observe和Dep,方便理解
Observe.prototype = {
    defineReactive: function(data,key,value){
        let dep = new Dep();
        Object.defineProperty(data,key,{
            enumerable: true,//可枚舉
            configurable: false,//不能再define
            get: function(){
                console.log('你訪問了' + key);
                //說明這是實例化Watcher時引起的,則添加進訂閱器
                if (Dep.target){
                    //console.log('訪問了Dep.target');
                    dep.addSub(Dep.target);
                }
                return value;
            },
        })
    }
}

Dep.prototype = {
    addSub: function(sub){this.subs.push(sub);
    },
}

我們知道在Observe()函數執行時,我們為每個屬性都添加了一個訂閱器dep,而這個dep被閉包在屬性的get/set函數內。所以,我們可以在實例化Watcher時調用this.get()函數訪問data.name屬性,這會觸發defineProperty()函數內的get函數,get方法執行的時候,就會在屬性的訂閱器dep添加當前watcher實例,從而在屬性值有變化的時候,watcher實例就能收到更新通知。

那麼Watcher()函數中的get()函數內Dep.taeger = this又有什麼特殊的含義呢?我們希望的是在實例化Watcher時將相應的Watcher實例添加一次進dep訂閱器即可,而不希望在以後每次訪問data.name屬性時都加入一次dep訂閱器。所以我們在實例化執行this.get()函數時用Dep.target = this來標識當前Watcher實例,當添加進dep訂閱器後設置Dep.target=null。

實現VMVM

MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果。

function MVVM(options) {
    this.$options = options || {};
    var data = this._data = this.$options.data;
    var _this = this;
    // 數據代理
    // 實現 vm._data.xxx -> vm.xxx 
    Object.keys(data).forEach(function(key) {
        _this._proxyData(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this);
}

 學習鏈接

以下是vue的分析文章,對我理解vue起到很大的幫助。感謝作者對自己知識的分享。

vue 源碼分析之如何實現 observer 和 watcher

剖析vue實現原理,自己動手實現mvvm

對vue早期源碼的理解

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • BFC的概念以及作用 BFC的定義: 直譯為 。它是一個獨立的渲染區域,只有Block level box參與, 它規定了內部的Block level Box如何佈局,並且與這個區域外部毫不相干。 我們常說的文檔流其實分為==定位流、浮動流和普通流==三種。而 其實就是指BFC中的FC。 是`for ...
  • 代碼可以在 https://pan.baidu.com/s/1uN120-18hvAzELpJCQfbXA 處下載 下麵來 移除 廣告元素,在js目錄下,創建一個 removeAds.js, 用來移除網頁中的廣告元素 修改manifest.json 同時, 發現了一個問題,如果打開的是 https: ...
  • 理解好javascript的變數作用域和鏈式調用機制對用好變數起著關鍵的作用,本文來談談這兩個概念。 ...
  • 關於行內元素(補充一點) 行內元素只能容納文本或其他行內元素。(a特殊a裡面可以放塊級元素) 例子: 關於行高tip: 選擇器的嵌套層級不應大於3級,位置靠後的限定條件應儘可能的精確。 屬性定義必須另起一行。 關於行高的測量: css的三大特性(層疊 優先 繼承) a、層疊性:多種css樣式的疊加 ...
  • 使用html5視頻背景 直到現在,仍然不存在一項旨在網頁上顯示視頻的標準。今天,大多數視頻是通過插件(比如 Flash)來顯示的。然而,並非所有瀏覽器都擁有同樣的插件。HTML5 規定了一種通過 video 元素來包含視頻的標準方法。 瀏覽器支持的視頻格式 當前,video 元素支持Ogg,MPEG ...
  • Require.js與Sea.js的區別 相同之處 和 都是模塊載入器,倡導模塊化開發理念,核心價值是讓 JavaScript 的模塊化開發變得簡單自然。 不同之處 兩者的主要區別如下: RequireJS 想成為瀏覽器端的模塊載入器,同時也想成為 Rhino / Node 等環境的模塊載入器。Se ...
  • 場景: 假如有一天,你的在寫一個前端項目,是關於一份點餐商家電話信息表,你啪塔啪塔地寫完了,突然間項目經理跑過來找你,要求你在每一個商家的電話號碼前都添加一個電話符號,來使得電話號碼更直觀和頁面更美觀。這個時候你就糾結了,這不是折磨人嗎?這不是要我在每個電話號碼前都添加一個<img>標簽?這要整到猴 ...
  • [TOC] 前後端如何通信 前段:客戶端 後端: 伺服器端 所謂的全棧,其實是你可以實現客戶端和伺服器端程式的編寫,而且可以實現倆端之間的通信 客戶端和伺服器端是如何通信的? 本地開發(當前項目可以在本地預覽) 部署到伺服器上,讓別人可以通過功能變數名稱或者外網訪問 購買一臺伺服器(阿裡雲獨立主機,虛擬服務 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...