vue的數據驅動原理及簡單實現

一、目標實現

  1. 理解雙向數據綁定原理;
  2. 實現{{}}、v-model和基本事件指令v-bind(:)、v-on(@);
  3. 新增屬性的雙向綁定處理;

PS:實例源碼https://github.com/wuwhs/imit...,歡迎隨手給個start,就此謝過!javascript

二、雙向數據綁定原理

vue實現對數據的雙向綁定,經過對數據劫持結合發佈者-訂閱者模式實現的。html

2.1 Object.defineProperty

vue經過Object.defineProperty來實現數據劫持,會對數據對象每一個屬性添加對應的get和set方法,對數據進行讀取和賦值操做就分別調用get和set方法。vue

Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {

        // do something

        return val;
    },
    set: function(newVal) {

        // do something
    }
});

咱們能夠將一些方法放到裏面,從而完成對數據的監聽(劫持)和視圖的同步更新。java

definePropertypng

2.2 過程說明

實現雙向數據綁定,首先要對數據進行數據監聽,須要一個監聽器Observer,監聽全部屬性。若是屬性發生變化,會調用setter和getter,再去告訴訂閱者Watcher是否須要更新。因爲訂閱者有不少個,咱們須要一個消息訂閱器Dep來專門收集這些訂閱者,而後在監聽器Observer和訂閱者Watcher之間進行統一管理。還有,咱們須要一個指令解析器Complie,對每一個元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher,並替換模板數據或綁定相應函數。當訂閱者Watcher接收到相應屬性的變化,就會執行對應的更新函數,從而更新視圖。node

intruduce

三、實現Observer

Observer是一個數據監聽器,核心方法是咱們提到過的Object.defineProperty。若是要監聽全部屬性的話,則須要經過遞歸遍歷,對每一個子屬性都defineProperty。react

/**
 * 監聽器構造函數
 * @param {Object} data 被監聽數據
 */
function Observer(data) {

    if (!data || typeof data !== "object") {
        return;
    }

    this.data = data;
    this.walk(data);

}

Observer.prototype = {
    /**
     * 屬性遍歷
     */
    walk: function(data) {
        var self = this;
        Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
        });
    },

    /**
     * 監聽函數
     */
    defineReactive: function(data, key, val) {

        observe(val);

        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }

                val = newVal;

                console.log("屬性:" + key + "被監聽了,如今值爲:" + newVal);

                updateView(newVal);
            }
        });

        updateView(val);
    }
}

/**
 * 監聽器
 * @param {Object} data 被監聽對象
 */
function observe(data) {

    return new Observer(data);
}

/**
 * vue構造函數
 * @param {Object} options 全部入參
 */
function MyVue(options) {

    this.vm = this;

    this.data = options.data;

    // 監聽數據
    observe(this.data);

    return this;
}

/**
 * 更新視圖
 * @param {*} val
 */
function updateView(val) {
    var $name = document.querySelector("#name");
    $name.innerHTML = val;
}

var myvm = new MyVue({
    el: "#demo",
    data: {
        name: "hello word"
    }
});

Observer

四、實現Dep

在流程介紹中,咱們須要建立一個能夠訂閱者的訂閱器Dep,主要負責手機訂閱者,屬性變化的時候執行相應的訂閱者,更新函數。下面稍加改造Observer,就能夠插入咱們的訂閱器。git

Observer.prototype = {
    // ...

    /**
     * 監聽函數
     */
    defineReactive: function(data, key, val) {
        var dep = new Dep();

        observe(val);

        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {

                // 判斷是否須要添加訂閱者 何時添加訂閱者呢? 與實際頁面DOM有關聯的data屬性才添加相應的訂閱者
                if (Dep.target) {
                    // 添加一個訂閱者
                    dep.addSub(Dep.target);
                }

                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }

                val = newVal;

                console.log("屬性:" + key + "被監聽了,如今值爲:" + newVal);

                // 通知全部訂閱者
                dep.notify(newVal);
            }
        });

        updateView(val);

        // 訂閱器標識自己實例
        Dep.target = dep;
        // 強行執行getter,往訂閱器中添加訂閱者
        var v = data[key];
        // 釋放本身
        Dep.target = null;
    }
}

/**
 * 監聽器
 * @param {Object} data 被監聽對象
 */
function observe(data) {

    return new Observer(data);
}

/**
 * 訂閱器
 */
function Dep() {
    this.subs = [];
    this.target = null;
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
        console.log("this.subs:", this.subs);
    },
    notify: function(data) {
        this.subs.forEach(function(sub) {
            sub.update(data);
        });
    },
    update: function(val) {
        updateView(val)
    }
};

// ...

PS:將訂閱器Dep添加到一個訂閱者設計到getter裏面,是爲了讓Watcher初始化進行觸發。github

五、實現Watcher

訂閱者Watcher在初始化的時候須要將本身添加到訂閱器Dep中,那該如何添加呢?咱們已經知道監聽器Observer是在get函數執行添加了訂閱者Watcher的操做,因此咱們只要在訂閱者Watcher初始化的時候觸發對應的get函數去執行添加訂閱者操做。那麼,怎樣去觸發get函數?很簡單,只需獲取對應的屬性值就能夠觸發了,由於咱們已經用Object.defineProperty監聽了全部屬性。vue在這裏作了個技巧處理,就是咋咱們添加訂閱者的時候,作一個判斷,判斷是不是事先緩存好的Dep.target,在訂閱者添加成功後,把target重置null便可。數組

// ...

/**
 * 訂閱者
 * @param {Object} vm vue對象
 * @param {String} exp 屬性值
 * @param {Function} cb 回調函數
 */
function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    // 將本身添加到訂閱器
    this.value = this.get();
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;

        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        // 緩存本身 作個標記
        Dep.target = this;

        // 強制執行監聽器裏的get函數
        // this.vm.data[this.exp] 調用getter,添加一個訂閱者sub,存入到全局變量subs
        var value = this.vm.data[this.exp];

        // 釋放本身
        Dep.target = null;

        return value;
    }
};

/**
 * vue構造函數
 * @param {Object} options 全部入參
 */
function MyVue(options) {

    this.vm = this;

    this.data = options.data;

    observe(this.data);

    var $name = document.querySelector("#name");

    // 給name屬性添加一個訂閱者到訂閱器中,當屬性發生變化後,觸發回調
    var w = new Watcher(this, "name", function(val) {
        $name.innerHTML = val;
    });

    return this;
}

到這裏,其實已經實現了咱們的雙向數據綁定:可以根據初始數據初始化頁面特定元素,同時當數據改變也能更新視圖。緩存

五、實現Compile

步驟4整個過程都能有去解析DOM節點,而是直接固定節點進行替換。接下來咱們就來實現一個解析器,完成一些解析和綁定工做。

  1. 獲取頁面的DOM節點,遍歷存入到文檔碎片對象中;
  2. 解析出文本節點,匹配{{}}(暫時只作"{{}}"的解析),用初始化數據替換,並添加相應訂閱者;
  3. 分離出節點的指令v-on、v-bind和v-model,綁定相應的事件和函數;

progress

// ...

/**
 * 編譯器構造函數
 * @param {String} el 根元素
 * @param {Object} vm vue對象
 */
function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    /**
     * 初始
     */
    init: function() {
        if (this.el) {
            console.log("this.el:", this.el);
            // 移除頁面元素生成文檔碎片
            this.fragment = this.nodeToFragment(this.el);
            // 編譯文檔碎片
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log("DOM Selector is not exist");
        }
    },

    /**
     * 頁面DOM節點轉化成文檔碎片
     */
    nodeToFragment: function(el) {
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;

        // 此處添加打印,出來的不是頁面中原始的DOM,而是編譯後的?
        // NodeList是引用關係,在編譯後相應的值被替換了,這裏打印出來的NodeList是後來被引用更新了的
        console.log("el:", el);
        // console.log("el.firstChild:", el.firstChild.nodeValue);
        while (child) {
            // append後,原el上的子節點被刪除了,掛載在文檔碎片上
            fragment.appendChild(child);
            child = el.firstChild;
        }

        return fragment;
    },
    /**
     * 編譯文檔碎片,遍歷到當前是文本節點則去編譯文本節點,若是當前是元素節點,而且存在子節點,則繼續遞歸遍歷
     */
    compileElement: function(fragment) {
        var childNodes = fragment.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {
            // var reg = /\{\{\s*(.+)\s*\}\}/g;
            var reg = /\{\{\s*((?:.|\n)+?)\s*\}\}/g;
            var text = node.textContent;

            if (self.isElementNode(node)) {
                self.compileAttr(node);
            } else if (self.isTextNode(node) && reg.test(text)) {
                reg.lastIndex = 0

                /* var match;
                while(match = reg.exec(text)) {
                    self.compileText(node, match[1]);
                } */

                self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);
            }
        });
    },

    /**
     * 編譯屬性
     */
    compileAttr: function(node) {
        var nodeAttrs = node.attributes;
        var self = this;

        Array.prototype.forEach.call(nodeAttrs, function(attr) {
            var attrName = attr.name;

            // 只對vue自己指令進行操做
            if (self.isDirective(attrName)) {
                var exp = attr.value;

                // v-on指令
                if (self.isOnDirective(attrName)) {
                    self.compileOn(node, self.vm, exp, attrName);
                }
                // v-bind指令
                if(self.isBindDirective(attrName)) {
                    self.compileBind(node, self.vm, exp, attrName);
                }
                // v-model
                else if (self.isModelDirective(attrName)) {
                    self.compileModel(node, self.vm, exp, attrName);
                }

                node.removeAttribute(attrName);
            }
        })
    },

    /**
     * 編譯文檔碎片節點文本,即對標記替換
     */
    compileText: function(node, exp) {
        var self = this;
        var exps = exp.split(".");
        var initText = this.vm.data[exp];

        // 初始化視圖
        this.updateText(node, initText);

        // 添加一個訂閱者到訂閱器
        var w = new Watcher(this.vm, exp, function(val) {
            self.updateText(node, val);
        });
    },

    /**
     * 編譯v-on指令
     */
    compileOn: function(node, vm, exp, attrName) {
        // @xxx v-on:xxx
        var onRE = /^@|^v-on:/;
        var eventType = attrName.replace(onRE, "");

        var cb = vm.methods[exp];

        if (eventType && cb) {
            node.addEventListener(eventType, cb.bind(vm), false);
        }
    },

    /**
     * 編譯v-bind指令
     */
    compileBind: function (node, vm, exp, attrName) {
        // :xxx v-bind:xxx
        var bindRE = /^:|^v-bind:/;
        var attr = attrName.replace(bindRE, "");

        var val = vm.data[exp];

        node.setAttribute(attr, val);
    },

    /**
     * 編譯v-model指令
     */
    compileModel: function(node, vm, exp, attrName) {
        var self = this;
        var val = this.vm.data[exp];

        // 初始化視圖
        this.modelUpdater(node, val);

        // 添加一個訂閱者到訂閱器
        new Watcher(this.vm, exp, function(value) {
            self.modelUpdater(node, value);
        });

        // 綁定input事件
        node.addEventListener("input", function(e) {
            var newVal = e.target.value;
            if (val === newVal) {
                return;
            }
            self.vm.data[exp] = newVal;
            // val = newVal;
        });
    },

    /**
     * 更新文檔碎片相應的文本節點
     */
    updateText: function(node, val) {
        node.textContent = typeof val === "undefined" ? "" : val;
    },

    /**
     * model更新節點
     */
    modelUpdater: function(node, val, oldVal) {
        node.value = typeof val == "undefined" ? "" : val;
    },

    /**
     * 屬性是不是vue指令,包括v-xxx:,:xxx,@xxx
     */
    isDirective: function(attrName) {
        var dirRE = /^v-|^@|^:/;
        return dirRE.test(attrName);
    },

    /**
     * 屬性是不是v-on指令
     */
    isOnDirective: function(attrName) {
        var onRE = /^v-on:|^@/;
        return onRE.test(attrName);
    },

    /**
     * 屬性是不是v-bind指令
     */
    isBindDirective: function (attrName) {
        var bindRE = /^v-bind:|^:/;
        return bindRE.test(attrName);
    },

    /**
     * 屬性是不是v-model指令
     */
    isModelDirective: function(attrName) {
        var mdRE = /^v-model/;
        return mdRE.test(attrName);
    },

    /**
     * 判斷元素節點
     */
    isElementNode: function(node) {
        return node.nodeType == 1;
    },

    /**
     * 判斷文本節點
     */
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
};

/**
 * vue構造函數
 * @param {Object} options 全部入參
 */
function MyVue(options) {

    this.vm = this;

    this.data = options.data;

    this.methods = options.methods;

    observe(this.data);

    new Compile(options.el, this.vm);

    return this;
}

這樣咱們就能夠調用指令v-bind、v-on和v-model。

<head>
    <meta charset="UTF-8">
    <style>
        .red {
            color: red;
        }
    </style>
</head>

<body>
    <div id="demo">
        <h2 v-bind:class="myColor">&#123;&#123; name &#125;&#125;</h2>
        <input type="text" name="" v-model="name">
        <button @click="clickOk">Ok</button>
    </div>
</body>

<script>
var myvm = new MyVue({
    el: "#demo",
    data: {
        name: "hello word",
        myColor: "red"
    },
    methods: {
        clickOk: function() {
            alert("I am OK");
        }
    }
});

setTimeout(function() {
    myvm.data.name = "wawawa...vue was born";
}, 2000);
</script>

imitate5

五、其餘

5.1 proxy代理data

可能注意到了,咱們不論是在賦值仍是取值,都是在myvm.data.someAttr上操做的,而在vue上咱們習慣直接myvm.someAttr這種形式。怎樣實現呢?一樣,咱們能夠用Object.defineProperty對data全部屬性作一個代理,即訪問vue實例屬性時,代理到data上。很簡單,實現以下:

/**
 * 將數據拓展到vue的根,方便讀取和設置
 */
MyVue.prototype.proxy = function(key) {
    var self = this;

    Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get: function proxyGetter() {
            return self.data[key];
        },
        set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

imitate6

5.2 parsePath

上面對於data的操做只是到對於簡單的基本類型屬性,對於對象屬性的改變該怎麼更新到位呢?其實,只要深度遍歷對象屬性路徑,就能夠找到要訪問屬性值。

/**
 * 根據對象屬性路徑,最終獲取值
 * @param {Object} obj 對象
 * @param {String} path 路徑
 * return 值
 */
function parsePath(obj, path) {
    var bailRE = /[^\w.$]/;
    if (bailRE.test(path)) {
        return
    }
    var segments = path.split('.');

    for (var i = 0; i < segments.length; i++) {
        if (!obj) { return }
        obj = obj[segments[i]];
    }
    return obj;
}

用這個方法替換咱們的全部取值操做
vm[exp] => parsePath(vm, exp)

imitate6_1

六、新增屬性的雙向數據綁定

6.1 給對象添加屬性

Vue 不容許在已經建立的實例上動態添加新的根級響應式屬性 (root-level reactive property)。然而它可使用 Vue.set(object, key, value) 方法將響應屬性添加到嵌套的對象上。
也就是咱們須要在Vue原型上添加一個set方法去設置新添加的屬性,新屬性一樣要進行監聽和添加訂閱者。

/**
 * vue的set方法,用於外部新增屬性 Vue.$set(target, key, val)
 * @param {Object} target 數據
 * @param {String} key 屬性
 * @param {*} val 值
 */
function set(target, key, val) {
    if (Array.isArray(target)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val);
        return val;
    }

    if (target.hasOwnProperty(key)) {
        target[key] = val;
        return val
    }
    var ob = (target).$Observer;

    if (!ob) {
        target[key] = val;
        return val
    }

    // 對新增屬性定義監聽
    ob.defineReactive(target, key, val);

    ob.dep.notify();

    return val;
}

MyVue.prototype.$set = set;

6.1 給數組對象添加屬性

把數組當作一個特殊的對象,就很容易理解了,對於unshift、push和splice變異方法是添加了對象的屬性的,須要對新加的屬性進行監聽和添加訂閱者。

var arrKeys = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
var extendArr = [];

arrKeys.forEach(function(key) {
    def(extendArr, key, function() {
        var result,
            arrProto = Array.prototype,
            ob = this.$Observer,
            arr = arrProto.slice.call(arguments),
            inserted,
            index;

        switch (key) {
            case "push":
                inserted = arr;
                index = this.length;
                break;
            case "unshift":
                inserted = arr;
                index = 0;
                break;
            case "splice":
                inserted = arr.slice(2);
                index = arr[0];
                break;
        }

        result = arrProto[key].apply(this, arguments);

        // 監聽新增數組對象屬性
        if (inserted) {
            ob.observeArray(inserted);
        }

        ob.dep.notify();

        return result;
    });
});

var arrayKeys = Object.getOwnPropertyNames(extendArr);

/**
 * 監聽器構造函數
 * @param {Object} data 被監聽數據
 */
function Observer(data) {

    this.dep = new Dep();

    if (!data || typeof data !== "object") {
        return;
    }

    // 在每一個object上添加一個observer
    def(data, "$Observer", this);

    // 繼承變異方法
    if (Array.isArray(data)) {

        // 把數組變異方法的處理,添加到原型鏈上
        data.__proto__ = extendArr;

        // 監聽數組對象屬性
        this.observeArray(data);
    } else {
        this.data = data;
        this.walk(data);
    }
}

Observer.prototype = {
    // ...

    /**
     * 監聽數組
     */
    observeArray: function(items) {
        console.log("items:", items);
        for (var i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    }
};

本文是在查看vue源碼及大神相關博客而成,只爲加深本身的學習印象,拿出來和你們一塊兒學習,有什麼不對的地方歡迎提出,參考文章:
http://www.cnblogs.com/giggle...
http://www.cnblogs.com/canfoo...

相關文章
相關標籤/搜索