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

相关文章
相关标签/搜索