天天看點

手寫MVVM架構 之vue雙向資料綁定原理剖析

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title>自定義MVVM架構,這是比較牛逼的v-text,v-model和資料綁定原理介紹</title>
    </head>

    <body>
        <div id="app">
            <p v-text="message"></p>
            <input type="text" v-model="message"/>
            <p>{{message}}</p>
        </div>
        <script src="observer.js"></script>
        <script src="TemepCompiler.js"></script>
        <script src="watcher.js"></script>
        <script src="mvvm.js"></script>
        <script>
            var vm = new MVVM({
                el: "#app", //挂載視圖
                data: {
                    msg:'這是自己寫的mvvm架構',
                    message:'mvvm出來吧'
                } //定義資料
            })
        </script>
    </body>

</html>      
//建立一個MVVM架構類
class MVVM { //類構造器(創造執行個體模闆代碼)
    constructor(options) { //相當于函數的參數 {el:...,data:...}
        //緩存重要屬性
        this.$vm = this;
        this.$el = options.el;
        this.$data = options.data;

        //判斷視圖是否存在
        if(this.$el) {
            //添加屬性觀察對象(實作屬性挾持)
            new Observer(this.$data);
            //建立模闆編譯器,來解析視圖
            this.$compiler = new TemepCompiler(this.$el, this.$vm);
        }
    }
}      
class TemepCompiler { //類構造器(創造執行個體模闆代碼)
    constructor(el, vm) { //相當于函數的參數 (this.$el,this.$vm)
        //緩存重要屬性
        this.vm = vm;
        this.el = this.isElemetNode(el) ? el : document.querySelector(el);
        //判斷視圖是否存在
        if(this.el) {
            //1.把模闆内容放入記憶體(片段)
            var fragment = this.node2fragment(this.el);
            //2.解析模闆
            this.compile(fragment);
            //3.把記憶體的結果傳回頁面
            this.el.appendChild(fragment);
        }
    }
    //工具方法
    isElemetNode(node) {
        return node.nodeType === 1 //1.元素節點 2.屬性節點 3.文本節點
    }
    isTextNode(node) {
        return node.nodeType === 3 //1.元素節點 2.屬性節點 3.文本節點
    }
    toArray(arr) {
        return [].slice.call(arr); //假數組轉為數組;
    }
    isDerective(attrName) {
        return attrName.indexOf("v-") >= 0; //判斷屬性名中是否有v-開頭的屬性
    }
    //核心方法(節省記憶體)把模闆放入記憶體,等待解析
    node2fragment(node) {
        //建立記憶體片段
        var fragment = document.createDocumentFragment(),
            child;
        //模闆内容丢到記憶體
        while(child = node.firstChild) {
            fragment.appendChild(child);
        }
        //傳回
        return fragment;
    }
    compile(parentNode) {
        //擷取子節點
        var childNodes = parentNode.childNodes, //類數組
            compiler = this;
        //周遊每一個節點
        this.toArray(childNodes).forEach((node) => {
            //判斷節點類型
            if(compiler.isElemetNode(node)) {
                //1.屬性節點(解析指令)
                compiler.compileElement(node);

            } else {
                //2.文本節點(解析指令)
                var textReg = /\{\{(.+)\}\}/; //(\轉義)文本表達式驗證規則
                var expr = node.textContent;
                //var expr = node.innerText; //谷歌支援
                //按照規則驗證内容
                if(textReg.test(expr)) {
                    expr = RegExp.$1 //緩存最近一次的正則裡面的值;
                    //調用方法編譯
                    compiler.compileText(node, expr); //如果還有子節點,繼續解析;
                }
            }
        });
    }
    //解析元素節點的指令的方法
    compileElement(node) {
        //擷取目前元素節點的所有屬性
        var arrs = node.attributes;
        self = this;
        //周遊目前元素所有屬性
        this.toArray(arrs).forEach(attr => {
            var attrName = attr.name;
            //判斷屬性是否是指令
            if(self.isDerective(attrName)) {
                var type = attrName.substr(2); //v-text,v-model...
                var expr = attr.value; //指令的值就是表達式
                //找幫手
                CompilerUntils[type](node, self.vm, expr);
            }
        })
    }
    //解析表達式的方法
    compileText(node, expr) {
        CompilerUntils.text(node, this.vm, expr);
    }

}

//解析指令幫手
CompilerUntils = {
    //解析text指令
    text(node, vm, expr) {
        //第一次觀察
        //1.找到更新規則對象的更新方法
        var updaterFn = this.updater["textUpdater"];
        //2.執行方法
        updaterFn && updaterFn(node, vm.$data[expr]) //等價if(updaterFn){updaterFn(node,vm.$data[expr])}

        //第n+1次觀察
        new Watcher(vm, expr, (newVaule) => {
            //觸發訂閱時按之前的規則對節點進行更新;v-model的也一樣;
            updaterFn && updaterFn(node, newVaule);
        })
    },
    //解析model指令
    model(node, vm, expr) {
        //1.找到更新規則對象的更新方法
        var updaterFn = this.updater["modelUpdater"];
        //2.執行方法
        updaterFn && updaterFn(node, vm.$data[expr]) //等價if(updaterFn){updaterFn(node,vm.$data[expr])}

        //第n+1次觀察
        new Watcher(vm, expr, (newVaule) => {
            //觸發訂閱時按之前的規則對節點進行更新;v-model的也一樣;
            updaterFn && updaterFn(node, newVaule);
        })
        //視圖到模型變化
        node.addEventListener("input", (e) => {
            var newValue = e.target.value;
            //把值放進資料
            console.log(newValue+'新值');
            vm.$data[expr] = newValue;
        })
    },
    updater: {
        //文本更新方法
        textUpdater(node, value) {
            node.textContent;
            //node.innerText;  //谷歌支援
        },
        //輸入框值更新方法
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}      
class Observer {
    //構造函數
    constructor(data) {
        //提供一個解析方法,完成屬性的分析,和挾持
        this.observe(data);
    }
    observe(data) {
        //判斷資料的有效性 必須是對象
        if(!data || typeof data !== "object") {
            return;
        }
        var keys = Object.keys(data); //拿到所有的屬性(key)轉為數組
        keys.forEach((key) => {
            //重新定義key
            this.defineReactive(data, key, data[key]); //動态屬性需要中括号不能.
        })

    }
    //針對目前對象屬性的重新定義(挾持)
    defineReactive(obj, key, val) {
        var dep = new Dep();
        //重新定義 
        Object.defineProperty(obj, key, {
            enumerable: true, //是否可以周遊
            configurable: false, //是否可以重新配置
            get() { //getter取值
                Dep.target && dep.addSub(Dep.target); //拿到訂閱者
                //傳回屬性
                return val;
            },
            set(newValue) { //setter修改值
                val = newValue; //新值覆寫舊值
                dep.notify(); //通知,觸發update操作;
            }

        })

    }
}

//建立釋出者
//1.管理訂閱者
//2.通知
class Dep {
    constructor() {
        this.subs = [];
    }
    //添加訂閱
    addSub(sub) { //其實就是Watcher執行個體
        this.subs.push(sub);
    }
    //集體通知
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}      
//定義一個訂閱者
class Watcher {
    //構造函數
    //1.需要使用訂閱功能的節點
    //2.全局vm對象,用于擷取資料
    //3.需要使用訂閱功能的節點
    constructor(vm, expr, cb) {
        //緩存重要屬性
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;

        //緩存目前值
        this.value = this.get();
    }

    //擷取目前值
    get() {
        //把目前訂閱者添加到全局
        Dep.target = this; //watcher執行個體
        //擷取目前值
        var value = this.vm.$data[this.expr];
        //清空全局
        Dep.target = null;
        //傳回
        return value;
    }
    //提供一個更新
    update() {
        //擷取新值
        var newValue = this.vm.$data[this.expr];
        //擷取老值
        var oldValue = this.value;
        //執行回調
        if(newValue !== oldValue) {
            this.cb(newValue); //效果一樣關鍵需不需要改變this指向this.cb.call(this.vm,newValue)
        }
    }

}