由Vue中三个常见问题引起的深度思考

前言

工做中咱们经过搜索引擎或者官方文档很容易就会知道一个语法怎么使用,可是你知道其中的原理吗?我想有一部分同窗应该作不到清楚的说明其实现原理。众所周知,现在技术更新迭代速度很快,据 Vue 做者尤雨溪表示 Vue3.x 会在今年的下半年发布正式版本,视频地址在这里 VUE CONF 杭州之 3.0 进展 。若是你在使用 Vue 或者正在学习 Vue,那么我建议你观看一遍完整的视频,这可能对你之后接触 3.0 版本有很大帮助。这篇文章是基于 Vue2.x 的版本讲述的。主要是围绕如下三个问题展开讨论。javascript

  • 为何 data 要写成函数,而不容许写成对象?
  • Vue 中常说的数据劫持究竟是什么?
  • Vue 实例中数组改变 length 或下标赋值为何不能更新视图?

Tips

若是你已经掌握了这三个问题的缘由和原理;html

若是你以为你不须要掌握原理会用便可;vue

抖个机灵——请看本文最后一行。java

接下来,咱们就对这三个问题一一解答。react

问题一

为何 data 要写成函数,而不容许写成对象?

想要理解这个问题,咱们首先要知道如下三点。面试

注意要点

  1. data是 Vue 实例上的一个属性。2. 对象是对于内存地址的引用。3. 函数有本身的做用域空间。

第一点无可厚非,data属性附着于 Vue 实例上。算法

第二点,JS 的数据类型分为基本类型和引用类型,基本类型存储在栈内存中,引用类型存储在堆内存中,而且引用类型指向的是栈内存中的堆区地址。下面两个例子能够帮助你清晰地理解这句话。windows

基本类型赋值

var a = 10;
var b = 10;
var c = a;

console.log(a === b); // true

a ++ ;

console.log(a); // 11
console.log(c); // 10
复制代码

这段代码分别给 a、b 赋值 10,a 和 b 是全等的。而后用 a 来初始化 c,那么 c 的值也是 10。但 c 中的 10 与 a 中的是彻底独立的,该值只是 a 中的值的一个副本,此后, 这两个变量能够参加任何操做而相互不受影响。具体位置以下示意图。数组

引用类型赋值

var a = {};
var b = {};
var c = a;

console.log(a === b); // false

a.name = 'Marry';
a.say = () => console.log('Hi Marry!');

console.log(c.name); // 'Marry'
console.log(c.say()); // 'Hi Marry!'
复制代码

上面这段代码。首先声明了a、b两个空对象,而后把 a 赋值给 c。由于对象是对栈内存的地址的引用,因此不一样的对象的地址是不一样的,因此他们不是全等的。接着给 a 新增长属性和方法,c 一样能够拥有此属性和方法,主要是由于 c 和 a 指向堆内存中的同一个地址。其关系图以下示意图所示。app

至于第三点,大多数有 JS 基础的同窗应该都能理解,每一个函数都有本身的做用域。

以上是对三个注意点的说明,那么接下来咱们就以两个例子解释问题一:为何 data 要写成函数,而不容许写成对象?

data 为对象示例代码

function MyCompnent() {}

MyCompnent.prototype.data = {
  age: 12
};

var JackMa = new MyCompnent();
var PonyMa = new MyCompnent();

console.log(JackMa.data.age === PonyMa.data.age); // true

JackMa.data.age = 13;

console.log('JackMa ' + JackMa.data.age + '岁;' + 'PonyMa ' + PonyMa.data.age + '岁'); 
// JackMa 13岁;PonyMa 13岁

复制代码

上面的示例中,咱们建立一个构造函数 MyCompnent,它充当的角色至关于 Vue,在他的原型属性上声明一个data属性,其实也至关于 Vue.$data。接着声明两个引用,改变一个引用的值,另一个引用也跟着改变,这个道理其实和引用类型赋值大同小异。

data 为函数的示例代码

function MyCompnent() {
  this.data = this.data();
}

MyCompnent.prototype.data = function() {
  return {
    age: 12
  }
};

var JackMa = new MyCompnent();
var PonyMa = new MyCompnent();

console.log(JackMa.data.age === PonyMa.data.age); // true

JackMa.data = {age: 13};

console.log('JackMa ' + JackMa.data.age + '岁;' + 'PonyMa ' + PonyMa.data.age + '岁');
// JackMa 13岁;PonyMa 12岁

复制代码

上述代码模拟了 Vue 实例上的data为函数的时候,若是改变一个实例的data属性的值,那么不会影响到另一个实例上的data的值。

Tips

面试过程当中常常会被问到 JS 数据类型问题,若是你只回答基本类型和引用类型可能你只能获得一半分数,可是若是你能把存储位置等要点回答出来而且举例说明,想必是很加分的。

小结

Vue 里面data属性之因此不能写成对象的格式,是由于对象是对地址的引用,而不是独立存在的。若是一个.vue 文件有多个子组件共同接收一个变量的话,改变其中一个子组件内此变量的值,会影响其余组件的这个变量的值。若是写成函数的话,那么他们有一个做用域的概念在里面,相互隔阂,不受影响。

问题二

Vue 中常说的数据劫持究竟是什么?

相信大多数用过或者了解 Vue 的同窗都听过数据劫持,进一步问为何可能你也能答出一二,例如 getter、setter 之类。今天我就系统地和你说一下数据劫持之美。首先咱们先看一看下图。

上图完整的描述了 Vue 运行的机制,首先数据发生改变,就会通过 Data 处理,而后Dep会发出通知(notify),告诉 Watcher 有数据发生了变化,接着 Watcher 会传达给渲染函数跟他说有数据变化了,能够渲染视图了(数据驱动视图),进而渲染函数执行render 方法去更新 VNODE,也就是咱们说的虚拟DOM,最后虚拟DOM根据最优算法,去局部更新须要渲染的视图。这里的 Data 就作了咱们今天要说的事——数据劫持。

想要更深刻地理解如何劫持,咱们就须要看源码实现。

Observer

/** * Vue中的每个变量都是由 Observer 构造函数生成的。 * 细心的你可能会发现,你打印出来任何一个Vue上的引用类型属性,后面都有 __ob__: Observer 的字样。 */
var Observer = function Observer (value) {
  this.value = value;
  // 这里把发布者 Dep 注册了
  this.dep = new Dep();
  // ···
  // 此处调用 walk
  this.walk(value);
};
复制代码
/** * 此处会将 obj 里面的每个值用 defineReactive$$1 处理,而它就是今晚的主角。 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};
复制代码

数据劫持

/** * 这个函数就是数据劫持的根据地,里面为对象重写了 get 和 set 方法以及固有属性 enumerable 等。 * */
function defineReactive$$1 ( obj, key, val, customSetter, shallow ) {
 // ···
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ···
      return value
    },
    set: function reactiveSetter (newVal) {
      // ···
      // 此处最为关键,这个函数的主要做用就是经过 notify 告诉 Watcher 有数据变化了。
      dep.notify();
    }
  });
}
复制代码

Dep

/** * subs 是全部 Watcher 的收集器,类型为数组;notify 实则是调用了每一个Watcher的 update方法 。 */

Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  // ···
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

复制代码

Watcher

/** * 更新视图的最直观的方法就是 Watcher 上的 update 方法 , Dep subs 反复调用 * 这里最终都是调用 run 方法。 */
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

复制代码
/** * run 方法内的 cb 方法创建 Watcher 和 VNode 之间的关联关系,从而引起视图的更新。 */
Watcher.prototype.run = function run () {
  // ···
  if (this.user) {
    // ···
    this.cb.call(this.vm, value, oldValue);
    // ···
  } else {
    this.cb.call(this.vm, value, oldValue);
  }
  // ···
};

复制代码

小结

至此,咱们不但了解了数据劫持的的原理,还知道了谁去劫持,劫持事后作了什么,是谁引起的视图更新等等。是否是对 Vue 的运行机制更明白了一些呢?

问题三

Vue 实例中数组改变 length 或下标赋值为何不能更新视图?

情景再现

上图示例中,为方便调试在 mounted 周期内执行windows.vm = this;week 包含周一到周五五个元素,咱们尝试改变 weeklength 为 3 以及给它下标为 4 的元素赋值一个周八,结果都没有生效。那怎么能够生效呢?请看下图。

主要是由于咱们调用了一个数组的内置方法 push,若是你愿意尝试,你会发现调用数组的 slice 方法是不行的。只要是由于 Vue 提取了数组的能够改变原数组的原生方法,进行了再加工。只有通过 Vue 处理过的方法才有更新视图的能力。下面我将从内置方法和源码的角度给你们说明这个结论。

内置方法

上图中咱们展开 week 数组,发如今第一个 __proto__ 里面内置了 poppush 等多个数组的方法;在第二个 __proto__ 不但有上面几种还有更多其余的方法。因而可知,Vue 是对数组的 Api 进行了劫持。

源码解析

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

/** * Intercept mutating methods and emit events * 此方法主要做用就是遍历数组局部方法,调用的同时去调用 dep 的 notify 通知 Watcher 进而更新视图 */
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); }
    // 这一步相当重要
    ob.dep.notify();
    return result
  });
});

复制代码

小结

到这里咱们知道了,Vue 劫持了数组能够改变原数组的 Api,使得每次调用都会执行 dep.notify() 方法进而去更新视图。

结束语

分享的时光永远这么短暂,但我还要对你说:工做中常常遇到此类问题,但愿咱们多问本身一个为何?去研究它究竟是怎么实现的,掌握设计理念,学习设计思想,而不是仅限于知道如何使用。这样本身才会有更多的成长!

最后,若是你愿意点个赞,这将给我很大的动力!