Vue --双向数据绑定原理

先说面试答案:

答:

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

具体步骤:

第一步 : 需要 observer(数据劫持) 对数据对象进行 递归遍历 ,包括 子属性对象的属性 ,都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会 触发setter ,那么就能监听到了数据变化

第二步 compiler(订阅者) 解析模板指令,将模板中的变量替换成数据,然后 初始化 渲染页面视图,并将每个指令对应的节点 绑定更新函数 ,添加监听数据的 订阅者 ,一旦数据有变动,收到通知,更新视图

第三步 Watcher(观察者) Observer Compiler 之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己

2、自身必须有一个 update ()方法

3、待属性变动 dep.notice ()通知时,能调用自身的 update ()方法,并触发 Compile 中绑定的回调,则功成身退。

第四步 MVVM 作为数据绑定的入口, 整合Observer、Compiler和Watcher三者 ,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

回答以上内容即可,下方内容,可以帮助大家理解


一、Vue数据双向绑定的原理

  • new Vue({}) Vue核心类

1.接收选项数据 {el:‘#app’,data:{mag:‘’}

2.调用Observer,实现数据劫持

3.调用Compiler指令解析指令与模板插值

  • Observer 数据劫持类( 数据劫持在vue 生命周期 中的 created阶段

1.数据劫持,把$data中的每个成员转换成getter和setter

2.初始化Dep

3.在getter中添加依赖watcher

4.在setter中通知更新页面

  • Dep 通知变化

1.收集Watcher 依赖

2.通知更新

  • Watcher 观察者

1.更新页面

2.每个节点对应一个 Watcher (类似侦听器)

3.节点编译Compiler 时实例化,实例保存到Dep观察者中

  • Compiler 解析指令-订阅者

1.负责解析指令和插值表达式

总结

1.vue首先通过 Observer 类,使用 Object.defineProperty 方法包装了数据,使object变成一个具有 getter/setter 属性的数据。

读取数据的时候通过 getter 方法读取,并在 getter 方法里面调用了 Dep 模块的 dep.depend() 方法 收集依赖 ,并为该依赖创建一个对应的 watcher 实例。

通过 setter 方法改变数据的时候调用了 Dep 模块的 dep.notify() 方法来 通知依赖 ,即依赖对应的watcher实例,遍历所有的watcher实例。

2. watcher 实例不直接更新视图,而是交给 scheduler 调度器, scheduler 维护一个事件队列通过 nextTick 执行事件,从而更新视图。

3. Compiler 解析指令和模板,和 Observer 是同时进行的,将节点实例化后,将 实例保存在Dep 中,当 Watcher 观察者发现数据变化通知视图更新。


二、什么是setter、getter

答:首先,他们就是一会要说的get、set

对象有两种属性:

  1. 数据属性: 就是我们经常使用的属性

  1. 访问器属性: 也称存取器属性(存取器属性就是一组获取和设置值的函数)

再看一行代码:

log打印出来的如下:

数据属性就是a和b;

get和set就是关键字 它们后面各自对应一个函数,这个函数就是上面红字部分所讲的,存储器属性。

get对应的方法称为getter,负责获取值,它不带任何参数。set对应的方法为setter,负责设置值,在它的函数体中,一切的return都是无效的。

三、什么是Object.defineProperty() ?

答:我们先看一句定义:

对象是由多个名/值对组成的无序的集合。对象中每个属性对应任意类型的值。

定义对象可以使用构造函数或字面量的形式:

除了以上添加属性的方式,当然还可以使用Object.defineProperty定义新属性或修改原有的属性;

语法:
Object.defineProperty(obj, prop, descriptor)
参数:
obj: 必需。目标对象;
prop: 必需。需定义或修改的属性的名字;
descriptor: 必需。目标属性所拥有的特性;
返回值:
传入函数的对象,即第一个参数obj;

OK,定义介绍完了,我们现在说一下一会关于双向绑定我们要用到的知识点: 存取器 描述;(诶?你是不是发现和上面好像有点关系)

对头,它是这样使用的:

现在无论是你获取还是设置我们都可以接到通知,是不是有一点双向数据绑定的影子了,别急下面还有;

OK,我终于叨叨完没用的了,现在开始说正题,如何理解Vue的双向数据绑定,哈哈,先来一个定义:

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

我们来看一个很粗暴的栗子,low版双向绑定:

是不是有点明白了呢,当然这也不是全部,我们刚刚说的大概就是下面Observer的部分,对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

别急,下面也很简单:

Dep,它就像一个依赖管理一样,小伙伴又问啥是依赖管理? 上图!

我用一个例子来解释一下上面这张图 下面高能预警:

在vue中 v-model,v-name,{ {}} 等都可以对数据进行展示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变;

于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个 Dep 中增加一个订阅者( addSub ),其订阅者只是更新自己的指令对应的数据,也就是 v-model='name' { {name}} 有两个对应的订阅者,各自管理自己的地方;

每当属性的 set方法触发 ,就循环更新 Dep 中的订阅者( notify );

OK,Dep是不是很明白了呢

集合上面的那张图来看,就是 Observer 一旦有了 set 触发,就会通知到 Dep ,那 Dep 接到通知之后呢?从图上来看,下面所讲的就应该是 Compiler 了,也很简单:

首先,先要知道它负责干什么?

compiler主要做的事情是解析模板指令,将模板中的变量替换成数据

其次知道它什么时候要工作,只有两种情况,先上图:

1)初始化,init的时候 初始化渲染页面视图;

2)将每个指令对应的节点绑定更新函数,添加监听数据的订阅者;

Dep 负责维护依赖,而订阅者则来自于 compiler ,一旦有数据变动,则会绑定更新函数,此时也就是产生了 订阅者 ,这个时候 Dep 内就增加了一个 订阅者 ,而一旦数据变动,则会收到通知,更新视图;

好了,你是不是觉得上面这行说不通,或是读不通,当然,因为上面的这个流程了缺少了,我们最后要说的Watcher,我把上面这句话补全,就是 Watcher 的工作了;

Dep负责维护依赖,而订阅者则来自于compiler ,一旦有数据变动,则会通过Watcher绑定更新函数,此时Watcher也向Dep中添加了订阅者,一旦Dep接到Observer的通知,它就会再去通知Watcher,Watcher则会调用自身的update()方法,并触发Compile中绑定的回调,更新视图;

最后敲黑板:

首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;

然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{ {}}也会,v-bind也会,只要用到该属性的指令理论上都会;

接着为input会添加监听事件,修改值就等于为该属性赋值,则会触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。


下面让我们来看代码演示:

先简单的实现一个js的双向数据绑定来熟悉一下Object.defineProperty()方法

 <input type="text" id="in"/>
    输入的值为:<span id="out"></span>
    <script>
        var int = document.getElementById('in');
        var out = document.getElementById('out');
        var obj = {};
        Object.defineProperty(obj, 'msg', {
            enumerable: true,
            configurable: true,
            set (newVal) {
                out.innerHTML = newVal;
            }
        })
        int.addEventListener('input', function(e) {
            obj.msg = e.target.value;
        })
    </script>

这样我们就能实现js的双向数据绑定,随着文本框输入文字的变化,span中会同步显示相同的文字内容;这样就实现了 model => view 以及 view => model 的双向绑定。

通过给 in 输入框添加事件监听 input 来触发 obj对象的set方法 ,而set再修改了访问器属性的同时,也修改了dom样式,改变了span标签内的文本。

四、Vue响应式的缺陷

1. Observer类的触发只发生在beforeCreate和created之间时间段

2.由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性。

3.会导致对象熟悉变化了(增加或者删除了),页面视图不会渲染更新。

解决方法:

  • 因此vue提供了 $set $delete 两个实例方法来解决这种情况。

// 新增
this.$set(this.obj, b, 2)

//删除
this.$delete(this.obj, b)

$delete 和 delete 的区别:

1. delete与vue.delete 删除对象区别:无区别

delete和和Vue.delete都是对数组或对象进行删除的方法。这两种方法对于对象来说其实是没有区别的,使用方法会直接删除对象的属性( 物理删除

let obj = {
name: 'fufu',
age: 20
}
// delete obj.age => {name: 'fufu'}
// Vue.delete(obj, 'age') => {name: 'fufu'}
// 测试发现对于对象来说delete和Vue.delete是没有任何区别的

2. delete与vue.delete删除 数组的区别 :

delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。数组长度也不变。(逻辑删)

Vue.delete是直接删除该元素,改变数组的键值,长度发生变化。(物理删)

var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[1]
console.log(a) //[1,undefined,3,4]
this.$delete(b,1)
console.log(b) //[1,3,4]

有Object.defineProperty() 和 Proxy 对象(代理)两种方式来实现数据双向绑定。 用对数据的劫持操作的方式。当访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截行为,然后进行额外的操作,然后返回结果。

Vue2.x 是使用 Object.defindProperty(),来进行对对象的监听的;
Vue3.x 版本之后就改用Proxy,进行实现的。

使用Object.defineProperty()实现数据双向绑定:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>数据双向绑定</title>
</head>
<body>
  <input type="text" id="in">
  <span id="p1"></span>
  <script>
    var inputName = document.getElementById('in');
    var spanName = document.getElementById('p1');

    var student = {};
    Object.defineProperty(student, 'name', {
      get: function() {
        return val;
      },

      set: function(val) {
        spanName.innerHTML = val;
      }
    });
    inputName.oninput = function() {
      student.name = this.value;
    }
  </script>
</body>
</html>

使用Proxy(代理)实现数据双向绑定:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>数据双向绑定</title>
</head>
<body>
  <input type="text" id="in">
  <span id="p1"></span>
  <script>
    var inputName = document.getElementById('in');
    var spanName = document.getElementById('p1');

    var student = {};
    var proxy = new Proxy(student, {
        get: function(target, prop) {
          return target[prop];
        },

        set: function(target, prop, value) {
          target[prop] = value;
          observer();
        }
      });

      function observer() {
        inputName.value = student.name;
        spanName.innerHTML = student.name;
      }

      inputName.oninput = function() {
        proxy.name = this.value;
      }
  </script>
</body>
</html>

五、Vue3.0的响应式

- 实现原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
- 通过Reflect(反射): 对源对象的属性进行操作。
      new Proxy(data, {
          // 拦截读取属性值
          get (target, prop) {
              return Reflect.get(target, prop)
          },
          // 拦截设置属性值或添加新属性
          set (target, prop, value) {
              // return Reflect.set(target, prop, value)
            Reflect.set(target, prop, value)
          },
          // 拦截删除属性
          deleteProperty (target, prop) {
              return Reflect.deleteProperty(target, prop)
          }
      })
      
      proxy.name = 'tom'