vue数据双向绑定原理

什么是数据响应式?

数据响应式即数据双向绑定,就是把Model绑定到view,当我们通过js修改Model,View会自动更新;若我们更新了View,Model的数据也会自动更新,这就是双向绑定。 

数据响应式原理

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,

那么vue是如果进行数据劫持的???

1)vue2.0版本是利用了Object.defineProperty()这个方法重新定义对象获取属性值的get和设置属性值set的操作来实现的。

2)vue3.0版本采用了Es6的Proxy对象来实现。

我们先分别了解下defineProperty方法、Proxy对象。

什么是defineProperty?

defineProperty简言之,是定义对象的属性。它可以来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举等。

它其实并不是核心的为一个对象做数据绑定,而是给对象做属性标签。定义对象的属性。只不过是属性的get和set实现了响应式。

属性名 默认值
value undefined
get undefined
set undefined
writable false
enumerable false
configurable false

  

接下来,我们先研究下它对应的两个描述属性get和set。

在平常,我们很容易就可以打印出一个对象的属性数据:

var person = {
  name: '小白'
};
console.log(person.name);  // 小白

如果想要在执行console.log(person.name)的同时,直接给书名加个书名号,那要怎么处理呢?或者说要通过什么监听对象 person 的属性值。

这时候Object.defineProperty( )就派上用场了,代码如下:

var person = {
  name: '小白'
}
var value = person.name
Object.defineProperty(person, 'name', {
  set: function (newValue) {
    value = newValue
    return '名字是' +  value;
  },
  get: function () {
    return '***' + value + '***'
  }
})

结果展示:

 

我们通过Object.defineProperty( )设置了对象person的name属性,对其get和set进行重写操作。

顾名思义,get就是在读取name属性这个值触发的函数,set就是在设置name属性这个值触发的函数。

实现一个简单完整版的mvvm双向绑定代码。 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
    <title>vuetest</title>
  </head>
  <body>
    <div id="app">
      <input v-model="text" id="test"></input>
      <div id="show"></div>
    </div>
    <!-- built files will be auto injected -->
  </body>

  <script>
var obj = {
    a:1,
    b:2
};
var _value = obj.a
Object.defineProperty(obj,'a',{
    get:function(){
        console.log("get方法")
        return _value
    },
    set:function(newValue){
        console.log("set方法")
        _value = newValue
        document.getElementById('test').value   =_value
        document.getElementById('show').innerHTML  =_value
        return _value
    }
});
document.getElementById('test').addEventListener('input',function(e){
    obj.a = e.target.value;
})
</script>
</html>

Object.defineProperty的缺点:

无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。

https://www.cnblogs.com/YikaJ/p/4278255.html

所以vue才设置了7个变异数组(push、pop、shift、unshift、splice、sort、reverse)的 hack 方法来解决问题。

只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。如果能直接劫持一个对象,就不需要递归 + 遍历了

监听数组的改变

1)先拷贝一份数组原型链 

2)定义一个方法总类

3)遍历数组,给数组原型链重新写方法,然后触发更新 dep.notify

 

proxy代理(详情点击 )

 

Proxy对象用于定义基本操作的自定义行为,和defineProperty功能类似,只不过用法有些不同

上述例子,用Proxy来替换defineProperty进行数据劫持。

var proxyObj = new Proxy(obj, {
  get:function(target, key, receiver){ //我们在这里拦截到了数据
        console.log("get方法",target,key,receiver)
        return true
    },
    set:function(target,key,value, receiver){  //改变数据的值,拦截下来额
        console.log("set方法",target,key,value, receiver)
        target[key]= value
        document.getElementById('test').value= value
        document.getElementById('show').innerHTML=value
        return true
    }
})

document.getElementById('test').addEventListener('input',function(e){
  proxyObj.a = e.target.value;
})

为什么vue3中改用proxy

1)defineProperty只能监听某个属性,不能对全对象监听,所以可以省去for in 提升效率

2)可以监听数组,不用再去单独对数组做操作

3)Proxy只是代理了原对象,不会污染原对象

那么,在vue中从一个数据到发生改变的过程是什么?

发布者-订阅者模式

 

 

 

Observer是个监听器,用来监听vue中的data中定义的属性。

通过Obeject.defineProperty()来监听数据的变动,通过递归方法遍历所有属性值。如果属性发上变化了,会通知给对应的dep。

Dep 在监听器Observer和订阅者Watcher之间进行统一管理的。

主要的作用就是收集观察者Watcher和通知观察者目标更新。每个属性拥有自己的消息订阅器dep,Dep实例里面存放所有订阅了该属性的观察者对象,

当数据发生改变时,会遍历订阅者列表(dep.subs),通过dep.notify()通知Watcher。


depend 实例方法用来收集依赖,notify 实例方法用来触发依赖的执行。经过了 depend => watcher.addDep => addSub (watcher 表示 Watcher 的一个实例)之后,subs 中收集的依赖实际上都是 Watcher 实例,再经过 notify => watcher.update 之后就可以触发实例化 Watcher 时的渲染函数和回调函数(如果有)的执行了。

Watcher类主要用来收集依赖和触发更新。 主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),

Compile是指令解析器,解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数

 

 

}
 

被监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明这个观察者是依赖该数据的(如计算属性中,计算某一属性会用到其他已经被监听的数据,就说该属性依赖于其他属性,会对其他属性进行取值),就会把这个观察者添加到该数据的订阅器subs里面,留待后面数据变更时通知(会先通过观察者id判断订阅器中是否已经存在该观察者),同时该观察者也会把该数据的订阅器dep添加到自身deps中,方便其他地方使用。

被监听的数据进行赋值操作时(setter)时,就会触发dep.notify(),循环该数据订阅器中的观察者,进行更新操作。

 

为什么要进行依赖收集?

new Vue({

    data(){

        return {

             name:'zane',

             sex:'男'

        }

    }

})

假设页面只使用到了name,并没有使用sex,根据Object.defineProperty的转换,如果我们设置了this.sex='女',那么Vue也会去执行一遍虚拟DOM的比较,

这样就无形的浪费了一些性能,因此才需要做依赖收集,页面用到了就收集,没有用到就不收集。

 

 

首先根据上图实现整体的一个架构,用到订阅发布者的设计模式。

然后实现MVVM中的由M到V,把模型里面的数据绑定到视图。

最后实现V-M,当文本框输入文本,触发更改模型中的数据,及更新相对应的视图。

我们可以先来看一下通过控制台输出一个定义在vue初始化数据上的对象是个什么东西。

  <div id="app">
      <input v-model="text" id="test"></input>
      <div id="show"></div>
   </div>

 

class Vue{
  constructor(options){
    console.log(options)
  }
}
const app = new Vue({
    el: '#app',
    data:{
      text: 'test'
    }
})

打印:

 

 

 

 

 

 

 

 var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };

 

 /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   * 即浏览所有属性并将其转换为get/set。仅当值类型为“对象”时才应调用此方法。
   */
  Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  };

 

 /**
   * Observe a list of Array items.
   * 即观察数组项列表
   */
  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

 

posted @ 2020-02-09 23:33  灰姑娘的冰眸  阅读(298)  评论(0编辑  收藏  举报