手把手教你Vue從零擼一個迷你版MVVM框架

這段時間 在工作之餘的休息時間,學習瞭解Vue ,學習Vue的設計思想,通過Vue官網學習Vue的語法,通過Vue前端技術,搭建構建了一個簡單的項目,在項目學習完之後,發現Vue是一個很有意思的前端技術,沒事就看了源碼,第一次看的時候不知道如何下手,就開始在百度,谷歌,兩大搜索神器的幫助下,找到了學習的快捷之路。發現了難啃的骨頭,纔是最有意思的事。

Vue框架到底爲我們做了什麼?

  • 數據和視圖分離,解耦(開放封閉原則)
    1.所有數據和視圖不分離的,都會命中開放封閉原則
    2.Vue 數據獨立在 data 裏面,視圖在 template 中
  • 以數據驅動視圖,只關心數據變化,dom 操作被封裝
    1. 使用原生js是直接通過操作dom來修改視圖,例如
    ducument.getElementById('xx').innerHTML="xxx"
    
    1. 以數據驅動視圖就是,我們只管修改數據,視圖的部分由框架去幫我們修改,符合開放封閉模式.

如何理解 MVVM ?

  • MVC
    1. Model 數據 → View 視圖 → Controller 控制器
  • MVVM
    1. MVVM不算是一種創新
    2. 但是其中的 ViewModel 是一種創新
    3. ViewModel 是真正結合前端應用場景的實現
  • 如何理解MVVM
    1. MVVM - Model View ViewModel,數據,視圖,視圖模型
    2. 三者與 Vue 的對應:view 對應 template,vm 對應 new Vue({…}),model 對應 data
    3. 三者的關係:view 可以通過事件綁定的方式影響 model,model 可以通過數據綁定的形式影響到view,viewModel是把 model 和 view 連起來的連接器
我們來看一下最後我們要達到的效果

vue.gif

我們先來看下我們簡單的結構

mini-MVVM結構
Vue項目源碼

以上就是我們簡單搭建的項目目錄結構,有需要了解可以去閱讀源碼來了解更多,這裏就不做過多的展開

我們先來看下我們index.html完整代碼

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>MiniMVVM 框架demo演示</title>
		<link rel="stylesheet" href="./css/mini-mvvm.css"/>	
	</head>
	<body>
		<div id="app">
			<h1>{{title}}</h1>
			<div>
				<span>文本輸入框:</span>
				<input type="text" v-model="message"/>
			</div>
			<section>
				<samp>數據顯示結果:</samp>
				<hr/>
				<span>{{message}}</span>
			</section>
		</div>

	<script type="text/javascript" src="js/mini-mvvm.js"></script>
	<script>
		var app=new MiniMVVM({
			el:'#app',
			data:{
				title:'Mini-MVVM demo演示',
				message:'Hello World ! ! !'
			}
		});
	</script>

	</body>
</html>

大家看到這裏有沒有發現,我們調用不是Vue 而是我們自己自定義的MiniMVVM,沒錯。那我們再來看看Vue的用法,幫大家回顧一下

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
Hello Vue!

我們已經成功創建了第一個 Vue 應用!看起來這跟渲染一個字符串模板非常類似,但是 Vue 在背後做了大量工作。現在數據和 DOM 已經被建立了關聯,所有東西都是響應式的。我們要怎麼確認呢?打開你的瀏覽器的 JavaScript 控制檯 (就在這個頁面打開),並修改 app.message 的值,你將看到上例相應地更新。
接下來是我們的函數類

(function () {
    //初始化類
    class MiniMVVM {...}
    //模板編譯類
    class TemplateCompile {...}

    //數據劫持類,響應式
    class Observer {...}

    //觀察者
    class Watcher {...}

    //發佈訂閱類 自定義事件
    class Emitter {...}

    //存儲指令和工具方法
    let CompileTool = {...};
    window.MiniMVVM = MiniMVVM;
})();

這是我們mini版的函數類,這裏我們只實現了簡單的雙向數據綁定,目的是爲了讓大家學習思想,以及理解分析


  function initMixin (Vue) {...}

  function initInternalComponent (vm, options) {...}

  function resolveConstructorOptions (Ctor) {...}

  function resolveModifiedOptions (Ctor) {...}

  function Vue (options) {
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }

  initMixin(Vue);
  stateMixin(Vue);
  eventsMixin(Vue);
  lifecycleMixin(Vue);
  renderMixin(Vue);

  /*  */

  function initUse (Vue) {...}

  /*  */

  function initMixin$1 (Vue) {...}

  /*  */

  function initExtend (Vue) {

如果讓我們光來看Vue.js 就是一萬兩千行代碼,我相信大多數和我一樣,看到這麼多行代碼人已經崩潰了直接就選擇了放棄,我們雖然是mini版的,我們也藉助vue設計來寫我們mini版。
下面我們就來簡單的分析下我們這個函數的作用以及用法

//初始化
class MiniMVVM {
        constructor(options) {
            this.$el = options.el;
            this.$data = options.data;
            //判斷是否有模板,如果有模板的話,就執行編譯和數據劫持
            if (this.$el) {
                //1,數據劫持
                this.$data = (new Observer()).observe(this.$data);
                //2,模板編譯
                new TemplateCompile(this.$el, this);
            }
        }
    }

這裏我們主要是爲了獲取我們模板信息以及數據
下一步我們來看一下我們數據劫持監聽

 //數據劫持類,響應式
    class Observer {
        constructor(data) {
            this.data = data;
            this.observe(this.data);
        }

        observe(data) {
            //驗證是否存在,是否是對象
            if (!data || typeof data !== 'object') {
                return;
            }
            //使用proxy 代理攔截監聽
            let that = this;
            //初始化發佈訂閱
            let emitter = new Emitter();
            let handler = {
                get(target, key) {
                    //遞歸當前target[key] 是否是對象
                    if (typeof target[key] === 'object' && target[key] !== null) {
                        return new Proxy(target[key], handler);
                    }
                    //判斷當前key下面的watcher是否存在
                    if (Emitter.watcher) {
                        emitter.addSub(Emitter.watcher);
                    }
                    return target[key];
                },
                set(target, key, newVal) {
                    if (target[key] !== newVal) {
                        target[key] = newVal;
                        emitter.notify();
                    }
                }
            }
            //創建proxy代理
            let pdata = new Proxy(data, handler);
            return pdata;
        }
    }

Proxy語法和用例

let p = new Proxy(target, handler);

將目標和處理程序傳遞給Proxy構造函數,這樣就創建了一個proxy對象。

//模板編譯類
    class TemplateCompile {
        constructor(el, vm) {
            //1,獲取元素
            this.el = (el.nodeType === 1 ? el : document.querySelector(el));
            //2,獲取vm實例
            this.vm = vm;
            //如果元素存在,然後我們進行編譯
            if (this.el) {
                //獲取模板
                let fragment = this.nodeToFragment(this.el);
                //模板語法解析
                this.cpmpile(fragment);
                //把最後生成的文檔結構重新append到我們頁面中去
                this.el.appendChild(fragment);
            }

        }
        //獲取模板
        nodeToFragment(el) {
            let fragment = document.createDocumentFragment();
            let firstChild = null;

            while (firstChild = el.firstChild) {
                fragment.appendChild(firstChild);
            }
            return fragment;
        }
        //解析文檔碎片,編譯模板中的變量
        cpmpile(fragment) {
            //1,獲取所有的子節點(包括元素解點)
            let childNodes = fragment.childNodes;
            //2,遍歷循環每個節點
            Array.from(childNodes).forEach(node => {
                //如果是元素節點
                if (node.nodeType === 1) {
                    //遞歸遍歷子節點
                    this.cpmpile(node);
                    //處理我們的元素節點
                    this.complieElement(node);
                } else {
                    //處理我們的文本節點
                    this.complieText(node);
                }
            })
        }

        //編譯處理
        complieText(nodeText) {
            //獲取文本節點的內容
            let exp = nodeText.textContent;
            //這裏我們通過正則獲取{{}}裏面的數據
            let re = /{{[^}]+}}/g;
            if (re.test(exp)) {
                //渲染頁面數據
                CompileTool.text(nodeText, this.vm, exp)
            }

        }
        //編譯處理元素節點
        complieElement(node) {
            console.log(node);
            //取出所有的屬性
            let attrs = node.attributes;
            //邊裏屬性 檢測是否具備‘-v’
            Array.from(attrs).forEach(attr => {
                console.log(attr.name);
                if (attr.name.includes('v-')) {
                    //如果是v-attr 這樣的指令 我們去我們對應的尋找屬性值字符串所對應的值
                    let exp = attr.value;
                    let type = exp.split('-')[1];
                    //調用model指令對應的方法,渲染頁面數據
                    CompileTool.model(node, this.vm, exp);
                }
            })

        }
    }

這是我們模板類代碼,這裏主要是解析我們定義好的模板語法,下面我們再來看下我們觀察者類

//觀察者
    class Watcher {
        constructor(vm, exp, callback) {
            this.vm = vm;
            this.exp = exp;
            this.callback = callback;
            //獲取當前的值
            this.oldValue = this.get();
        }
        get() {
            //初始化發佈訂閱狀態值
            Emitter.watcher = this;
            //獲取這個data上面的值
            let val = CompileTool.getVal(this.vm, this.exp)
            //清空一下發布狀態置
            Emitter.watcher = null;
            //返回當前值
            return val;
        }
        update() {
            //獲取當前值
            let newVal = CompileTool.getVal(this.vm, this.exp)
            //拿到舊的值
            let oldVal = this.oldValue;
            //比較兩次的值,是否保持一致,如果不一致就執行回調
            if (newVal != oldVal) {
                this.callback(newVal);
            }
        }
    }

觀察者見名思義,看過或者學習過設計模式的都應該知道觀察者模式,這裏就不做過多的解釋,有興趣的朋友可以百度一下。
剩下的就是我們的發佈訂閱

//發佈訂閱類 自定義事件
    class Emitter {
        constructor() {
            this.subs = [];
        }
        //添加一個訂閱
        addSub(watcher) {
            this.subs.push(watcher);
        }
        //通知執行訂閱
        notify() {
            this.subs.forEach(v => v.update());
        }
    }

通過以上不到300的行的代碼,我們就實現了簡單的雙向數據綁定
有朋友會說你現在沒有其他的文件以及配置,是的,不需要我們也是能實現數據的雙向綁定,這裏的剩下的webpack打包,webpack打包這個算是比較簡單了,對於學習前端vue來說,那是再熟悉不過的了,這裏就不過多的介紹了,下一章節我們在繼續探討webpack打包腳本詳細介紹。

下面提供幾個學習es地址大家可以去看一下,中英文都有。

ES5地址

中文版:中文地址
英文版:英文地址

ES6地址

中文版:中文地址
英文版:英文地址
ES6的瀏覽器兼容性問題:地址

非常感謝您能抽出時間來閱讀,如果對你有所幫助,歡迎轉發分享,您的轉發分享就是對我最大的鼓勵和支持。

有興趣的朋友歡迎加入交流!!!
微信號

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