vue数据劫持

540 阅读3分钟

主流程

在创建Vue实例时,会初始化数据,这时会调用initState方法对各类数据进行劫持。

数据劫持的初始化

比如初始化data属性中的数据,会实例化一个Observer对象,然后会对data中的所有属性进行遍历,为每个属性生成其独有的用来收集依赖的Dep实例。然后通过Object.defineProperty()方法为每个属性都设置get/set方法做依赖收集和通知依赖更新的功能。

以上完成了对data数据劫持的初始化工作。

依赖收集

初始化数据完成,就需要解析模版代码和挂载了。以在线编译版本为例,先把模版代码解析为AST后再生成render函数。然后创建一个Watcher实例,对render函数进行求值,并把当前watcher实例赋值给Dep.target。在求值的过程中,如果需要获取data中的属性值时,必定会调用到该属性值的get方法。在执行get方法时,会利用闭包获取到在初始化时为每个属性创建的Dep实例,把当前的Watcher实例,也就是Dep.target中的值,放入到里面。在此vue为了避免重复收集依赖,还做了优化处理(维护了两个set,如果在当前watcher实例中不存在dep的id,才调用dep.addSub方法添加依赖者)。当render函数执行完毕后,在此渲染函数中出现到的data中的属性,就把当前watcher分别收集到了各自的dep实例中了。然后把Dep.target还原回去就结束了。

以上完成了data数据对当前watcher的收集工作。

通知更新依赖

当修改了data中的某个属性时,就会触发该属性的set方法。首先vue会做一个优化,就是判断新旧值是否一致,如果一致,直接返回不做任何操作,如果不同,就需要通知依赖该属性的所以watcher进行更新操作。也就是调用当前属性的dep.notify()方法进行通知。notify方法会遍历dep中所有的watcher,然后watcher调用自己的update方法进行页面更新。

以上就是data中的数据被修改后,进行通知的工作。

对嵌套对象的处理

如果data中的属性值是个对象,比如a, 那么在对该值设置get/set方法之前,会再次调用observe方法,执行observe(val),为val添加__ob__属性并返回__ob__指向的Observer实例。最后的返回值childOb其实就是data.a.ob

其实,get方法中还有一句是判断childOb是否有值,如果有值,也需要对该值的dep实例收集依赖。这么做主要是因为在vue.$set方法中是调用__ob__的dep中的notify方法的,所以之前要把依赖也收集到__ob__的dep中。

对嵌套数组的处理

如果data中的属性值是个数组,那么在对该值设置get/set方法之前,也会再次调用observe方法,参数就是该数组的值。在observe方法中,会判断数据是否是数组,如果是,会对当然属性的原型进行重写。

重写是因为,数组类型的数据原型上会有一些可以改变原数组的方法,如:push, pop, shift, unshift, splice, sort,reverse。当调用这些方法时,原始数组改变,但是数组没有办法用get/set方法拦截,所以只能在这里手动调用notify方法进行通知依赖更新,而且这个notify方法还是 ob 中的dep上的方法。同时,push,unshift,splice方法还会新增属性,新增的属性同样需要响应式,所以调用__ob__中的observeArray方法进行数据处理。

注:为什么调用的是observeArray?因为新增的数据是以参数形式传入的,参数是类数组数据。而且,unshift和push可以传多个参数。

vue在对数组属性重置原型的操作也有做兼容处理。如过当前环境支持__proto__,就直接把重写后的原型对象赋值给__proto__,如果不支持,就把这些方法,用访问器属性的方式设置到这个数组上,并且把这些方法设置为不可枚举。

把这些重写的方法替换到当前数组属性之后,就调用observeArray观察数组的方法,给每个数组元素调用observe方法进行拦截处理。

新增属性无法拦截怎么办?vue.$set()解决

我们知道,vue2是无法自动拦截到新增的属性从而对其检测的,为了解决这个问题。vue2提供了vue.$set()方法。 set方法主要支持三种场景:

为目标对象是数组的数据增改元素

为目标对象是数组的增改数据都是调用该数组元素的splice方法,通过判断传入的坐标和数组长度判断是修改数组已有元素还是新增元素。因为目标对象是数组,在以前已经重写过splice方法了,所以这里调用的时候,就有响应式处理了。所以调用完splice方法就直接返回了。

为目标对象修改元素

对于修改目标元素已有的元素,也是直接赋值就可以,因为前面已经对属性进行响应式处理。

为目标对象新增元素

对于新增的元素,只需要给它设置get/set方法,让它具备响应式功能就可以了。然后在手动调用目标对象的notify方法,通知依赖更新。

Vue.$delete

这个就比较简单了,同样也是分数组和对象两种情况处理目标对象。如果是数组,就调用splice方法删除就可以,同样会调用响应式。如果是对象,就用delete方法把这个属性从目标对象里删除就可以,然后手动调用下目标函数的notify方法。

Vue3.0的响应式

Vue3.0不再使用getter和setter来替换有状态对象的属性,而是通过Proxy特性。Proxy方式将允许我们消除Vue现有的限制(例如无法检测新的属性添加),并提供更好的性能。

然而,Proxy是一个原生语言特性,在传统浏览器中这个特性无法用polyfill来兼容。因此,为了利用这个特性,我们必须调整Vue框架的浏览器支持范围,这是一个重大的突破性的改变,只能在新的主要版本中发布。