从Object.defindeProperty与Proxy出发。。。
前言
最近在看一些关于vue3.0的文章,其中提及到vue3.0将使用ES6的Proxy作为其观察者机制,取代之前使用的Object.defineProperty。我们都知道vue实现双向绑定是基于数据劫持从而监听数据变化来响应渲染函数实现dom更新。那么问题来了。为什么要取代呢?他们之前的优劣在哪?分别是怎么实现的数据劫持?他们的具体实现过程(原理)是什么样的?
什么是数据劫持
一般来说就是劫持对象的访问器,在属性值发生变化时我们可以获取变化。或者说通过Object.defineProperty来重写get,set函数。废话有点多,直接上代码。。。
1 | const data = { |
数据劫持的优势
数据劫持的目前应用就在双向绑定上,而双向绑定简单来说无非就是数据变化更新视图,视图变化更新数据。视图变化可以通过事件监听得到,比如input标签就监听input上的事件就行了。重点就是在数据变化更新视图。所以数据劫持的优势就体现了:
- 可精确得知变化数据。我们劫持了属性的setter,当属性值改变,我们可以精确获知变化的内容newVal。
- 无需显示调用。例如Vue运用数据劫持+发布订阅,直接可以通知变化后调用渲染函数来驱动视图。
基于Object.defineProperty数据劫持的双向绑定
极简版的双向绑定
1 | <input id="input"></input> |
但是这个双向绑定貌似问题很多首先这代码耦合性太高数据,方法,dom都连接在一起,而且只能监听对象的一个属性。所以我们要膜拜一下尤大大vue的发布订阅模式来实现。。
发布订阅模式的实现的双向绑定
首先我们要监听所有属性所以要实现一个监听器Observer用来监听所有属性,如果属性变化了就要通知订阅者Watcher看是否需要更新。因为每一个订阅者绑定一个更新函数,所以我们要实现一个订阅器Dep来收集这些订阅者,来负责储存订阅者和消息的分发。所以我们实现的就是:
- 监听器Observer => 用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- 订阅者Watcher => 收到属性的变化并执行相应函数来更新视图。
订阅器Dep => 收集所有订阅者,负责消息的分发执行notify。
实现监听器Observer
核心方法就是Object.defineProperty()。因为要遍历所有属性(包括子属性),所以要通过递归来遍历监听所有值。
1 | function defineReactive(targetObj,key,val){ //监听的具体方法 |
实现订阅器Dep
维护一个数组,用来收集订阅者,数据变化触发notify,再调用订阅者的update方法。
1 | function Dep() { //订阅器构造函数 |
我们知道在初始化vue实例的时候监听器Observer要监听实例的data的所有属性,而每一个属性都要关联一个订阅者Watcher来根据属性的变化来执行相应的函数。而我们的订阅器Dep就是专门来收集订阅者并通知触发各个订阅者的函数。那么问题来了,如何在vue实例化的时候就把订阅者添加到订阅器呢?带着这个疑问我们看一下订阅者Watcher
实现订阅者Watcher
1 | function Watcher(vm,exp,cb) { |
所以Dep.target就是连接订阅器Dep和订阅者Watcher的桥梁,并且利用订阅者Watcher实例化的时候触发get方法从而强制触发监听(Object.defineProperty)里的get方法来把自己Watcher添加到订阅器Dep中;所以上面的监听器Observer要做一些修改。
1 | function defineReactive(targetObj,key,val){ //监听的具体方法 |
到这就可以实现一个简单的从数据响应到dom的绑定了,虽然编译器compile没做,不过不在这篇文章的讨论的范围内。当然Watcher过滤也没做不过都可以暂时忽略。。。 现在我们通过一个简单例子把Observer和Watcher关联起来。
1 | <h1 id="name">name</h1> |
到这已经完全实现了基于Object.defineProperty来实现的数据响应到dom的绑定了。Proxy相对于Object.defineProperty来实现数据劫持的优势:
- 从上面的实现中可以看到Object.defineProperty的数据劫持是要对各个对象的每个属性都要遍历(Object.keys(targetObj).forEach(key => { defineReactive(targetObj, key, targetObj[key]); //监听的具体方法 })),如果属性值也是对象那么需要递归遍历,显然能劫持一个完整的对象是更好的选择。所以Proxy就来了,Proxy可以理解为他在目标对象外层设了一个拦截,外界对目标对象访问时都要通过这层拦截,因此可以对外界的访问进行过滤和改写,也就是所说的数据劫持。所以他的优势就很好体现了。不用递归遍历从而省性能。
- 如果把上面的{‘name’:’fuquanmeng’}改成{‘name’:[]}然后再onclick函数里写_myVue.data.name.push(111)。显然监听不到了。。。所以Object.defineProperty无法监听数组变化,虽然Vue已经把数组的这些方法[‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’]hack掉了但是其他的数组的属性依旧还是检测不到。而Proxy可以直接监听数组的变化。
还是极简版的双向绑定我们用proxy来改写
基于Proxy的极简双向绑定
1 | <input id="input"></input> |
监听数组变化就直接const obj = []也是可以监听得到,在这就不列举了。
总结
- 优势:Proxy相对于Object.defineProperty的优点在于可以检测数组的变化而且不用对目标对象的各个属性遍历监听,而且Proxy返回的是新对象我们可以只操作新对象来达到目的。
- 劣势:Proxy的劣势就是兼容性问题,而且无法用polyfill来磨平各个浏览器的差异。。。。