Vue2.x | 响应式原理

522 阅读5分钟

前言

Vue最独特的特性之一,是非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。当你修改它们的时候,视图也会进行更新。——官方文档

当你在使用Vue.js开发的时候,你是否有想过,为什么我们一定要把数据定义在data选项里?Vue.js是怎么实现响应式数据?为什么当我们改变data里的数据,视图就会自动的重新渲染,让我们看到改变后的数据。相信看完这篇文章你会对Vue的响应式原理会有个深刻的理解。

响应式原理

翻开Vue官网,我们会找到官方给出的原理解释,让我们来一起再看一遍。

流程如下:

当一个JavaScript对象作为Vuedata项被传入的时候,Vue会遍历该对象。并使用Object.defineProperty把每个属性转为getter/setter。就是把data里的数据转为图上紫色的部分。

Vue使用getter/setter,在内部追踪收集的依赖。在属性被访问或修改的时候,发出变更通知。

每个组件中都有一个watcher,在组件渲染的时候,watcher会把每个touch过的数据属性记录依赖,也就是会触发getter然后收集依赖(Collect as Dependency)。在属性被setter的时候,由依赖dep通知watcher,重新渲染关联的组件。

以上是响应式原理的主要过程,可能会暂时不理解。但是没关系,我们会接着去实现一个响应式系统,去感受它的原理。

实现一个简易的Vue

首先,定义一个Vue类。我们在使用Vue的时候,都会先将需要侦测的数据放到data选项里。所以我们传入一个options,获取里面的data数据。

class Vue {
  constructor(options) {
    this._data = options.data;
  }
}

接着就是要将这些数据转为响应式的,也就是getter/settter。如何转换呢?上面官方文档告诉我们使用的是Object.defineProperty。由于ES6在浏览器支持度不是很理想,Vue一直使用的是ES5的API。但是在Vue3.0中将会使用ES6的Proxy

在这里我们暂时不考虑数组的情况。因为数组的响应式的实现方式和对象的方式不同。

class Vue {
  constructor(options) {
    this._data = options.data;
    // 新增
    new Observer(this._data);
  }
}

class Observer {
  constructor(value) {
    if (!Array.isArray(value)) {
      this.walk(value);
    }
  }
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

以上代码中,Vue类里新增了一段,new 了一个Observer实例。接着将data传入这个实例,它的作用就是为了遍历data里的所有属性,并且将属性转为getter/setter。转换方法将会写在defineReactive里。

function defineReactive(target, key, val) {
  // 递归,子属性
  if (typeof value === 'object') {
    new Observe(val);
  }

  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    set: (value) => {
      if (val === value) {
        return;
      }
      val = value;
    },
    get: () => {
      return val;
    },
  });
}

在上面代码中,我们先判断val是否是object类型,因为data里的属性的子属性可能是对象。然后使用Object.defineProperty劫持数据,这样我们就可以在数据被访问和修改的时候监听到这些操作。

注意:Object.defineProperty不能侦测到属性的添加和删除。Vue提供给我们两个方法Vue.$setVue.$delete来解决。

当然光做到监听操作还是不行的,我们还需要知道,这个数据被哪些组件或模板使用了。所以我们要收集到这些依赖,并在数据发生改变的时候通知这些依赖。这个过程是通过Dep来管理的。它的作用就是将wathcer添加到Dep中,在需要的时候调用watcher中的update方法更新视图。所以需要"添加"和"发送通知"的方法,下面来实现。

class Dep {
  constructor() {
    this.subs = [];
  }
  depend() {
    // window.target用来存储当前的Watcher对象的实例
    this.subs.push(window.target);
  }
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

接着我们在defineReactvie中把依赖收集到dep中。并且在setter的时候调用dep.notify()通知变更。

function defineReactive(target, key, val) {
  // 新增
  const dep = new Dep();
  // 递归,子属性
  if (typeof value === 'object') {
    new Observe(val);
  }

  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    set: (value) => {
      if (val === value) {
        return;
      }
      val = value;
      // 新增
      dep.notify();
    },
    get: () => {
      // 新增
      dep.depend();
      return val;
    },
  });
}

以上新增了3处,分别是new了一个Dep,来存储依赖。第二处在set的时候通知视图更新,第三处把当前window.target添加到依赖中。这个window.target到底是啥呢?其实它就是一个watcher

wathcer的作用是响应数据的变化,然后提供更新视图的方法。

class Watcher {
  constructor() {
    // 存储当前Watcher对象的实例
    window.target = this;
  }
  update() {
    console.log('视图更新了');
  }
}

再来完善我们的Vue

class Vue {
  constructor(options) {
    this._data = options;
    new Watcher();
    new Observer(this._data);
  }
}

以上代码在Vue类中实例化了一个Watcher方法,在实例化的时候window.traget就是当前的Watcher实例,在defineReactive方法里,会把当前的Watcher实例添加到Dep中。这样就完成了依赖收集。

最后来验证一下我们的简易Vue能否跑起来

const vm = new Vue({
  num: 10,
});

vm._data.num; // 先调用一下get,完成依赖收集
vm._data.num = 20; // 被set监听到,Dep派发通知,通知watcher更新视图
// '视图更新了'

总结

到这里,一个简易的Vue类已经完成了,虽然代码很简单,但是这种思想我们要掌握。

我们再来回顾一下整个流程

首先我们定义了一个Vue类来接收options参数。然后在这个Vue类里通过实例化一个Observer去侦测data里的数据,。

Observer类里接收data,并循环datakey,将每个属性传给defineReactive让它来实现getter/setter的转换。

defineReactive中需要收集依赖,所以我们定义一个Dep类,然后将依赖存储在Dep类中。接着,我们通过Object.defineProperty劫持数据,然后在get操作的时候将依赖添加到Dep中,set操作的时候,通知Dep中的所有依赖更新视图。

所谓的依赖就是WatcherWatcher的作用就是响应数据的变化然后更新视图。