一篇文章带你彻底了解Vue.js如何实现数据双向绑定

Object.defineProperty中的秘密

学习过Vue.js的小伙伴都知道,Vue.js的核心在于组件化开发和数据的双向绑定来实现响应式布局,而在Vue2.x中提到数据的双向绑定,就一定会想到Object.defineProperty(),下面先来介绍一下Vue.js是如何实现数据的双向绑定的吧!

一、数据双向绑定的原理

一张图片描述出Vue.js实现数据双向绑定的过程

  1. 首先实现了一个监听器observer:对数据对象进行遍历,包括子属性对象的属性,利用**Object.defineProperty()**方法给属性都加上get()和set()方法,这样给这个对象的某个值进行赋值的时候就会触发set()方法,这样就会监听到了数据的变化。
let data = {name:'xiaomao'};
observer(data);
data.name = 'wangxuejiao';   //值发生了变化

function observer(data){
   if(!data && typeof data !== 'object'){
       return;
   }
   //取出所有属性进行遍历
   Object.keys(data).forEach(function(key){
      defineReactive(data, key, data[key]);
   })
}

function defineReactive(data,key,val){
   observer(val);  //监听子属性
   Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}
  1. 实现了一个订阅器Dep:本质上是一个数组,采用发布——订阅的设计模式,用来收集订阅者watcher,实现对监听器observer和订阅者watcher的统一管理
function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
  1. 实现了一个订阅者watcher:可以收到属性变化的通知并执行相应的函数,从而更新视图,是observer和complie之间的桥梁,主要功能是订阅observer中属性值变化的消息,当收到消息时,触发解析器compile中对应的更新函数。
function watcher(vm,exp,cb){
   this.vm = vm;
   this.exp = exp;
   this.vm = vm;
   //此处为了触发属性的getter,从而在dep里面添加自己,结合Observer更易理解
   this.value = this.get();
}
Watcher.prototype = {
   update:function(){
       this.run();  //属性值变化收到通知
   },
   run:function(){
       var value = this.get();   //取到最新值
       var oldVal = this.value;
       if(value !== oldValue){
          this.value = value;
          this.cb.call(this.vm,value,oldVal);  //执行Compile中绑定的回调,更新视图
       }
   }
   get:function(){
       Dep.target = this;   //将当前订阅者指向自己
       var value = this.vm[exp];  //触发getter,添加自己到属性订阅器中
       Dep.target = null;    //添加完毕,重置
       return value;
   }
}
  1. 实现了一个解析器compile:解析Vue模板指令,将模板中变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点,绑定更新数据,添加监听数据的订阅者,一旦数据有变动收到通知调用更新函数进行数据更新。
function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    },
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
     },
    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    },
    // 指令处理集合
	var compileUtil = {
	   text: function(node, vm, exp) {
	       this.bind(node, vm, exp, 'text');
	   },
	   // ...省略
	   bind: function(node, vm, exp, dir) {
	       var updaterFn = updater[dir + 'Updater'];
	       // 第一次初始化视图
	       updaterFn && updaterFn(node, vm[exp]);
	       // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
	       new Watcher(vm, exp, function(value, oldValue) {
	           // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
	           updaterFn && updaterFn(node, value, oldValue);
	       });
	   }
}

总结:

  • 整体主要是给data对象每一个属性,通过Object.defineProperty()添加get()以及set()方法
  • 在get()方法中通过闭包使用消息订阅器数组添加存放订阅器对象,然后返回属性值
  • 在set()方法中通过先判断新旧值是否一致,如果不一致,则把新值赋给属性,并且调用订阅器的notify方法,这个方法会去通知所有订阅者,订阅者就会去执行对应的更新函数

二、Object.defineProperty()存在的缺陷

Object.defineProperty()是javascript中监听数据变化的方法,vue2.x采用Object.defineProperty()来实现数据双向绑定,但是它却存在一些缺陷,所以在Vue3.0中,尤大大放弃使用Object.defineProperty()而是采用Proxy来实现数据双向绑定,下面就来总结一下Object.defineProperty()到底有哪些缺点才会让尤大大放弃使用呢

  1. 对于对象而言,它无法检测到对象属性的新增或删除

由于Vue会在初始化实例的时候对属性执行getter和setter转化,所以属性必须在data对象上存在才能将它转化成响应式的

对于已经创建的实例,Vue不允许动态添加根级别的响应式属性,但是可以使用Vue.set(object,propertyName,value)方法向嵌套对象添加响应式属性

Vue.set(obj,'name','wxj');
Vue.$set(thgis.obj,'name','wxj');

如果想要添加多个属性,使用Object.assign(),这样添加到对象身上的属性也不会触发更新,应该用原对象与要混合进去的对象的属性一起创建一个新的对象

this.obj = Object.assign({},this.obj, { b: 1, e: 2 })
this.$set(this.obj,'f',0)
this.obj = {...this.obj,...{ b: 3, e: 2 }}

删除属性

Vue.delete(obj, propertyName/index);
vue.$delete(obj, propertyName/index);
  1. 对于数组而言,无法监听以下数组的变化
  • 利用一个索引值直接设置一个数组项
Vue.set(vm.items,name,newValue);
  • 修改数组的长度
Vue.items.splice(newLength);
  • 以下七种方法不能实现响应式,Vue重写了这七种方法:push pop shift unshift splice sort reverse
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值