Vue2响应式原理详解

695 阅读4分钟

首先自己也是有看过很多的文章,挺多来回cv的,所以也不太理解。但是后来串一下突然就顿悟了。接下来带各位一步步去实现。

首先下面是最基本的一个封装操作。get返回值,set更改值。

// 最初版
function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
    },
  });
}

这就是一个很简单的封装,目前没啥功能。 那么接下来就是要收集依赖了,我们都知道 get收集依赖 (get默认取值会触发监听,知道哪里在使用当前的值,所以在get当中收集依赖),set触发依赖(也就是触发收集依赖的地方的数据的更新方法也就是对应的update方法)。

大家都知道data当中数据都是响应式的,比如说在template当中的某个地方使用了data当中的属性,所以当数据变化了,要告诉template里面用到了该数据的地方。

收集依赖,我们首先肯定是想到的是数组

function defineReactive(data, key, val) {
  let dep = []; // 新增
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.push(window.target); // 新增
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }
      // 新增
      for (let i = 0; i < dep.length; i++) {
        dep[i](newVal, val);
      }
      val = newVal;
    },
  });
}

肯定有人会看不明白这个window.target这个东西,先别着急,到最后的时候会串一遍,那个时候就是有值的,并且会指向当前的一个watcher实例对象,放在window身上是为了能够全局访问。(以下简称 问题一

虽然此时功能是能够实现的,但是后续还会有一些删除和更新等等的方法,所以都写在 defineReactive 这里面肯定是不行的,以下对于Dep类做出了一个简单的封装操作。

export default class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  removeSub(sub) {
    remove(this.subs, sub);
  }

  depend() {
    if (window.target) {
      this.addSub(window.target);
    }
  }

  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

然后呢,再去更改一下之前的代码,使用一下我们的封装类Dep,这样看起来就是非常的简洁了

function defineReactive(data, key, val) {
  let dep = new Dep(); // 修改
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend(); // 修改
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
      dep.notify(); // 新增
    },
  });
}

那么现在说一下window.target,首先我们要知道,依赖是要去执行响应的update()方法的,需要有一个东西来帮助我们去做这个事情,对于每一个data都会有一个watcher实例对象去监视他,并触发这个data所对应的数据更新方法 update

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    // 执行this.getter(),就可以读取data.a.b.c的内容
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    window.target = this;                           // 精髓一
    let value = this.getter.call(this.vm, this.vm); // 精髓二
    window.target = undefined;
    return value;
  }

  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

上面这行代码是精髓所在,建议将上面的代码截个图置顶方便理解。

下面的这段话是最主要的东西,也会将问题一解决。

精髓一:首先this是当前对象的watcher实例,window.target = this; 将window.target赋值为当前的watcher实例对象

精髓二:当调用getter方法的时候会默认读取这个对象的属性值,那么就会触发Object.defineProperty当中的get方法,get当中会触发dep.depend(),而精髓一将this赋值给了window.target然后在dep.depend()当中需要用到window.target,此时如果是响应式对象那么window.target必定是一个watcher实例

那么就会将它添加到当前data对象对应的dep数组当中,也就是添加了一个watcher实例进去,当数据变化时,调用watcher对象的update方法去执行更新方法,当依赖收集结束后,将window.target赋值为undefined

没看懂的多串一下代码之间的联系

/**
* Observer类会附加到每一个被侦测的object上。
 * 一旦被附加上,Observer会将object的所有属性转换为getter/setter的
形式
* 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
*/
export class Observer {
  constructor(value) {
    this.value = value;

    if (!Array.isArray(value)) {
      this.walk(value);
    }
  }

  /**
   * walk会将每一个属性都转换成getter/setter的形式来侦测变化
   * 这个方法只有在数据类型为Object时被调用
   */
  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]]);
    }
  }
}

function defineReactive(data, key, val) {
  // 新增,递归子属性
  if (typeof val === "object") {
    new Observer(val);
  }
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }

      val = newVal;
      dep.notify();
    },
  });
}

以下是一个Vue类,也就是 main.js/ts 当中的createApp(App),App是一个入口文件,由此经过Vue类的监听,数据变为了响应式的数据。

        class Vue {
            constructor(options = {}) {
                this.el = options.el
                this.exp = options.exp
                this.data = options.data
                el.innerHTML = this.data[this.exp] //初始化页面内容
                let observer = new Observer()
                observer.defineReactive(this.data) //监听数据
                new Watcher(this, this.exp, function(val) { //创建watcher实例,调用构造函数。
                    el.innerHTML = val
                })
                return this
            }
        }

在new Watcher的时候会调用对应数据的get方法,而 observer.defineReactive(this.data) 目的是添加对应的set,get方法。由此与上部分也联系了起来。也就是精髓二的部分。