模拟Vue 中数据双向绑定

一、原理

如果使用Object.defineProperty,实现一个最简单的双向绑定其实很简单,只需如下:

<script>
    var Vue = {};
    Object.defineProperty(Vue,'$data',{
        set(val){
            document.getElementById('vue-item').innerText = val
        }
    });
    document.addEventListener('keyup', function(e){
        Vue.$data = e.target.value
    })
</script>

上面这个demo就是vue双向绑定最简化的原理。

 

二、替换元素

想想我们使用vue时的规则

new Vue({
    el:'app',
    data:{
        text:'hello world'
    }
});

写上页面结构:

<div id = 'app'>
    <input type='text' v-model='text'>
    {{text}}
</div>

我们把Vue抽象为一个构造函数,传入这些值

function Vue(options){
        this.data = options.data;
        this.id = options.el;
        getAllNode(document.getElementById(this.id), this);
};

替换掉节点中所有的{{xxxx}}:

function compile (node, vm){
        var reg = /\{\{(.*)\}\}/;// 匹配{{}}内任意字符
        // 节点类型为元素
        if (node.nodeType === 1) {
            var attr = node.attributes;
            // 解析属性
            for (var i = 0; i < attr.length; i++){
                if(attr[i].nodeName === 'v-model'){
                    var name = attr[i].nodeValue;// 获取绑定的属性的名字
                    node.value = vm.data[name];// 替换值
                    node.removeAttribute('v-model');  //移除v-model
                }
            };
        }
        // 节点类型为text
        if(node.nodeType === 3) {
            if(reg.test(node.nodeValue)) {
                var name = RegExp.$1; //匹配到的第一个字符
                name = name.trim();
                node.nodeValue = vm.data[name]; // 将data的值赋值给node
            }
        }
    };

    function getAllNode(node, vm){
        var length = node.childNodes.length;
        for(var i = 0; i < length; i++){
            compile(node.childNodes[i], vm)
        }
    };

这样就可以成功替换掉{{}}:

 

三、绑定元素

上面我们只是替换了元素,但还没有实现绑定

实现数据绑定,就要用到definedProperty的set和get方法:

首先我们要给vue的所有属性都添加set和get方法:

function Vue(){
    // *****
    observe(data,this)
}

// 遍历
function observe (obj, vm){
    for(var key in obj){
        active(vm, key, obj[key]);
    }
}
// 添加set和get
function active (obj, key, val) {
    Object.defineProperty(obj, key, {
        get(){
            return val;
        },
        set(newVal){
            if (newVal === val){ return };
            val = newVal;
        }
    });
}

再来明确我们要做的事,获取输入的值,改变Vue中相应的data的值,同时改变{{}}中的值;

我们已经给data的每个属性都添加了get和set的方法,现在要做的就是如何触发它们。

触发它肯定是在赋值的时候,所以我们在有v-model属性的节点监听输入事件,同时赋值,触发set事件:

function compile (node, vm){
    // ***********
    if (node.nodeType === 1) {
        var attr = node.attributes;
        // 解析属性
        for (var i = 0; i < attr.length; i++){
            if(attr[i].nodeName === 'v-model'){
                var name = attr[i].nodeValue;// 获取绑定的属性的名字

// 监听input事件 node.addEventListener('input', function(e){ // 给相应的data属性赋值,触发set vm.data[name] = e.target.value }) node.value = vm.data[name];// 替换输入框的值为data中的值 node.removeAttribute('v-model'); } }; } // ************ }

我们监听了input事件,接下来要获取输入的值并同步改变文本;

我们肯定希望只希望哪里改变了就对哪里做处理就行了,所以我们引入一个简单的发布——订阅组件:

function pubsub(){
    this.subs = []
}

pubsub.prototype = {
    addSub: function(sub){
        this.subs.push(sub);
    },
    pub: function(){
        this.subs.forEach(function(sub){
            sub.update();
        })
    }
}

在添加set和get的同时订阅事件:

function active (obj, key, val) {

    var pubsub = new pubsub();

    Object.defineProperty(obj.data, key, {
        get(){
            // 添加订阅
            if(Pubsub.target){
                pubsub.addSub(Pubsub.target);
            }
            return val;
        },
        set(newVal){
            if (newVal === val){ return };
            val = newVal;

            // 发出通知
            pubsub.pub();
        }
    });
}

添加一个方法,来在pubsub发出通知时处理事件,我们命名为watcher:

function Watcher(vm, node, name){
        Pubsub.target = this;
        this.name = name;
        this.node = node;
        this.vm = vm;
        this.update();
        Pubsub.target = null
    }

Watcher.prototype = {
      update(){
          this.get();
          this.node.nodeValue = this.value
      },
      // 获取data中的属性值
      get(){
          this.value = this.vm[this.name] // 触发相应的get
      }
  }

function getAllNode(node, vm){ var length = node.childNodes.length; for(var i = 0; i < length; i++){ compile(node.childNodes[i], vm) } };

这个watcher我们在什么时候添加呢?当然是在一开始的时候(compile里):

function compile (node, vm){
    var reg = /\{\{(.*)\}\}/;// 匹配{{}}内任意字符
    // 节点类型为元素
    if (node.nodeType === 1) {
        var attr = node.attributes;
        // 解析属性
        for (var i = 0; i < attr.length; i++){
            if(attr[i].nodeName === 'v-model'){
                var name = attr[i].nodeValue;// 获取绑定的属性的名字

                node.addEventListener('input', function(e){
                    // 给相应的data属性赋值,触发set
                    vm.data[name] = e.target.value
                })

                node.value = vm.data[name];// 替换输入框的值为data中的值
                node.removeAttribute('v-model');
            }
        };
    };
    // 节点类型为text
    if(node.nodeType === 3) {
        if(reg.test(node.nodeValue)) {
            var name = RegExp.$1; //匹配到的第一个字符
            name = name.trim();

            // node.nodeValue = vm[name];

            new Watcher(vm, node, name);// 观察输入的值
        }
    }
};

至此,便模拟了整个数据绑定的流程

 

四、总结

最后理清整个过程的思路

创建Vue:

 

input事件:

 

posted @ 2017-05-03 20:55  lastnigtic  阅读(1283)  评论(0编辑  收藏  举报