vue雙向綁定實現

前置知識

1. Object.defineProperty,能夠看這篇文章javascript

2. 觀察者模式,能夠看個人筆記html


雙向綁定

雙向綁定一個是視圖改變數據,這個簡單,好比input中輸入的文本綁定到數據中,那麼能夠經過監聽input事件實現vue

另外一個是數據改變視圖,這個具體怎麼實現呢?先看如下總結,帶着這些總結看代碼更容易理解java

怎麼實現

咱們要實現Watcher Dep Observer Compile,如下是它們的介紹。node

1. Watcher:首先要知道,每一個雙向綁定的屬性(如綁定在v-model中的屬性)都會生成watcher實例,watcher包含一個更新視圖的方法(命名爲update)。git

2. Dep:觀察者系統,用於存放(訂閱)watcher,在屬性改變時(setter)觸發,會執行watcher的update方法從而更新視圖。github

3. Observer:循環data中的屬性,對於每一個屬性都生成一個觀察者系統實例dep,而後設置getter,在getter中包含一個讓dep訂閱該屬性的watcher的操做,這個操做是怎麼執行的呢?實際上是經過在生成該屬性的watcher時,讀取一下該值,那麼就會進入到getter中,從而執行訂閱操做。而後再設置setter,在setter中觸發dep,從而執行watcher中更新dom的方法update,達到屬性值改變時更新視圖的目的。segmentfault

4. Compile:這個用於解析dom,初始化視圖和爲全部雙向綁定的屬性(如v-mode {{}} )生成watcher實例,由3可知,爲屬性建立watcher時,會讀取一下該屬性,讓這個屬性的觀察者dep訂閱該watcher。bash

實現一下

1. 首先實現下Vue構造函數,由此可知Vue在實例化時會作什麼。dom

function Vue (options) {
    this.data = options.data(); // vue的data是一個工廠函數
    let dom = document.querySelector(options.el);

    observe(this.data); // 爲data 的屬性進行 Object.defineProperty 操做
    new Compile(dom, this); // 解析dom,初始化視圖,爲雙向綁定的屬性生成watcher實例
}複製代碼

看看這個Vue構造函數好像有點不妥,好比我要讀取data中的一個name屬性時,我要這樣寫this.data.name,可是想一想咱們平時用vue時是否是直接this.name就能讀取到呢?因此這裏要給屬性作一下代理。

function Vue(options) {
    this.data = options.data(); // vue的data是一個工廠函數
    let dom = document.querySelector(options.el);

    // 代理下data的屬性    
    for (let key of Object.keys(this.data)) {
        this.proxy(key);    
    }
    
    // 爲data 的屬性進行 Object.defineProperty 操做
    observe(this.data);

    // 解析dom,初始化視圖,爲雙向綁定的屬性生成watcher實例
    new Compile(dom, this);
}

Vue.prototype.proxy = function (key){
    Object.defineProperty(this, key, {
        configurable: false,

        enumerable: true,

        get () {
            return this.data[key];
        },

        set (newVal) {
            this.data[key] = newVal;
        }
    });
}複製代碼

2. 由1可知,Vue實例化時會執行observe方法,上面已經介紹過observe主要是設置getter和setter,而且會用到觀察者模式,因此咱們先實現一個觀察者系統,再實現observe方法。

// 觀察者系統 用於訂閱watcher
function Dep() {
    this.subs = [];
}

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

    notify: function () {
        this.subs.forEach(function (sub) {
            sub.update(); // 執行watcher的更新視圖的方法。
        });
    }
}

function observe(data) {
    if (typeof data !== 'object') return;
    for (let key of Object.keys(data)) {
        defineReactive(data, key, data[key]);
    }
}

function defineReactive(data, key, val) {
    observe(val);

    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,

        configurable: true,

        get() {

            /* 
               在getter中包含訂閱watcher的操做,在實例化該屬性的watcher時,
               會把watcher綁定到Dep的靜態屬性target上,而後讀取一下該屬性,
               從而進入getter這裏執行這個訂閱操做。
            */
            if (Dep.target) {
                dep.addSub(Dep.target);
            }

            return val;
        },

        set(newval) {
            val = newval;

            // 觸發觀察者,從而執行watcher的update方法,更新視圖
            dep.notify();
        }
    })
}複製代碼

3. 由2可知,在getter中有個訂閱watcher的操做,那麼咱們實現下watcher,watcher會包含一個更新視圖的方法update。

function Watcher(vm, exp, cb) {
    this.cb = cb; // 一個更新視圖的方法 
    this.vm = vm;
    this.exp = exp;   
 
    // 綁定本身到Dep.target
    Dep.target = this;

    // 就是此處,讀取一下本身,從而進入getter,訂閱本身(Dep.target)
    this.value = this.vm[this.exp];

    // 釋放Dep.target
    Dep.target = null;
}

Watcher.prototype = {
    update () {
        let newValue = this.vm[this.exp];
        let oldValue = this.value;

        if (newValue !== oldValue) {
            this.cb.call(this.vm, newValue, oldValue)
        }
    }
}複製代碼

4. 好了 watcher有了,那麼實現下Compile,初始化視圖併爲雙向綁定的屬性生成watcher。

function Compile (el, vm) {
    this.el = el;
    this.vm = vm;

    this.compileElement(el);
}

Compile.prototype = {
    compileElement (el) {
        let childs = el.childNodes;
        Array.from(childs).forEach(node => {
            let reg = /\{\{(.*)\}\}/; 
            let text = node.textContent;

            if (this.isElementNode(node)) // 元素節點
                this.compile(node)
            else if (this.isTextNode(node) && reg.test(text)) { // 文本節點
                this.compileText(node, reg.exec(text)[1]);
            }

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

    compile (node) {
        let nodeAttr = node.attributes;
        Array.from(nodeAttr).forEach(attr => {
            if (this.isDirective(attr.nodeName)) { // v-model屬性
               node.value = this.vm[attr.nodeValue]; // 初始化

                // 綁定input事件,達到視圖更新數據目的 
               node.addEventListener('input', () => {
                    this.vm[attr.nodeValue] = node.value;
               })

               new Watcher(this.vm, attr.nodeValue, val => {
                    node.value = val;
               })
            }
       })    
   },

   compileText (node, exp) {
        node.textContent = this.vm[exp]; // 初始化

        new Watcher(this.vm, exp, val => {
            node.textContent = val;
        });
    },

    isElementNode (node) {
        return node.nodeType === 1;
    },

    isTextNode (node) {
        return node.nodeType === 3;
    },    
    
    isDirective (attr) {
        return attr === 'v-model';
    }
}複製代碼

大功告成,使用一下看看效果吧。

html:
<div id="demo">
    <div>{{text}}</div>
    <input v-model="text">
</div>

script:
new Vue({
    el: '#demo',
    data() {
        return {
            text: 'hello world'
        }
    }
})
複製代碼

代碼已提交到 github

相關文章
相關標籤/搜索