使用Proxy实现vue数据双向绑定


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

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

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

  因此vue3.xx中之后就改用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>

  首先我们想实现类似vue那要的初始化代码,如:new Vue()。因此使用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方法

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

  然后同时代码中如果有 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'];

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.data,监听该对象的值发生改变的话,同样会调用 Proxy中的set函数,最后也是一样执行函数去更新视图的。

  如上就是使用proxy实现数据双向绑定的基本原理。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM