vue实现双向绑定的原理

Posted by CodingWithAlice on September 23, 2021

vue实现双向绑定的原理

MVVM 模式在于数据与视图的保持同步,意思是说 数据改变时会自动更新视图,视图发生变化时会更新数据。所以我们需要做的就是如何检测到 数据的变化 然后通知我们去 更新视图,如何检测到 视图的变化 然后去 更新数据

  • 检测视图 这个比较简单,无非就是我们 利用事件的监听即可
  • 那么如何才能知道数据属性发生变化呢?这个就是利用我们上面说到的 Object.defineProperty 当我们的属性发生变化时,它会自动 触发 set 函数 从而能够通知我们去更新视图。

数据双向绑定作为 Vue 核心功能之一,Vue 则采用的是 数据劫持与发布订阅相结合的方式 实现双向绑定。

其中数据劫持是利用了 Object.defineProperty() 方法重新定义了对象获取属性值get和设置属性值set的操作 来实现的;

preview

劫持了数据之后,我们就需要一个监听器 Observer 来监听属性的变化。得知属性发生变化之后我们需要一个 Watcher 订阅者来更新视图,我们还需要一个 compile 指令解析器,用于解析我们的节点元素的指令与初始化视图。

  • Observer 监听器:用来监听属性的变化 通知订阅者
  • Watcher 订阅者:收到属性的变化,然后 更新视图(这个过程中我们可能会有很多个订阅者 Watcher 所以我们要创建一个容器 Dep 去做一个统一的管理)
  • Compile 解析器:解析指令,初始化模版,绑定订阅者

语法 Object.defineProperty(obj,prop,descriptor)

参数:obj:目标对象;prop:需要定义的属性或方法的名称;descriptor:目标属性所拥有的特性

可供定义的特性列表

可供定义的特性  
value 属性的值
writable 如果为false,属性的值就不能被重写
get 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户
set 一旦目标属性被赋值,就会调回此方法
configurable 如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化
enumerable 是否能在for…in循环中遍历出来或在Object.keys中列举出来
var obj = { };
var name;
Object.defineProperty(obj, "data", {
    //获取值
    get:function () {return name;},
    //设置值
    set:function (val) {name = val;console.log(val);}
})
obj.data = 'aaa';// 赋值调用set
console.log(obj.data); // 取值调用get

当我们 访问或设置 对象的属性的时候,都会 触发相对应的函数,然后在这个函数里返回或设置属性的值。既然如此,我们当然可以在触发函数的时候动一些手脚做点我们自己想做的事情,这也就是“劫持”操作。

在Vue中其实就是通过 Object.defineProperty劫持对象属性的setter和getter操作,并“种下”一个监听器,当数据发生变化的时候发出通知

1/2、针对 Object 类型的劫持

该方法每次只能设置一个属性,那么就需要遍历对象来完成其属性的配置:

Object.keys(student).forEach(key => defineReactive(student, key))

另外还必须是一个 具体的属性,这也非常的致命。假如后续需要扩展该对象,那么就必须手动为新属性设置 setter 和 getter 方法,这就是为什么不在 data 中声明的属性无法自动拥有双向绑定效果的原因 。这时需要调用 Vue.set() 手动设置。

2/2、针对 Array 类型的劫持

数组是一种特殊的对象,其下标实际上就是对象的属性,所以理论上是可以采用 Object.defineProperty() 方法处理数组对象。但是 Vue 并没有采用上述方法劫持数组对象,原因分析:

  • 1、特殊的 length 属性,相比较对象的属性,数组下标变化地相对频繁,并且改变数组长度的方法也比较灵活,一旦数组的长度发生变化,那么在无法自动感知的情况下,开发者只能手动更新新增的数组下标,这可是一个很繁琐的工作

  • 2、数组主要的操作场景还是遍历,而对于每一个元素都挂载一个 get 和 set 方法,恐怕也是不小的 性能负担

最终 Vue 选择劫持一些 常用的数组操作方法,从而知晓数组的变化情况push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'

数组方法的劫持涉及到原型相关的知识,首先数组实例大部分方法都是来源于 Array.prototype 对象。顺便提一下,采用 Vue.set() 方法设置数组元素时,Vue 内部实际上是调用劫持后的 splice() 方法来触发更新

总结:由上述内容可知,Vue 中的数据劫持分为两大部分:

  • 针对 Object 类型,采用 Object.defineProperty() 方法劫持属性的读取和设置方法
  • 针对 Array 类型,采用原型相关的知识劫持常用的函数,从而知晓当前数组发生变化。

并且 Object.defineProperty() 方法存在以下缺陷

每次只能设置一个具体的属性,导致需要遍历对象来设置属性,同时也导致了无法探测新增属性;属性描述符 configurable 对其的影响是致命的。

发布订阅模式

观察者 模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。

发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。

观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

1568963964686

数据无法双向绑定的情况

1/2、 Object.freeze();
var obj = {  foo: 'bar'}
Object.freeze(obj)
new Vue({  el: '#app',  data: obj})

Object.freeze()方法有三个特点:

1.使对象不可扩展,无法向其添加新属性。

2.为对象的所有属性将 configurable 特性设置为 false。在 configurablefalse 时,无法更改属性的特性且无法删除属性。

3.为对象的所有数据属性将 writable 特性设置为 false。当 writable 为 false 时,无法更改数据属性值。

2/2、数据未在实例中进行初始化

只有当 实例被创建data 中存在的属性才是响应式的。也就是说如果你添加一个新的属性在created生命周期完成后,那么数据将无法进行绑定,如直接使用vm.a=”xxx”,虽然a成了实例的一个属性,但是对它的任何修改将不是响应式的