Vue--深入响应式原理

使用Vue开发也快半年了,在使用过程中越发的觉得是真的好用,上手简单,看一遍官方文档就可以直接上手了。但是在做了两个项目之后,乘着项目的休息期,觉得自己需要提高一下对于Vue原理的一些了解,其中最感兴趣的就是响应式原理了,于是开始着手进行学习,并把自己学习所收获的和大家分享一下,也希望能和大家一起探讨研究下,共同学习,共同进步!

对象属性

在了解响应式原理之前,必不可少的是要理解对象的属性和setter、getter,这是响应式原理实现的基石。ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。ECMScript中将对象的属性分为了两类:数据属性和访问器属性。那么何为属性?可参考下图:

对象属性

属性值可以由一个或两个方法替代,这两个方法就是getter和setter,由getter和setter定义的属性称之为“存取器属性”。存取器属性不具有value和writable特性,被getter和setter所取代。如下demo:

let test = {
  //普通的数据属性
  w: 0,
  x: 1,
  y: 2,
  //存取器属性定义为一个或两个和属性同名的函数
  get z() {
    return this.x + this.y;
  },
  set z(newVal) {
    const oldVal = this.x + this.y;
    const ratio = newVal / oldVal;
    this.x *= ratio;
    this.y *= ratio;
  }
};
console.log(test.z); // 3
test.z = 6;
console.log(test.x, test.y); // 2 4
test.y = 5;
console.log(test.x, test.z); // 2 7

//{value:0,configurable:true,enumerable:true,writable:true}
Object.getOwnPropertyDescriptor(test, "w");

//{get:/*func*/,set:/*func*/,configurable:true,enumerable:true,writable:true}
Object.getOwnPropertyDescriptor(test, "w");

//操作失败但是不会报错,在严格模式中则抛出类型异常错误
//Uncaught TypeError: Cannot assign to read only property 'w' of object '#<Object>'
Object.defineProperty(test, "w", { writable: false });
test.x = 3;

//属性依然是可以配置的,可通过直接修改value属性进行修改
Object.defineProperty(test,"w",{value:3});
console.log(test.w); //3

// 将x从数据属性修改为存取器属性
Object.defineProperty(test,"w",{get:function(){return Math.random()}});
console.log(test.w); 

对象属性的setter和getter是Vue响应式原理的基础,当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。

发布-订阅模式

响应式原理的代码实现中,其核心设计模式就是发布-订阅模式了。发布-订阅模式又叫做观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。引用一下《JavaScript设计模式与开发实践》一书中对于发布订阅模式的举例:

不论是在程序世界里还是现实生活中,发布—订阅模式的应用都非常之广泛。我们先看一个
现实中的例子。
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼
MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。
但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除
了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决
定辞职,因为厌倦了每天回答 1000 个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在
了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一
样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册,
遍历上面的电话号码,依次发送一条短信来通知他们。

其模式关系如下图所示:
发布-订阅模式
发布-订阅模式的实现流程:
模式流程

Vue的属性变化追踪

当我们理解了对象的属性和发布-订阅者模式后,我们可以开始来看一下在Vue中是如何对属性变化进行追踪的了。多的不说,先上官网的示意图:
这里写图片描述
官网的注解也是很简单

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

相信你看到这些时仍然是一脸懵逼,因为我也是如此。在看了源码之后我画了一张Vue实例化过程中实例data流程图:

Vue的属性变化追踪
在上图中,我们可以看到最关键最终的三个模块是Obsever、Watcher、Dep,这三个模块间互相关联,那么他们具体有什么作用呢?对照之前我们讲的对象的属性和发布-订阅者模式,我们可以这样理解:
1、Obsever的核心作用有:将data的每个属性通过setter跟getter变成响应式数据,每个属性调用Dep将其变成一个发布者;
2、编译html时为每个与数据绑定的相关节点生成一个订阅者Watcher,每个订阅者初始化时需要获取对应属性的值,此时会触发getter,getter中会将Watcher添加到Dep.subs中,其实也就是建立了花名册;
3、属性setter时会调用notify方法通知所有watcher,也就是所有订阅者,watcher从而执行update方法进行视图更新。
大概的逻辑和流程就是这些,接下来就直接深入对源码进行一些了解。

核心代码解析

首先我们来看下如何Observe这块的核心功能

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
        // 如果对象原本用用getter方法则执行
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          console.log(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;
        //通过getter获取新值,如果与当前值相同则return
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return;
        }
        if ("development" !== "production" && customSetter) {
          customSetter();
        }
        //若setter已存在则直接执行
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        //若是新的只则重新进行observe,保证数据响应式
        childOb = !shallow && observe(newVal);
        //dep对象发出消息通知所有watcher
        dep.notify();
      }
    });

关于Dep模块,自己参考源码写了一个简易版的Dep类,以方便大家理解阅读

//发布者,保存订阅者信息,并通知订阅者进行相关操作
class Dep {
  constructor() {
    //存放所有的订阅者
    this.subs = [];
  }

  //添加一个订阅者
  addSub(watcher) {
    this.subs.push(watcher);
  }

  //收集订阅者
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  //通知触发相关依赖者更新
  notify() {
    for (let i = 0, l = this.subs.length; i < l; i++) {
      this.subs[i].update();
    }
  }
}

Watch模块同样自己手写了一个简易版,以方便大家理解阅读

//订阅者,发布者notify时调用订阅者update方法,
//update跟据值是否改变来判定执行回调
class Watcher {
  constructor(vm, expOrFn, cb) {
    // 回调函数
    this.cb = cb;
    //传进来的对象
    this.vm = vm;
    //收集发布者,用于移除监听
    this.newDeps = [];
    //表达式
    this.getter = expOrFn;
    this.value = this.get();
  }
  update() {
    this.run();
  }
  run() {
    var value = this.get();
    var oldValue = this.value;
    this.cb.call(this.vm, value, oldValue);
  }
  get() {
    pushTarget(this);
    const vm = this.vm;
    var value;
    try {
      value = this.vm.data[this.getter];
    } catch (error) {}
    return value;
  }

  addDep(dep) {
    this.newDeps.push(dep);
    dep.addSub(this);
  }
}

function pushTarget(watcher) {
  Dep.target = watcher;
}

看了这些源码,大家可以再参考下上面所画的流程图,就大概了解其原理了。

小结:要想真正理解Vue的深入响应式原理,还是在大概理解其实现原理的基础上再深入源码,方才有所收获。自己水平有限,文中不免有错误之处,还望各位能指正教导,共同学习!

猜你喜欢

转载自blog.csdn.net/u011200023/article/details/80930186