vue进阶之路 —— 详解vue2.x到vue3.0数据响应式原理变化

在本篇文章中,主要为大家分享从vue2.x到vue3.0数据响应式的原理变化。谈到数据响应,必然绕不开数据的双向绑定,那么什么是数据的双向绑定呢?

vue2.x中是通过Object.defineProperty实现数据的双向绑定的,这个方法有一个缺陷:在一个对象的访问器属性中不能直接操作它的数据属性,也就是说无法给现有的数据属性设置访问器属性。

var a = {
  b: 123,
  c: 456
}

Object.defineProperty(a, "b", {
  get: function() {
    console.log('you get b');
    return this.b;
  },
  set: function(newVal) {
    console.log('you set b');
    this.b = newVal;
  }
});

调用a.b在控制台持续输出‘you get b‘,最后抛出异常
vue进阶之路 —— 详解vue2.x到vue3.0数据响应式原理变化_第1张图片
给a.b赋值也是同样的效果。
那怎么解决这个问题呢?
1,单独给对象a创建一个额外的访问器属性d,例如

var a = {
  b: 123,
  c: 456
}

Object.defineProperty(a, "d", {
  get: function() {
    console.log('you get b');
    return this.b;
  },
  set: function(newVal) {
    console.log('you set d');
    this.b = newVal;
  }
});

通过访问器属性d来获取和设置b的值。

2,提前将a.b拿出来赋给一个单独的变量,然后在get和set方法中操作这个变量。

接下来用一个简单的示例演示vue2.x中是如何通过Object.defineProperty实现数据的双向绑定。

新建一个vue_test.js文件,为了简单,就不在Vue的构造函数中传递options参数了,直接写死data值。在observe函数中对data进行遍历,如果遍历到属性为object类型,则回调observe,否则调用Object.defineProperty为data的每一个属性值设置get和set方法。在get方法中先不讨论依赖收集,只是简单的返回属性值;在set方法中更新属性值,并且重新渲染。

为了克服上述提到的Object.defineProperty实现数据双向绑定的缺陷,代码在observe方法中定义了一个额外的中间变量value,看起来有点冗余。这是因为Object.defineProperty的核心并不是给一个对象做双向数据绑定,而是给对象做属性标签。vue2.x中的这一设计缺陷已经在vue3.0中改用Proxy来进行修复。

function Vue() {
  this. el = document.getElementById("app");
  this.$data = {
    a: 55
  },
  this.observe(this.$data);
  this.render();
}

Vue.prototype.observe = function(obj) {
  var value;
  var self = this;
  for (var key in obj) {
    value = obj[key];
    if (typeof value === 'object') {
      this.observe(value)
    } else {
      Object.defineProperty(this.$data, key, {
        get: function() {
          return value;
        },
        set: function(newVal) {
            value = newVal;
            self.render();
        }
      });
    }
  }
};

Vue.prototype.render = function() {
  // 真正的vue里面先会用模版引擎解析模版语法
    this.el.innerHTML = 'the value of a is ' + this.$data.a;
}

再新建一个index.html,3s之后将data的a属性值变为88。在浏览器Chrome中可以看到演示效果,大家可以自己动手尝试一下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
<script type="text/javascript" src="vue_test.js"></script>
<script>
    var vm = new Vue();
    setTimeout(() => {
        vm.$data.a = 88;
    }, 3000);
</script>
</body>
</html>

接下来结合vue2.6.10源码为大家讲解依赖收集和virtualDom。

在源码中,函数defineReactive$$1为data的每一个属性定义get和set方法,完成数据的双向绑定。

function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

Vue2.x数据响应式原理:定义依赖收集对象dep,在get部分调用dep.depend收集依赖,在set部分调用dep.notify更新依赖。virtualDom在vue中表现为抽象语法树ast,包含子节点,本质就是一个json对象,外面包裹了一层观察者模式,dep.notify只需要触发observe,由observe触发ast进行更新。

在vue中,给数组下标赋值触发不了数据更新,只有用 ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’ 才可以,这是什么原因呢?
因为在源码中利用装饰者模式重写了数组原型中的这些方法

var arrayProto = Array.prototype;
  var arrayMethods = Object.create(arrayProto);

  var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];

  /**
 * Intercept mutating methods and emit events
   */
  methodsToPatch.forEach(function (method) {
    // cache original method
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      ob.dep.notify();
      return result
    });
  });

vue3.0将Object.defineProperty替换为es6的Proxy,在目标对象之上架了一层拦截,修改某些操作的默认行为。
具体有什么好处呢?

  • 可以对整个对象进行监听,省去了for…in循环提升效率
  • 省去额外的中间存储量
  • 可以监听数组,不用再单独对数组做特异性操作

用Proxy替换上例中observe函数中的Object.defineProperty,运行结果不变,但是代码量减少,逻辑清晰好多。

Vue.prototype.observe = function(obj) {
  var self = this;
  this.$data = new Proxy(this.$data, {
    get: function(target, key) {
      return target[key];
    },
    set: function(target, key, value) {
      target[key] = value;
      self.render();
    }
  });
};

Proxy除了做双向数据绑定,还能做什么呢?

  • 类型校验
  • 真正的私有变量

1,类型校验的代码示例如下,类似于vue中父组件传值到子组件,可以对赋值类型进行校验。

// 类型校验对象
var validator = {
    name(value) {
        return typeof value === 'string';
    },
    age(value) {
        return typeof value === 'number' && value >= 18;
    }
}

// 类型校验函数
function createValidator(target, validator) {
    return new Proxy(target, {
        _validator: validator,
        set: function(target, key, value, receiver) {
            if (target.hasOwnProperty(key)) {
                var validator = this._validator[key];
                if (validator(value)) {
                    Reflect.set(target, key, value, receiver);
                } else {
                    throw Error('type error');
                }
            }
        }
    });
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    return createValidator(this, validator);
}

var person1 = new Person('xiaoming', 20);

如果在控制台给person1的name和age属性赋值不符合校验规则,则会抛出异常
在这里插入图片描述
2,私有变量
在Proxy之前,我们都是通过给对象属性加下划线来表示只能通过对象方法访问的属性,但是这个方法有个缺陷:对象的属性不是完全私有的,通过对象也可以直接访问。
以《 javascript 高级程序设计(第3版)》中的一个例子为例来进行说明

var book = {
    _year: 2004,
    edition: 1 
};
Object.defineProperty(book, "year", {
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
} }
});
book.year = 2005;
alert(book.edition); //2

在控制台可以直接访问book._year

那么怎么利用Proxy将对象属性_year变为完全私有的呢?在原代码的基础上添加Proxy代理对象

var book = {
    _year: 2004,
    edition: 1 
};
Object.defineProperty(book, "year", {
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
} }
});
book = new Proxy(book, {
    get: function(target, key) {
        if (key === '_year') {
            throw Error('Can not get the value of _year');
        }
        return Reflect.get(target, key);
    },
    set: function(target, key, value) {
        if (key === '_year') {
            throw Error('Can not set the value of _year');
        }
        Reflect.set(target, key, value);
    }
});
book.year = 2005;
alert(book.edition); //2

在控制台直接访问book._year会抛出异常,只能通过对象的访问器属性 “year“ 来进行访问。

至此vue2.x到vue3.0数据响应式原理就讲完了,希望对你有所启发。共勉

你可能感兴趣的:(Vue)