原生js实现 vue的数据双向绑定

原生js实现一个简单的vue的数据双向绑定

vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调

所以我们要先做好下面3步:

1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

1.实现一个Observer

Observer是一个数据监听器,主要依赖于Object.defineProperty()方法,而这个方法在ie8及以下存在兼容问题,请看(MDN defineProperty)所以如vue官网所说:

兼容性

Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有(兼容 ECMAScript 5 的浏览器。)

 正因为这个方法,我们就可以利用Obeject.defineProperty()来监听属性变动 那么就可以把需要observer的数据对象进行递归遍历,给他的每个属性都可以加上get,set。

 而当给这个对象的某个值赋值操作的时候,就会触发setter,那么就能监听到了数据变化。

    function observer(data) {
       // 当不是对象的时候,退出
        if (!data || typeof data !== 'object') {
            return;
        }
        // 取出所有属性遍历
        Object.keys(data).forEach(function(key) {
         // 给每个属性加上get,set
            defineReactive(data, key, data[key]);
        });
    };
    function defineReactive(data, key, val) {
        observer(val); // 监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可遍历
            configurable: false, // 不能修改,删除
            get: function() {
                return val;
            },
            set: function(newVal) {
                val = newVal;
                console.log("已改变 "+newVal);
            }
        });
    }

 测试一下:

    var obj={
        name:'aaaaa',
        book:{
            name:'bbbbbb'
        }
    }
    observer(obj)
    obj.name='cc' //已改变 cc
    obj.book.name='dd' //已改变 dd

 

现在已经监听了数据中的属性变化了,而接下来就是在代码中加入发布者-订阅者模式,关于这个设计模式不懂的可以看这里(js设计模式-发布订阅模式

首先我们先创造一个订阅器Dep,他是用来收集订阅者Watcher的,然后在属性发生变化的时候执行对应订阅者的更新函数update 。

在上面的 defineReactive 函数最后加入下面代码:

function Dep() {
    this.subs = [];//存放消息数组
}
Dep.prototype = {
    addSub: function(sub) {   //增加订阅者函数
        this.subs.push(sub);
    },
    notify: function() {    //发布消息函数
        this.subs.forEach(function(sub) {
            sub.update();   //这里是订阅者的更新方法
        });
    }
};

 然后修改上面的 defineReactive 函数:

    function defineReactive(data, key, val) {
        var dep = new Dep(); //实例化一个订阅器
        observer(val); // 监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可遍历
            configurable: false, // 不能修改,删除
            get: function() {
                return val;
            },
            set: function(newVal) {
                if (val === newVal){return} //当前后数值相等,不做改变
                val = newVal;
                dep.notify(); //当前后数值变化,这时就通知订阅者了
                console.log("已改变 "+newVal);
            }
        });
    }

 在vue里面针对data这个对象的处理,区分了数组和对象,我们这只考虑对象这一种情况,更多(vue-数组处理)正由于这种处理,所以vue对于:

注意事项

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

这种情况,设计了 vm.$set 这个实例方法来弥补这方面限制带来的不便。

2.实现Watcher

 在第一步我们实现了订阅器,这一步实现订阅者,从上面代码看,在dep 调用 notify() 方法的时候,我们就该去遍历订阅者Watcher了,并且调用他自己的update()方法,

 先实现订阅者对象,如下:

    function Watcher (vm, exp, cb){
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();//初始化的时候就调用
    }

    Watcher.prototype={
        // 只有在订阅者Watcher初始化的时候才需要添加订阅者
        get:function(){
            Dep.target = this;  // 在Dep.target缓存下订阅者
            var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放订阅者
            return value;
        },
        // dep.subs[i].notify() 会执行到这里
        update:function() {
            this.run();
        },
        run:function() {
            // 执行 get()获得value ,call更改cb的this指向 。
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
        }
    }

 

而在这个时候,我们需要通过Dep的addSub(),将Watcher加进去,在Watcher的构造函数中会调用这句话

this.value = this.get();//初始化的时候就调用

所以我们可以在 defineReactive 函数中做个调整,修改一下Object.defineProperty的get要调用的函数,来对应Watcher构造函数的这个get函数。

而在这里需要判断一下:是不是Watcher的构造函数在调用,如果是,说明他就是这个属性的订阅者。

这里就会用到    Dep.target   这样一个全局唯一的变量,用来判断。代码如下:

    function defineReactive(data, key, val) {
        var dep = new Dep(); //实例化一个订阅器
        observer(val); // 监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可遍历
            configurable: false, // 不能修改,删除
            get: function() {
                // 如果这个属性存在,说明这是watch 引起的
                if(Dep.target){
                    // 那我调用dep.addSub把这个订阅者加入订阅器里面
                    dep.addSub(Dep.target)
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal){return} //当前后数值相等,不做改变
                val = newVal;
                dep.notify(); //当前后数值变化,这时就通知订阅者了
                console.log("已改变 "+newVal);
            }
        });
    }

  最后在Dep函数后面加上

        Dep.target = null;//释放每一个订阅者

 

 

写到这里我们可以写个简单的例子来测试一下:

  先写个函数将我们的Observer和Watcher关联起来:

        function pvp(data,el,exp){
            this.data=data;
            observer(data); //给data每个属性加上get,set
            el.innerHTML=this.data[exp]; //粗暴的直接绑定,测试一下效果
            new Watcher(this,exp,function(val){
                el.innerHTML = val;  //调用Watcher直接赋值
            })
            return this
        }

  在html中写下:

    <h1 id="name">{{name}}</h1>

 

  js中写下:

        var ele = document.querySelector('#app');
        var pvp = new pvp(
          {test: 'hello pvp'},
          ele,
          'test'
     );

 

  结果如下:

 

好了,下面我们来实现对dom节点的解析,可以让pvp写起来更像vue的写法

3.实现Compile     

解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,这个环节需要对dom操作比较频繁,

所以可以用一个通用的办法,先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理。请看(空文档对象

            //创造一个空白节点
            nodeToFragment: function (el) {
                var fragment = document.createDocumentFragment();
                var child = el.firstChild;
                while (child) {
                    // 将Dom每个元素都移入fragment中
                    fragment.appendChild(child);
                    child = el.firstChild;
                }
                return fragment;
            },

 

 而在这里我们需要实现一个 Compile 方法,代码如下:

        function Compile(el, vm) {
            this.vm = vm;
            this.el = document.querySelector(el);
            this.fragment = null;
            this.init();   //初始化一个方法,直接调用解析节点
        }
        Compile.prototype = {
            init: function () {
                if (this.el) {
                    this.fragment = this.nodeToFragment(this.el); //调用了上面的方法,把元素放入并返回
                    this.compileElement(this.fragment);//对这个里面的元素解析
                    this.el.appendChild(this.fragment);//再重新放回去
                } else {
                    console.error("找不到节点")
                }
            },
            //创造一个空白节点
            nodeToFragment: function (el) {
                var fragment = document.createDocumentFragment();
                var child = el.firstChild;
                while (child) {
                    // 将Dom每个元素都移入fragment中
                    fragment.appendChild(child);
                    child = el.firstChild;
                }
                return fragment;
            },
            // 解析节点
            compileElement: function (el) {
                var childNodes = el.childNodes;
                var self = this;
                [].slice.call(childNodes).forEach(function(node) {
                    var reg = /\{\{(.*)\}\}/;
                    var text = node.textContent;
                    if (node.nodeType == 1) { //如果是元素节点
                        self.compileFirst(node);
                    } else if (node.nodeType == 3 && reg.test(text)) {   //如果是文本节点
                        self.compileText(node, reg.exec(text)[1]);
                    }
                    if (node.childNodes && node.childNodes.length) { //如果下面还有子节点,继续循环
                        self.compileElement(node);
                    }
                });
            },
            //如果是元素节点
            compileFirst: function(node) {
                var nodeAttrs = node.attributes;
                var self = this;
                Array.prototype.forEach.call(nodeAttrs, function(attr) {
                    var attrName = attr.name;
                    var exp = attr.value;
                    if (attrName='p-model') {  //当这个属性为p-model的时候就解析model
                        self.compileModel(node, self.vm, exp);
                    }
                });
            },
            //如果是文本节点
            compileText: function(node, exp) {
                var self = this;
                var initText = this.vm[exp];
                this.updateText(node, initText);
                new Watcher(this.vm, exp, function (value) {
                    self.updateText(node, value);//通知Watcher,开始订阅
                });
            },
            //解析p-model
            compileModel: function (node, vm, exp) {
                var self = this;
                var val = this.vm[exp];
                this.modelUpdater(node, val);
                new Watcher(this.vm, exp, function (value) {
                    self.modelUpdater(node, value); //通知Watcher,开始订阅
                });
                node.addEventListener('input', function(e) {
                    var newValue = e.target.value;
                    if (val === newValue) {
                        return;
                    }
                    self.vm[exp] = newValue;
                    val = newValue;
                });
            },
            updateText: function (node, value) {
          //这里是直接替换文本节点 node.textContent
= typeof value == 'undefined' ? '' : value; }, modelUpdater: function(node, value, oldValue) { //如果不存在就返回空,这里是更新model node.value = typeof value == 'undefined' ? '' : value; } }

 然后我们在对上面的关联函数  pvp  进行修改主要是对 data 用 defineProperty 方法进行再一次封装,方便我们使用 xx.data 的写法去调用属性,在对这个函数进行修改,代码如下:

        function Pvp(options){
            var self = this;
            this.data=options.data;

            Object.keys(this.data).forEach(function(key) {
                self.proxyKeys(key);
            });
            observer(this.data); //给data每个属性加上get,set
            new Compile(options.el,this)
            return this
        }
        Pvp.prototype = {
            proxyKeys: function (key) {
                var self = this;
                Object.defineProperty(this, key, {
                    enumerable: false,
                    configurable: true,
                    get: function getter () {
                        return self.data[key];
                    },
                    set: function setter (newVal) {
                        self.data[key] = newVal;
                    }
                });
            }
        }

 

  最后在我们的html的script中写下:

         var src=new Pvp({
            el: '#app',
            data: {
                test: 'hello world',
            }
        });

 

  打完收工!!!!  最后点(源码)获取源码

 

转载于:https://www.cnblogs.com/oicb/p/8316759.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值