深入理解Proxy 及 使用Proxy实现vue数据双向绑定

936 阅读2分钟

据阮一峰文章介绍:Proxy可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。

如果对vue2.xx了解或看过源码的人都知道,vue2.xx中使用 Object.defineProperty()方法对该对象通过 递归+遍历的方式来实现对数据的监控的,具体了解
Object.defineProperty可以看我上一篇文章(www.cnblogs.com/tugenhua070…). 但是通过上一篇Object.defineProperty文章 我们也知道,当我们使用数组的方法或改变数组的下标是不能重新触发 Object.defineProperty中的set()方法的,因此就做不到实时响应了。所以使用 Object.defineProperty 存在如下缺点:

1. 监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话,需要重新编写数组的方法)。
2. 必须遍历每个对象的每个属性,如果对象嵌套很深的话,需要使用递归调用。

因此vue3.xx中之后就改用Proxy来更好的解决如上面的问题。在学习使用Proxy实现数据双向绑定之前,我们还是一步步来,先学习了Proxy基本知识点。

Proxy基本语法

const obj = new Proxy(target, handler);

参数说明如下:

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
});

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>