面试官:VUE双向数据绑定原理&&实现,你知否?

敲黑板划重点,这是考点。vue带给我们便利,我们也要知其然知其所以然,才能称对得起码农菜鸟这个称谓,才能和面试官闲话把vue家常。接下来,请集中注意力,我们来抽丝剥茧。

一、原理

先来看js对象的基本方法defineProperty():

var obj  = {};
Object.defineProperty(obj, 'name', {
    get: function() {
         console.log('我获取了name属性')
         return val;
     },
    set: function (newVal) {
         console.log('我设置了name属性为:' + newVal)
     }
})
obj.name = '魔丸';//在设置obj的name属性时,触发了set方法
var val = obj.name;//在获取obj的name属性时,触发了get方法

相信这个方法大家都了解,没错,vue就是运用了该方法实现的双向数据绑定。唠叨:vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变,大家都是拴在一条绳子上的蚂蚱。是不是似懂非懂,别急,继续上网图:

 

原理图讲解:

1 .observer(数据监听器/观察者):用来实现对vue的data中定义的每个属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知watcher(订阅者),watcher会触发它的update方法,对视图进行更新。

2.指令解析器Compile: 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,并绑定相应的更新函数。

3 .订阅者

  • 连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
  • 在vue中v-model,v-name,{{}}等都可以对数据进行显示,假如一个属性同时绑定了这三个指令,那么当这个属性值改变时,这三个指令对应的html视图都要改变。每当用到这样一个指令,就在Dep中增加一个订阅者。订阅者只是更新自己的指令对应的数据,也就是 v-model='name' 和 {{name}} 有两个对应的订阅者,各自管理自己的地方。

4.消息订阅器Dep:收集订阅者,数据变动后会触发notify,调用订阅者的update方法。

5.mvvm入口函数: 整合以上三者。

二、just do it

1.Observer实现思路:observe对被监听数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发setter,进而监听到数据变化。

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>双向绑定</title>
</head>

<body>
   <div id="app">
      <input type="text" class="name1" v-model="name">
      <div class="name2">{{name}}</div>
  </div>
</body>
<script> 

/** Vue构造函数
 * @param {*} param
 * */
 function Vue(options) {
  this.data = options.data;
  observe(this.data)
  this.$compile = new Compile(document.querySelector(options.el), this)
}
window.onload = function() {
    var app = new Vue({
      el:'#app',
      data: {
        name: '魔丸'
      }
    })
  }
function observe(data) {
  if(!data || typeof data  !== 'object') {
      return;
  }
  // 遍历所有属性
  Object.keys(data).forEach(function(key) {
    defineProp(data, key, data[key]);
  });
};

/** description
 * @data {*} 被修改data对象
 * @key {*} 被修改data对象的属性
 * @val {*} 被修改data对象的值
 * */
function defineProp(data, key, val) {
  observe(val); // 监听子属性
  //定义要修改对象的属性
  Object.defineProperty(data, key, {
      enumerable: true, // 可枚举
      configurable: false, // 不能再define
      get: function() {
          return val;
      },
      set: function(newVal) {
          console.log('监听到了,新属性值变化为 ', val, ' --> ', newVal);
          val = newVal;
      }
  });
}

</script>

</html>

2. compile订阅器实现:接下来我们需要订阅器去接收订阅者。当属性值变化时执行对应订阅者的更      新函数。显然订阅器是个数组容器。

设计思路:

  • Dep类定义在defineProp()函数中:每个属性对应多个Watcher,它们需要放在一个订阅器,当该属性值变化时,遍历并执行订阅器中的所有订阅者的update方法。
  • 添加订阅者操作放置在getter里面:让Watcher初始化时触发(需要判断是否需要添加订阅者)。
  • 通知watcher更新的操作放在在setter里面:若数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。
function defineProp(data, key, val) {
  var dep = new Dep();
  observe(val); // 监听子属性
  //定义要修改对象的属性
  Object.defineProperty(data, key, {
      enumerable: true, // 可枚举
      configurable: false, // 不能再define
      get: function() {
          //添加订阅者watcher到主题对象Dep
          if (Dep.currentWatcher) {
              dep.addWatcher(watcher); 
           }
          return val;
      },
      set: function(newVal) {
          console.log('监听到了,新属性值变化为 ', val, ' --> ', newVal);
          val = newVal;
          dep.notify(); // 通知所有订阅者
      }
  });
}
// 消息订阅器
function Dep() {
    this.watcherList = [];
}
Dep.prototype = {
    addWatcher: function(watcher) {
        this.watcherList.push(watcher);
    },
    notify: function() {
        this.watcherList.forEach(function(watcher) {
          watcher.update();
        });
    }
};

 

三. Watcher实现:

设计思路:

1、在自身实例化时往属性订阅器(dep)里面添加自己。
2、自身必须有一个update()方法:待属性变动,订阅器调用notice()通知时,能调用自身的update()方法。

/**订阅者
 * @param {*} vm 指令所属vue实例
 * @param {*} exp 指令对应的值
 * @param {*} dataItem 指令对应的data中的属性
 * */
function Watcher(vm, node, dataItem) {
  // 将当前订阅者指向自己,标记订阅者是当前watcher实例
  Dep.currentWatcher = this;   
  this.vm = vm; //当前vue实例
  this.node = node;//指令对应的DOM元素
  this.dataItem = dataItem; //指令对应的data中的属性
  this.value = this.get(); // 此处为了触发属性的getter,从而在dep添加自己
  // 添加完毕,释放对象。 Dep.currentWatcher 设为空。因为它是全局变量,
  // 也是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.currentWatcher 只有一个值。  
  Dep.currentWatcher = null;   
}
Watcher.prototype = {
  // 属性值变化收到通知
    update: function() {
      var newValue = this.get(); // 最新值
        var oldVal = this.value;
        if (newValue !== oldVal) {
            this.value = newValue;
            this.node.nodeValue = newValue; //更改节点内容的关键
        }    
    },
    get: function() {
         // 强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,
        var value = this.vm.data[this.dataItem];  
        return value;
    }
};

 

四.compile

设计思路

  • 为了减少页面渲染DOM元素的次数,需先将文档碎片化,等Dom节点渲染完毕,再将Dom内容插入原来的文档流中。
  • 需遍历所有节点及其子节点,扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定。
/** 解析器
 * @author liuyun 2020年06月08日 12:43:42'
 * @param {*} el id为app的Element元素
 * @param {*} vm vue实例
 * */
function Compile(el,vm) {
  // 将文档碎片化
  this.fragment = document.createDocumentFragment();
  let child;
  while (child = el.firstChild) {
    this.fragment.appendChild(child);
  }
  // 遍历所有节点及其子节点,扫描解析编译,调用对应的指令渲染函数进行数据渲染,调用对应的指令更新函数进行绑定
  this.compileElement(this.fragment,vm);
  //处理完所有节点后,重新把内容添加回去
  el.appendChild(this.fragment);
}

Compile.prototype = {
  compileElement: function(el,vm) {
    let _this = this;
    [].slice.call(el.childNodes).forEach(function(node) {
      var text = node.textContent;
      var reg = /\{\{(.*)\}\}/;    // 表达式文本
      // 如果是元素节点
      if (node.nodeType == 1) {
        for (let i = 0; i < node.attributes.length; i++) {
          let attr = node.attributes[i];
          if (attr.nodeName == 'v-model') { 
            let dataItemName = attr.nodeValue;
              node.addEventListener('input', function(e) {
              // 如果有v-model属性,则监听它的input事件
               vm.data[dataItemName] = e.target.value; // 给相应的data属性赋值,进而触发该属性的set方法
              })
              new Watcher(vm, node, dataItemName) //在消息订阅器中添加一个订阅者
              node.value = vm.data[dataItemName]; //将data中的值赋予给该node
              node.removeAttribute('v-model')
            }
        }
      } else if (node.nodeType == 3 && reg.test(node.nodeValue)) {
        //若是文本节点
        var name = RegExp.$1; // 获取匹配到的字符串
        name = name.trim();
        new Watcher(vm, node, name);
        node.nodeValue = vm.data[name];
      }
        // 遍历编译子节点
        if (node.childNodes && node.childNodes.length) {
          _this.compileElement(node,vm);
        }
     });
  }
}

动图效果:

getter/setter方法拦截数据的不足

需要vm.$set/Vue.set和vm.items.splice(newLength)解决,具体参看官方说明

1.增删对象时,是监控不到的。比如:data={name:"哪吒"},此时若再设置data.alias="魔丸",是监控不到的。因为属性的getter/setter方法是在observe初始化数据时遍历已有属性添加的,后面设置的alias没有设置getter/setter,所以检测不到变化。同样的,删除对象属性时,getter/setter会跟着属性一起被删除掉,拦截不到变化。

需要vm.$set/Vue.set和vm.$delete/Vue.delete这样的api来解决这个问题

2.getter/setter是针对对象的,像数组的修改(如push(),pop(),shift())导致arr发生了变化,同样需要更新视图,但是arr的getter/setter拦截不到变化(只有在赋值的时候才会调用setter,比如:arr=[1,2,3])。

对于这种情况,vue通过改写Array的默认方法,在调用这些方法的时候发布更新消息。一般无需关注。但是对于如下两种情况:

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

需要vm.$set/Vue.set和vm.items.splice(newLength)解决,具体参看官方说明

3.每次给数据设置值的时候,都会调用setter函数,这个时候就会发布属性更新消息,即使数据的值没有变。从性能方便考虑我们肯定希望值没有变化的时候,不更新模板。(像Angular这样把批量操作延时到一次更新,一次做完所有数据变更,然后整体应用到界面上)

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值