vue2.x和vue3.x双向绑定原理的区别,以及vue3.x双向绑定的深入解析

476 阅读3分钟

vue2.x和vue3.x双向绑定原理的区别

vue作为mvvm模式的框架,双向绑定一直是它的一个亮点,在vue2.x的使用过程中都曾遇到过很多坑点,为了避免这些坑又做了许多不必要的操作。那么为什么会有这些问题呢?我们先来看下vue2.x的原理,相比这个有了解的都知道是Object.defineProperty(),这里不多说直接上代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue 2.x双向绑定原理</title>
</head>

<body>
  <!-- 
    Vue 2.x 双向绑定原理,是利用了Object.defineProperty的存取描述符中的get和set
   -->
  <input type="text" id="app">
  <p id="content"></p>
</body>
<script>
  const obj = {};
  let val = '';
  Object.defineProperty(obj, 'value', {
    get() {
      return val;
    },
    set(nVal) {
      val = nVal;
      document.getElementById('content').innerHTML = val;
    }
  });
  document.getElementById('app').oninput = (e) => {
    obj.value = e.target.value;
  }
</script>

</html>

vue2.x的双向绑定是由Object.defineProperty()实现的,缺点也是Object.defineProperty()所带来的,vue3.x采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。 话不多说上代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue 3.x双向绑定原理</title>
</head>

<body>
  <input type="text" id="app">
  <p id="value"></p>
</body>
<script>
  const obj = {};
  const proxyObj = new Proxy(obj, {
    get(target, key) {
      return target[key];
    },
    set(target, key, newVal) {
      target[key] = newVal;
      document.getElementById('value').innerHTML = target[key];
    }
  });
  document.getElementById('app').oninput = (e) => {
    proxyObj[0] = e.target.value;
  }
</script>

</html>

乍看之下只是改了监听的方式,但是使用proxy后提升了数组内部等definepropty的缺点, 除此之外还有以下优点: 1 defineProperty只能监听某个属性,不能对全对象监听 2 可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可) 3 可以监听数组,不用再去单独的对数组做特异性操作

Vue3.x双向绑定原理深入解析

下边我们再来对Vue3.x双向绑定原理深入解析,会对这个发布者和订阅者更加清晰了解,同样话不多说上代码,代码能讲解很多内容:

custom.vue.js 文件

/**
 * 自定义vue.js 深入解析vue3.x如何利用proxy来实现双向绑定原理的
 */
// vue构造函数
class Vue {
  constructor(options) {
    // 挂载节点
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    // 所有订阅者集合,目标格式(一对多的关系)
    this.deps = {};
    // 调用观察者
    this.observer(this.$data);
    // 调用指令解析器
    this.compile(this.$el);
  }
}
// 指令解析器
Vue.prototype.compile = function (el) {
  let els = el.children;
  for (let i = 0; i < els.length; i++) {
    const domEle = els[i];
    if (domEle.children.length) {
      // 递归获取子节点
      this.compile(domEle);
    }
    // 当节点存在v-model指令
    if (domEle.hasAttribute('v-model')) {
      let attrVal = domEle.getAttribute('v-model');
      domEle.addEventListener('input', (() => {
        // 添加一个订阅者
        this.deps[attrVal].push(new Watcher(domEle, "value", this, attrVal));
        let thisElement = domEle;
        return () => {
          // 更新数据层的数据
          this.$data[attrVal] = thisElement.value;
        }
      })());
    }
    if (domEle.hasAttribute('v-html')) {
      let attrVal = domEle.getAttribute('v-html');
      // 添加一个订阅者
      this.deps[attrVal].push(new Watcher(domEle, 'innerHTML', this, attrVal));
    }
    if (domEle.innerHTML.match(/{{([^\{|\}]+)}}/)) {
      //获取插值表达式内容 - 这里针对单个,表达式做例子
      let attrVal = domEle.innerHTML.replace(/[{{|}}]/g, '');
      this.deps[attrVal].push(new Watcher(domEle, "innerHTML", this, attrVal)); //添加一个订阅者
    }
    if (domEle.hasAttribute('v-on:click')) {
      let attrVal = domEle.getAttribute('v-on:click');
      // 将this指向改为this.$data
      domEle.addEventListener('click', this.$methods[attrVal].bind(this.$data));
    }
  }
}

// 定义观察者,2.x和3.x的区别也就是在这一块
Vue.prototype.observer = function (data) {
  const _this = this;
  for (const key in data) {
    _this.deps[key] = [];
  }
  let handler = {
    get (target, property) {
      return target[property];
    },
    set (target, key, value) {
      let res = Reflect.set(target, key, value);
      const watchers = _this.deps[key];
      watchers.map(item => {
        item.update();
      });
      return res;
    }
  }
  this.$data = new Proxy(data, handler);
}

// 定义阅读者
class Watcher {
  constructor(el, attr, vm, attrVal) {
    this.el = el;
    this.attr = attr;
    this.vm = vm;
    this.val = attrVal;
    // 更新视图
    this.update();
  }
}
// 更新视图
Watcher.prototype.update = function () {
  this.el[this.attr] = this.vm.$data[this.val];
}

使用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>深入解析vue3.x双向绑定</title>
  <script src="./custom.vue.js"></script>
</head>

<body>
  <div id="app">
    姓名:
    <input type="text" v-model="username">
    <p v-html="username"></p>
    年龄:
    <input type="text" v-model="age">
    <p><span>{{age}}</span></p>
    <button v-on:click="handler">Click!!!</button>
    <h3>{{msg}}</h3>

  </div>

</body>
<script>
  const vm = new Vue({
    el: '#app',
    data: {
      username: '',
      age: '',
      msg: ''
    },
    methods: {
      handler() {
        console.log(`我是${this.username},我有${this.age}岁`);
        this.msg = `我是${this.username},我有${this.age}岁`;
      }
    }
  })
</script>

</html>

实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。 在数据变动时发布消息给订阅者,触发相应的监听回调。就必须要实现以下几点:

 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
 4、mvvm入口函数,整合以上三者

以上内容来自网络学习和整理,如有问题欢迎讨论