写在前面: 11月16日早上,Vue.js的做者尤大大在 Vue Toronto 的主题演讲中预演了 Vue.js 3.0的一些新特性,其中一个很重要的改变就是Vue3 将使用 ES6的Proxy 做为其观察者机制,取代以前使用的Object.defineProperty。我相信许多同窗深有体会,许多面试中Object.defineProperty是vue这个框架一个出现率很高的考察点,一开始你们对这个属性还有点陌生,慢慢的随着使用vue的人愈来愈多,这个属性常常被你们拿来研究,而就在你们渐渐熟悉了这个属性之后,vue的做者打算在下个vue版本中用 Proxy替换它,果真一入前端坑就爬不出来了哈哈。虽然vue3正式发布要等到明年下半年了,但咱们下面能够来探索下基于 Proxy 的观察者机制,预测下vue3关于Proxy这部分的代码(虽然看上去并无什么用哈哈)。javascript
一.为何要取代Object.defineProperty
既然要取代Object.defineProperty,那它确定是有一些明显的缺点,总结起来大概是下面两个:css
- Object.defineProperty没法监控到数组下标的变化,致使直接经过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,通过vue内部处理后可使用如下几种方法来监听数组
push()
pop()
shift() unshift() splice() sort() reverse()
因为只针对了以上八种方法进行了hack处理,因此其余数组的属性也是检测不到的,仍是具备必定的局限性。html
- Object.defineProperty只能劫持对象的属性,所以咱们须要对每一个对象的每一个属性进行遍历。Vue 2.x里,是经过 递归 + 遍历 data 对象来实现对数据的监控的,若是属性值也是对象那么须要深度遍历,显然若是能劫持一个完整的对象是才是更好的选择。
而要取代它的Proxy有如下两个优势;前端
- 能够劫持整个对象,并返回一个新对象
- 有13种劫持操做
看到这可能有同窗要问了,既然Proxy能解决以上两个问题,并且Proxy做为es6的新属性在vue2.x以前就有了,为何vue2.x不使用Proxy呢?一个很重要的缘由就是:vue
- Proxy是es6提供的新特性,兼容性很差,最主要的是这个属性没法用polyfill来兼容
相信尤大大在vue3.0的版本中会有效的提供兼容解决方案。java
关于Object.defineProperty来实现观察者机制,能够参照剖析Vue原理&实现双向绑定MVVM这篇文章,下面的内容主要介绍如何基于 Proxy来实现vue观察者机制。git
二.什么是Proxyes6
1.含义:github
- Proxy是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操做。
Proxy 让咱们可以以简洁易懂的方式控制外部对对象的访问。其功能很是相似于设计模式中的代理模式。- Proxy 能够理解成,在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。
- 使用 Proxy 的核心优势是能够交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,须要验证;某些属性的访问控制等)。 从而可让对象只需关注于核心逻辑,达到关注点分离,下降对象复杂度等目的。
2.基本用法:面试
let p = new Proxy(target, handler);
参数:
target
是用Proxy包装的被代理对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)。
handler
是一个对象,其声明了代理target 的一些操做,其属性是当执行一个操做时定义代理的行为的函数。
p
是代理后的对象。当外界每次对 p 进行操做时,就会执行 handler 对象上的一些方法。Proxy共有13种劫持操做,handler代理的一些经常使用的方法有以下几个:
get:读取
set:修改 has:判断对象是否有该属性 construct:构造函数
3.示例:
下面就用Proxy来定义一个对象的get和set,做为一个基础demo
let obj = {}; let handler = { get(target, property) { console.log(`${property} 被读取`); return property in target ? target[property] : 3; }, set(target, property, value) { console.log(`${property} 被设置为 ${value}`); target[property] = value; } } let p = new Proxy(obj, handler); p.name = 'tom' //name 被设置为 tom p.age; //age 被读取 3
p 读取属性的值时,实际上执行的是 handler.get() :在控制台输出信息,而且读取被代理对象 obj 的属性。
p 设置属性值时,实际上执行的是 handler.set() :在控制台输出信息,而且设置被代理对象 obj 的属性的值。
以上介绍了Proxy基本用法,实际上这个属性还有许多内容,具体可参考Proxy文档
三.基于Proxy来实现双向绑定
话很少说,接下来咱们就来用Proxy来实现一个经典的双向绑定todolist,首先简单的写一点html结构:
<div id="app"> <input type="text" id="input" /> <div>您输入的是: <span id="title"></span></div> <button type="button" name="button" id="btn">添加到todolist</button> <ul id="list"></ul> </div>
先来一个Proxy,实现输入框的双向绑定显示:
const obj = {}; const input = document.getElementById("input"); const title = document.getElementById("title"); const newObj = new Proxy(obj, { get: function(target, key, receiver) { console.log(`getting ${key}!`); return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { console.log(target, key, value, receiver); if (key === "text") { input.value = value; title.innerHTML = value; } return Reflect.set(target, key, value, receiver); } }); input.addEventListener("keyup", function(e) { newObj.text = e.target.value; });
这里代码涉及到Reflect
属性,这也是一个es6的新特性,还不太了解的同窗能够参考Reflect文档.
接下来就是添加todolist列表,先把数组渲染到页面上去:
// 渲染todolist列表 const Render = { // 初始化 init: function(arr) { const fragment = document.createDocumentFragment(); for (let i = 0; i < arr.length; i++) { const li = document.createElement("li"); li.textContent = arr[i]; fragment.appendChild(li); } list.appendChild(fragment); }, addList: function(val) { const li = document.createElement("li"); li.textContent = val; list.appendChild(li); } };
再来一个Proxy,实现Todolist的添加:
const arr = []; // 监听数组 const newArr = new Proxy(arr, { get: function(target, key, receiver) { return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { console.log(target, key, value, receiver); if (key !== "length") { Render.addList(value); } return Reflect.set(target, key, value, receiver); } }); // 初始化 window.onload = function() { Render.init(arr); }; btn.addEventListener("click", function() { newArr.push(parseInt(newObj.text)); });
这样就用 Proxy实现了一个简单的双向绑定Todolist,具体代码可参考proxy.html
四.基于Proxy来实现vue的观察者机制
1.Proxy实现observe
observe(data) {
const that = this; let handler = { get(target, property) { return target[property]; }, set(target, key, value) { let res = Reflect.set(target, key, value); that.subscribe[key].map(item => { item.update(); }); return res; } } this.$data = new Proxy(data, handler); }
这段代码里把代理器返回的对象代理到this.$data
,即this.$data
是代理后的对象,外部每次对this.$data
进行操做时,实际上执行的是这段代码里handler对象上的方法。
2.compile和watcher
比较熟悉vue的同窗都很清楚,vue2.x在 new Vue() 以后。 Vue 会调用 _init 函数进行初始化,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是经过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。相似于下面这个内部流程图:
而咱们上面已经用Proxy取代了Object.defineProperty这部分观察者机制,而要实现整个基本mvvm双向绑定流程,除了observe还须要compile和watche等一系列机制,咱们这里像模板编译的工做就不展开描述了,为了实现基于Proxy的vue添加Totolist,这里只写了
compile和watcher来支持observe的工做,具体代码参考proxyVue,这个代码至关于一个基于Proxy的一个简化版vue,主要是实现双向绑定这个功能,为了方便这里把js放到了html页面中