阅读目录javascript
1.什么是Proxy?它的做用是?html
据阮一峰文章介绍:Proxy能够理解成,在目标对象以前架设一层 "拦截",当外界对该对象访问的时候,都必须通过这层拦截,而Proxy就充当了这种机制,相似于代理的含义,它能够对外界访问对象以前进行过滤和改写该对象。vue
若是对vue2.xx了解或看过源码的人都知道,vue2.xx中使用 Object.defineProperty()方法对该对象经过 递归+遍历的方式来实现对数据的监控的,具体了解
Object.defineProperty能够看我上一篇文章(http://www.javashuo.com/article/p-qpgkyhbt-hh.html). 可是经过上一篇Object.defineProperty文章 咱们也知道,当咱们使用数组的方法或改变数组的下标是不能从新触发 Object.defineProperty中的set()方法的,所以就作不到实时响应了。因此使用 Object.defineProperty 存在以下缺点:java
1. 监听数组的方法不能触发Object.defineProperty方法中的set操做(若是要监听的到话,须要从新编写数组的方法)。
2. 必须遍历每一个对象的每一个属性,若是对象嵌套很深的话,须要使用递归调用。node
所以vue3.xx中以后就改用Proxy来更好的解决如上面的问题。在学习使用Proxy实现数据双向绑定以前,咱们仍是一步步来,先学习了Proxy基本知识点。git
Proxy基本语法github
const obj = new Proxy(target, handler);数组
参数说明以下:app
target: 被代理对象。
handler: 是一个对象,声明了代理target的一些操做。
obj: 是被代理完成以后返回的对象。函数
可是当外界每次对obj进行操做时,就会执行handler对象上的一些方法。handler中经常使用的对象方法以下:
1. get(target, propKey, receiver)
2. set(target, propKey, value, receiver)
3. has(target, propKey)
4. construct(target, args):
5. apply(target, object, args)
如上是Proxy中handler 对象的方法,其实它和Reflect里面的方法相似的,想要了解Reflect看这篇文章
以下代码演示:
const target = { name: 'kongzhi' }; const handler = { get: function(target, key) { console.log(`${key} 被读取`); return target[key]; }, set: function(target, key, value) { console.log(`${key} 被设置为 ${value}`); target[key] = value; } }; const testObj = new Proxy(target, handler); /* 获取testObj中name属性值 会自动执行 get函数后 打印信息:name 被读取 及输出名字 kongzhi */ console.log(testObj.name); /* 改变target中的name属性值 打印信息以下: name 被设置为 111 */ testObj.name = 111; console.log(target.name); // 输出 111
如上代码所示:也就是说 target是被代理的对象,handler是代理target的,那么handler上面有set和get方法,当每次打印target中的name属性值的时候会自动执行handler中get函数方法,当每次设置 target.name 属性值的时候,会自动调用 handler中的set方法,所以target对象对应的属性值会发生改变,同时改变后的 testObj对象也会发生改变。同理改变返回后 testObj对象中的属性也会改变原对象target的属性的,由于对象是引用类型的,是同一个引用的。若是这样仍是很差理解的话,能够简单的看以下代码应该能够理解了:
const target = { name: 'kongzhi' }; const testA = target; testA.name = 'xxx'; console.log(testA.name); // 打印 xxx console.log(target.name); // 打印 xxx
2.get(target, propKey, receiver)
该方法的含义是:用于拦截某个属性的读取操做。它有三个参数,以下解析:
target: 目标对象。
propKey: 目标对象的属性。
receiver: (可选),该参数为上下文this对象
以下代码演示:
const obj = { name: 'kongzhi' }; const handler = { get: function(target, propKey) { // 使用 Reflect来判断该目标对象是否有该属性 if (Reflect.has(target, propKey)) { // 使用Reflect 来读取该对象的属性 return Reflect.get(target, propKey); } else { throw new ReferenceError('该目标对象没有该属性'); } } }; const testObj = new Proxy(obj, handler); /* Proxy中读取某个对象的属性值的话, 就会使用get方法进行拦截,而后返回该值。 */ console.log(testObj.name); // kongzhi /* 若是对象没有该属性的话,就会进入else语句,就会报错: Uncaught ReferenceError: 该目标对象没有该属性 */ // console.log(testObj.name2); /* 其实Proxy中拦截的操做是在原型上的,所以咱们也可使用 Object.create(obj) 来实现对象的继承的。 以下代码演示: */ const testObj2 = Object.create(testObj); console.log(testObj2.name); // 看看他们的原型是否相等 console.log(testObj2.__proto__ === testObj.__proto__); // 返回true
若是没有这个拦截的话,若是某个对象没有该属性的话,会输出 undefined.
3.set(target, propKey, value, receiver)
该方法是用来拦截某个属性的赋值操做,它能够接受四个参数,参数解析分别以下:
target: 目标对象。
propKey: 目标对象的属性名
value: 属性值
receiver(可选): 通常状况下是Proxy实列
以下代码演示:
const obj = { 'name': 'kongzhi' }; const handler = { set: function(obj, prop, value) { return Reflect.set(obj, prop, value); } }; const proxy = new Proxy(obj, handler); proxy.name = '我是空智'; console.log(proxy.name); // 输出: 我是空智 console.log(obj); // 输出: {name: '我是空智'}
固然若是设置该对象的属性是不可写的,那么set方法就不起做用了,以下代码演示:
const obj = { 'name': 'kongzhi' }; Object.defineProperty(obj, 'name', { writable: false }); const handler = { set: function(obj, prop, value, receiver) { Reflect.set(obj, prop, value); } }; const proxy = new Proxy(obj, handler); proxy.name = '我是空智'; console.log(proxy.name); // 打印的是 kongzhi
注意:proxy对数组也是能够监听的;以下代码演示,数组中的 push方法监听:
const obj = [{ 'name': 'kongzhi' }]; const handler = { set: function(obj, prop, value) { return Reflect.set(obj, prop, value); } }; const proxy = new Proxy(obj, handler); proxy.push({'name': 'kongzhi222'}); proxy.forEach(function(item) { console.log(item.name); // 打印出 kongzhi kongzhi222 });
4.has(target, propKey)
该方法是判断某个目标对象是否有该属性名。接收二个参数,分别为目标对象和属性名。返回的是一个布尔型。
以下代码演示:
const obj = { 'name': 'kongzhi' }; const handler = { has: function(target, key) { if (Reflect.has(target, key)) { return true; } else { return false; } } }; const proxy = new Proxy(obj, handler); console.log(Reflect.has(obj, 'name')); // true console.log(Reflect.has(obj, 'age')); // false
5.construct(target, args, newTarget):
该方法是用来拦截new命令的,它接收三个参数,分别为 目标对象,构造函数的参数对象及创造实列的对象。
第三个参数是可选的。它的做用是拦截对象属性。
以下代码演示:
function A(name) { this.name = name; } const handler = { construct: function(target, args, newTarget) { /* 输出: function A(name) { this.name = name; } */ console.log(target); // 输出: ['kongzhi', {age: 30}] console.log(args); return args } }; const Test = new Proxy(A, handler); const obj = new Test('kongzhi', {age: 30}); console.log(obj); // 输出: ['kongzhi', {age: 30}]
6.apply(target, object, args)
该方法是拦截函数的调用的。该方法接收三个参数,分别是目标对象。目标对象上下文this对象 和 目标对象的数组;它和 Reflect.apply参数是同样的,了解
Reflect.apply(http://www.javashuo.com/article/p-sljulsac-eb.html).
使用demo以下演示:
function testA(p1, p2) { return p1 + p2; } const handler = { apply: function(target, ctx, args) { /* 这里的 ...arguments 其实就是上面的三个参数 target, ctx, args 对应的值。 分别为: target: function testA(p1, p2) { return p1 + p2; } ctx: undefined args: [1, 2] 使用 Reflect.apply(...arguments) 调用testA函数,所以返回 (1+2) * 2 = 6 */ console.log(...arguments); return Reflect.apply(...arguments) * 2; } } const proxy = new Proxy(testA, handler); console.log(proxy(1, 2)); // 6 // 也能够以下调用 console.log(proxy.apply(null, [1, 3])); // 8 // 咱们也可使用 Reflect.apply 调用 console.log(Reflect.apply(proxy, null, [3, 5])); // 16
7.使用Proxy实现简单的vue双向绑定
vue3.x使用了Proxy来对数据进行监听了,所以咱们来简单的来学习下使用Proxy来实现一个简单的vue双向绑定。
咱们都知道实现数据双向绑定,须要实现以下几点:
1. 须要实现一个数据监听器 Observer, 可以对全部数据进行监听,若是有数据变更的话,拿到最新的值并通知订阅者Watcher.
2. 须要实现一个指令解析器Compile,它可以对每一个元素的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的函数。
3. 须要实现一个Watcher, 它是连接Observer和Compile的桥梁,它可以订阅并收到每一个属性变更的通知,而后会执行指令绑定的相对应
的回调函数,从而更新视图。
下面是一个简单的demo源码以下(咱们能够参考下,理解下原理):
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>标题</title> </head> <body> <div id="app"> <input type="text" v-model='count' /> <input type="button" value="增长" @click="add" /> <input type="button" value="减小" @click="reduce" /> <div v-bind="count"></div> </div> <script type="text/javascript"> class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$methods = options.methods; this._binding = {}; this._observer(options.data); this._compile(this.$el); } _pushWatcher(watcher) { if (!this._binding[watcher.key]) { this._binding[watcher.key] = []; } this._binding[watcher.key].push(watcher); } /* observer的做用是可以对全部的数据进行监听操做,经过使用Proxy对象 中的set方法来监听,若有发生变更就会拿到最新值通知订阅者。 */ _observer(datas) { const me = this; const handler = { set(target, key, value) { const rets = Reflect.set(target, key, value); me._binding[key].map(item => { item.update(); }); return rets; } }; this.$data = new Proxy(datas, handler); } /* 指令解析器,对每一个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的更新函数 */ _compile(root) { const nodes = Array.prototype.slice.call(root.children); const data = this.$data; nodes.map(node => { if (node.children && node.children.length) { this._compile(node.children); } const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 若是是input框 或 textarea 的话,而且带有 v-model 属性的 if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } if (node.hasAttribute('v-bind')) { const key = node.getAttribute('v-bind'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); } if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); } }); } } /* watcher的做用是 连接Observer 和 Compile的桥梁,可以订阅并收到每一个属性变更的通知, 执行指令绑定的响应的回调函数,从而更新视图。 */ class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } update() { this.node[this.attr] = this.data[this.key]; } } </script> <script type="text/javascript"> new Vue({ el: '#app', data: { count: 0 }, methods: { add() { this.count++; }, reduce() { this.count--; } } }); </script> </body> </html>
如上代码咱们来分析下原理以下:
首先他是使用ES6编写的语法来实现的。首先咱们想实现相似vue那要的初始化代码,以下这样设想:
new Vue({ el: '#app', data: { count: 0 }, methods: { add() { this.count++; }, reduce() { this.count--; } } });
所以使用ES6 基本语法以下:
class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$methods = options.methods; this._binding = {}; this._observer(options.data); this._compile(this.$el); } }
Vue类使用new建立一个实例化的时候,就会执行 constructor方法代码,所以options是vue传入的一个对象,它有 el,data, methods等属性。 如上代码先执行 this._observer(options.data); 该 observer 函数就是监听全部数据的变更函数。基本代码以下:
1. 实现Observer对全部的数据进行监听。
_observer(datas) { const me = this; const handler = { set(target, key, value) { const rets = Reflect.set(target, key, value); me._binding[key].map(item => { item.update(); }); return rets; } }; this.$data = new Proxy(datas, handler); }
使用了咱们上面介绍的Proxy中的set方法对全部的数据进行监听,只要咱们Vue实列属性data中有任何数据发生改变的话,都会自动调用Proxy中的set方法,咱们上面的代码使用了 const rets = Reflect.set(target, key, value); return rets; 这样的代码,就是对咱们的data中的任何数据发生改变后,使用该方法从新设置新值,而后返回给 this.$data保存到这个全局里面。
me._binding[key].map(item => {
item.update();
});
如上this._binding 是一个对象,对象里面保存了全部的指令及对应函数,若是发生改变,拿到最新值通知订阅者,所以通知Watcher类中的update方法,以下Watcher类代码以下:
/* watcher的做用是 连接Observer 和 Compile的桥梁,可以订阅并收到每一个属性变更的通知, 执行指令绑定的响应的回调函数,从而更新视图。 */ class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } update() { this.node[this.attr] = this.data[this.key]; } }
2. 实现Compile
以下代码初始化
class Vue { constructor(options) { this.$el = document.querySelector(options.el); this._compile(this.$el); } }
_compile 函数的做用就是对页面中每一个元素节点的指令进行解析和扫描的,根据指令模板替换数据,以及绑定相应的更新函数。
代码以下:
_compile(root) { const nodes = Array.prototype.slice.call(root.children); const data = this.$data; nodes.map(node => { if (node.children && node.children.length) { this._compile(node.children); } const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 若是是input框 或 textarea 的话,而且带有 v-model 属性的 if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } if (node.hasAttribute('v-bind')) { const key = node.getAttribute('v-bind'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); } if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); } }); } }
如上代码,
1. 拿到根元素的子节点,而后让子元素变成数组的形式,如代码:
const nodes = Array.prototype.slice.call(root.children);
2. 保存变更后的 this.$data, 以下代码:
const data = this.$data;
3. nodes子节点进行遍历,若是改子节点还有子节点的话,就会递归调用 _compile方法,以下代码:
nodes.map(node => { if (node.children && node.children.length) { this._compile(node.children); } });
4. 对子节点进行判断,若是子节点是input元素或textarea元素的话,而且有 v-model这样的指令的话,以下代码:
nodes.map(node => { const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 若是是input框 或 textarea 的话,而且带有 v-model 属性的 if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } });
如上代码,若是有 v-model,就获取v-model该属性值,如代码:
const key = node.getAttribute('v-model');
而后把该指令通知订阅者 Watcher; 以下代码:
this._pushWatcher(new Watcher(node, 'value', data, key));
就会调用 Watcher类的constructor的方法,以下代码:
class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } }
把 node节点,attr属性,data数据,v-model指令key保存到this对象中了。而后调用 this._pushWatcher(watcher); 这样方法。
_pushWatcher代码以下:
if (!this._binding[watcher.key]) { this._binding[watcher.key] = []; } this._binding[watcher.key].push(watcher);
如上代码,先判断 this._binding 有没有 v-model指令中的key, 若是没有的话,就把该 this._binding[key] = []; 设置成空数组。而后就把它存入 this._binding[key] 数组里面去。
5. 对于 input 或 textarea 这样的 v-model 会绑定相对应的函数,以下代码:
node.addEventListener('input', () => { data[key] = node.value; });
当input或textarea有值发生改变的话,那么就把最新的值存入 Vue类中的data对象里面去,所以data中的数据会发生改变,所以会自动触发执行 _observer 函数中的Proxy中的set方法函数,仍是同样,首先更新最新值,使用代码:
const rets = Reflect.set(target, key, value);
而后遍历 保存到 this._binding 对象中对应的键;以下代码:
me._binding[key].map(item => {
console.log(item);
item.update();
});
如上,咱们在input输入框输入1的时候,打印item值以下所示:
而后执行 item.update()方法,update方法以下:
class Watcher { update() { this.node[this.attr] = this.data[this.key]; } }
就会更新值到视图里面去,好比input或textarea, 那么 attr = 'value', node 是该元素的节点,key 就是 v-model中的属性值,所以 this.node['value'] = this.data[key];
而后同时代码中若是有 v-bind这样的指令的话,也会和上面的逻辑同样判断和执行;以下 v-bind指令代码以下:
if (node.hasAttribute('v-bind')) { const key = node.getAttribute('v-bind'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); }
而后也会更新到视图里面去,那么 attr = 'innerHTML', node 是该元素的节点,key 也是 v-model中的属性值了,所以 this.node.innerHTML = thid.data['key'];
好比页面中html代码以下:
<div id="app"> <input type="text" v-model='count' /> <input type="button" value="增长" @click="add" /> <input type="button" value="减小" @click="reduce" /> <div v-bind="count"></div> </div>
实列化代码以下:
new Vue({ el: '#app', data: { count: 0 }, methods: { add() { this.count++; }, reduce() { this.count--; } } });
所以上面的 node 是 <input type="text" v-model='count' /> input中的node节点了,所以 node.value = this.data['count']; 所以 input框的值就更新了,同时 <div v-bind="count"></div> 该节点经过 node.innerHTML = this.data['count'] 这样的话,值也获得了更新了。
6. 对于页面中元素节点带有 @click这样的方法,也有判断,以下代码:
if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); }
如上代码先判断该node是否有该属性,而后获取该属性的值,好比html页面中有 @click="add" 和 @click="reduce" 这样的,当点击的时候,也会调用 this.$methods[methodName].bind(data)中对应 vue实列中对应的函数的。所以也会执行函数的,其中data 就是this.$data,监听该对象的值发生改变的话,一样会调用 Proxy中的set函数,最后也是同样执行函数去更新视图的。如上就是使用proxy实现数据双向绑定的基本原理的。