重学vue系列--响应式篇

231 阅读2分钟

由于项目变更,技术栈,需要从React转vue,于是决定将学习vue的过程记录下来,加深对框架的理解

前提

如果你对Vue原理或者源码比较了解,可以直接跳过

目标

本章节的目标,彻底弄懂vue的响应式是如何实现的,Observer、Dep、Watcher之间的关系是什么

如何追踪对象的变化

这里的对象,是指Plain Object,下文会提到数组如何监测变化。看过Vue官方文档的话,我们都知道,vue 3.x以前,是通过Object.defineProperty将数据对象变成响应式。定义函数defineReactive


/**
 * 将对象的属性变成响应式
 * @param {object} data 
 * @param {string} key 
 * @param {any} val 
 */
function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      return val;
    },
    set(newVal) {
      val = newVal;
    }
  });
}

每当访问data[key]时,get函数都会被触发一次,每当修改data[key]的值时,set函数都会被触发一次,我们可以在get函数里收集所有的依赖,然后在set函数里通知这些依赖

依赖收集怎么做

我们先假设依赖是一个函数,保存在window.target上。此时,defineReactive函数修改如下:

/**
 * 将数据转换成响应式数据
 * @param {object} data
 * @param {string} key
 * @param {any} val
 */
function defineReactive(data, key, val) {
  // 新增代码
  const dep = [];
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 新增代码
      dep.push(window.target);
      return val;
    },
    set(newVal) {
      val = newVal;
      // 新增代码
      for (let i = 0; i < dep.length; i++) {
        dep[i](newVal, val);
      }
    },
  });
}

在上面这段代码里,新增了dep,存放所有依赖此属性的相关函数,在get函数中pushdep数组中,在set函数里触发所有的函数。为了使代码复用,我们将dep相关代码抽离成单独的类,如下所示:

/**
 * 定义Dep类
 */
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  depend() {
    if (window.target) {
      this.addSub(window.target);
    }
  }
  notify(newValue, value) {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i](newValue, value);
    }
  }
}

/**
 * 将数据转换成响应式数据
 * @param {object} data
 * @param {string} key
 * @param {any} val
 */
function defineReactive(data, key, val) {
  // 改动
  // const dep = [];
  const dep = new Dep();
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 改动
      // dep.push(window.target);
      dep.depend();
      return val;
    },
    set(newVal) {
      val = newVal;
      // 改动
      // for (let i = 0; i < dep.length; i++) {
      //   dep[i](newVal, val);
      // }
      dep.notify(newVal, val);
    },
  });
}

window.target是什么

这里的window.target代表了多种读取数据的函数,比如模板渲染函数,比如选项watch里面的每一项,我们把这种函数或者对象,统称为Watcher,下面以$watch实例方法举例说明

$watch背后的逻辑

比如下面这段代码:

vm.$watch('a.b', function (newValue, oldValue) {
  // do something
});

/**
 * 先简单定义一下,只解析以.相连的字符串
 * @param {string} path
 */
function parsePath(path) {
  const segments = path.split('.');
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) {
        return;
      } else {
        obj = obj[segments[i]];
      }
    }
    return obj;
  };
}

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }
  get() {
    window.target = this;
    const value = this.getter(this.vm);
    window.target = undefined;
    return value;
  }
  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

/**
 * 定义Dep类
 */
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  depend() {
    if (window.target) {
      this.addSub(window.target);
    }
  }
  notify(newValue, value) {
    for (let i = 0; i < this.subs.length; i++) {
      // 改动
      // this.subs[i](newValue, value);
      this.subs[i].update();
    }
  }
}

上面这段代码可以看出,当执行$watch时,就是实例化了一个Watcher,在Watcher对象的构造函数里对数据进行了读取,读取之前将自身赋值给window.target,这样的话,dep.depend就将该watcher对象加入到了依赖数组中。值读取完毕后,将window.target设置为undefined。当a.b的值发生变更时,dep.notify就会通知到该watcher对象,即执行它的update方法,在watch对象的update方法内部,会再次读取到最新的值,并最后执行回调函数this.cb

递归所有的key

我们将一个数据内所有的属性变成响应式的的过程,抽离成一个单独的类,称之为Observer

class Observer {
  constructor(value) {
    this.value = value;
    if (!Array.isArray(value)) {
      this.walk(value);
    }
  }
  walk(data) {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]]);
    }
  }
}

/**
 * 将数据转换成响应式数据
 * @param {object} data
 * @param {string} key
 * @param {any} val
 */
function defineReactive(data, key, val) {
  // 递归子属性
  if (typeof val === 'object') {
    new Observer(val);
  }
  
  const dep = new Dep();
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      
      dep.depend();
      return val;
    },
    set(newVal) {
      val = newVal;
      dep.notify(newVal, val);
    },
  });
}

Array的变化监测

前面的代码,针对的是object的变化监测,下面将讲解如何实现Array的变化监测。

假如我们往数组中新增元素

this.todoList.push({
    text: '喝3杯水'
});

如果不对数组做特殊处理,上面这段代码并不会触发setter,因为并未改变this.todoList的引用,唯一的方法就是拦截array常见的api(push、pop、shift、unshift等),在调用原始api之前做些处理,达到监测的目的。于是,我们创建一个和Array.prototype一样的对象

const originProto = Array.prototype;
const newArrayProto = Object.create(originProto);
const methods = [
  'push',
  'pop',
  'splice',
  'shift',
  'unshift',
  'sort',
  'reverse',
];
methods.forEach((method) => {
  const original = originProto[method];
  Object.defineProperty(newArrayProto, method, {
    value: function (...args) {
      return original.apply(this, args);
    },
    writable: true,
    enumerable: false,
    configurable: true,
  });
});

class Observer {
  constructor(value) {
    this.value = value;
    if (!Array.isArray(value)) {
      this.walk(value);
    } else {
      // 新增
      value.__proto__ = newArrayProto;
    }
  }
  walk(data) {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]]);
    }
  }
}



针对value是数组的情况,在收集依赖时,需要做些改动。为了能在newArrayProto的每个属性里访问到依赖数组,需要将依赖数组挂在数组上。


function observe(value) {
  if (typeof value !== 'object') {
    return;
  }
  if (value.__ob__ && value.__ob__ instanceof Observer) {
    return value.__ob__;
  } else {
    return new Observer(value);
  }
}

/**
 * 将数据转换成响应式数据
 * @param {object} data
 * @param {string} key
 * @param {any} val
 */
function defineReactive(data, key, val) {
  // if (typeof val === 'object') {
  //   new Observer(val);
  // }

  const childOb = observe(val);

  const dep = new Dep();
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      dep.depend();
      // 改动
      if (childOb) {
        childOb.dep.depend();
      }

      return val;
    },
    set(newVal) {
      val = newVal;
     
      dep.notify(newVal, val);
    },
  });
}

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    writable: true,
    value: val,
    enumerable: !!enumerable,
    configurable: true,
  });
}
class Observer {
  constructor(value) {
    // 改动
    this.dep = new Dep();
    def(value, '__ob__', this);

    this.value = value;
    if (!Array.isArray(value)) {
      this.walk(value);
    } else {
      // 新增
      value.__proto__ = newArrayProto;
    }
  }
  walk(data) {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]]);
    }
  }
}

const originProto = Array.prototype;
const newArrayProto = Object.create(originProto);
const methods = [
  'push',
  'pop',
  'splice',
  'shift',
  'unshift',
  'sort',
  'reverse',
];
methods.forEach((method) => {
  const original = originProto[method];
  Object.defineProperty(newArrayProto, method, {
    value: function (...args) {
      // 改动
      const ob = this.__ob__;
      const result = original.apply(this, args);
      // 改动
      ob.dep.notify();
      return result;
    },
    writable: true,
    enumerable: false,
    configurable: true,
  });
});

通过以上代码,当对数组进行push、pop、splice、shift、unshift、sort、reverse等操作时,可以监测到数组的变化并作出响应。

除此之外,当数组里每个数组项发生变化时,也需要监测。通过修改Observer代码,将数组项进行响应式化

class Observer {
  constructor(value) {
    // 改动
    this.dep = new Dep();
    def(value, '__ob__', this);

    this.value = value;
    if (!Array.isArray(value)) {
      this.walk(value);
    } else {
      // 新增
      value.__proto__ = newArrayProto;
      this.observeArray(value);
    }
  }
  walk(data) {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]]);
    }
  }

  observeArray(items) {
    for (let i = 0; i < items.length; i++) {
      observe(items[i]);
    }
  }
}

另外,对于数组新增的元素,需要将它们也响应式化,于是增加如下代码:

const originProto = Array.prototype;
const newArrayProto = Object.create(originProto);
const methods = [
  'push',
  'pop',
  'splice',
  'shift',
  'unshift',
  'sort',
  'reverse',
];
methods.forEach((method) => {
  const original = originProto[method];
  Object.defineProperty(newArrayProto, method, {
    value: function (...args) {
     
      const ob = this.__ob__;
      const result = original.apply(this, args);
     
      ob.dep.notify();
      // 改动
      let newItems;
      switch (method) {
        case 'push':
        case 'unshift': {
          newItems = args;
          break;
        }
        case 'splice': {
          newItems = args.slice(2);
          break;
        }
      }
      if (newItems) {
        ob.observeArray(newItems);
      }
      return result;
    },
    writable: true,
    enumerable: false,
    configurable: true,
  });
});