从源码理解vue2响应式原理

560 阅读15分钟

当我们在Vue中写下this.message = 'Hello World'时,视图的自动更新看似魔法,实则是精密的响应式系统在默默运转。这背后其实是三个核心类的精密协作:Observer、Dep、Watcher。本文将通过源码逐层拆解,带你看懂Vue2响应式系统的运作机制。

写在前面

  很多人会认为数组没办法做响应式(即通过索引修改数组元素)是因为Object.defineProperty 没法对数组做劫持,其实不然。我们看下面这个例子:

const testArr = [1, 2, 3, 4]
for(let i in testArr) {
  let val = testArr[i]
  Object.defineProperty(testArr,i,{
    enumerable: true,
    configurable: true,
    get() {
        return val
    },
    set(newVal) {
      if(val === newVal) {
          return
      }
      val = newVal
      console.log('set')
    }
  })
}
testArr[2] = 5
console.log(testArr)

// 输出
// 'set'
// [1, 2, 5, 4]

  可以看到遍历数组索引绑定劫持是可以做到的,那么为什么 vue2 里修改索引下标确无法实现响应式处理呢?对原理不清楚的同学可以跟着我把响应式原理过一遍,知道的同学可以翻到最后看看我的理解。

源码分析路线图

src/core/observer/
├── index.js    # 数据的初始化
├── Observer    # 观察者类
|—— Dep         # 依赖管理
├── Watcher     # 更新触发器

你将看到:

  • 如何通过Object.defineProperty添加观察标记
  • 数组和对象如何实现响应式
  • Dep 如何管理当前 Watcher
  • 不同类型 Watcher 如何进行收集和更新

因为源码内容较多,我会挑些重要的步骤贴出来。

1.data数据的初始化——initData

在这个函数中主要做了三件事:

  • 检查 data 上的属性不能和 props 、methods 上的属性相同。
  • 通过 proxy 把 vm._data.xxx 的访问代理到 vm.xxx 上。
  • 调用 observe 把 data 上的数据变成响应式。
function initData(vm) {
    var data = vm.$options.data;
    data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
    //data不是对象就初始化为空对象
    if (!isPlainObject(data)) {
        data = {};
        ...
    }
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    // 判断去重, data 上的属性不能和 props、methods 上的属性相同。
    while (i--) {
        var key = keys[i];
        if (process.env.NODE_ENV !== 'production') {
            if (methods && hasOwn(methods, key)) {
                warn$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);
            }
        }
        if (props && hasOwn(props, key)) {
            process.env.NODE_ENV !== 'production' &&
                warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +
                    "Use prop default value instead.", vm);
        }
        // 代理数据到 vm 实例上
        else if (!isReserved(key)) {
            proxy(vm, "_data", key);
        }
    }
    // 响应式操作
    var ob = observe(data);
    ob && ob.vmCount++;
}

  第一点我这里就不讲了,比较好理解,就是检查 data 上和 props、methods 上的属性是否相同,相同就报错。这里主要讲一下第二点。通过 proxy 把 vm._data.xxx 的访问代理到 vm.xxx 上。
  proxy具体实现如下:

var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};
function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
      return this[sourceKey][key]; // 实际访问 this._data.xxx
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
      this[sourceKey][key] = val; // 实际修改 this._data.xxx
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

  proxy 接受三个参数target:vm实例,sourceKey:_data,key:data 上属性的key。这段代码的主要作用是通过 Object.defineProperty 把 vm 实例的 key 属性代理到特定数据源(如_data或_props)上,当开发者访问 this.message 时,通过代理实际访问的是 this._data.message。这也就是为什么我们把数据定义在data上,却能直接通过 this.xxx 访问到数据的原因。

2.创建观察者实例——Observer

  首先我们在inintData中调用了observe(data),这个函数就是创建观察者实例。看下这个函数的具体实现

function observe(value, shallow) {
  // 非对象和已经是响应式和 VNode 实例不做响应式处理
  if (!isObject(value) || isRef(value) || value instanceof VNode) {
      return;
  }
  var ob;
  // 如果 value 对象上存在 __ob__ 属性,则表示已经做过观察了,直接返回 __ob__ 属性
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
  }
  else if (shouldObserve &&
    !isServerRendering() &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value.__v_skip) {
    // 创建观察者实例
    ob = new Observer(value, shallow);
  }
  return ob;
}

  observe 就是给非 VNode 的对象类型创建观察者实例 Observer,如果已观察成功,直接返回已有的观察者,否则创建新的实例。
   Observer 类是响应式系统的核心,它通过递归遍历 data 对象,为每个属性创建一个 Dep 实例,并给每个属性添加一个 getter 和 setter。我们先来看看 Observer 类的构造函数。

var Observer = (function () {
  function Observer(value, shallow) {
    if (shallow === void 0) { shallow = false; }
    this.value = value;
    this.shallow = shallow;
    // 依赖存放的数组
    this.dep = new Dep();
    this.vmCount = 0;
    // 在 value 对象上设置 __ob__ 属性,引用了当前 Observer 实例
    def(value, '__ob__', this);
    if (isArray(value)) {
      // var hasProto = '__proto__' in {};
      // 覆盖数组默认的七个原型方法,以实现数组响应式
      if (hasProto) {
        protoAugment(value, arrayMethods);
      }
      else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      if (!shallow) {
        this.observeArray(value);
      }
    }
    else {
      this.walk(value, shallow);
    }
  }
  Observer.prototype.walk = function (obj, shallow) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      var key = keys[i];
      defineReactive(obj, key, NO_INIITIAL_VALUE, undefined, shallow);
    }
  };
  Observer.prototype.observeArray = function (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };
  return Observer;
}());

  Observer类构造函数主要做四件事:

  1. 在实例上定义了dep属性,用来存放依赖。
  2. 设置观测标识 def(value, '$\__ob__', this) 通过__ob__属性标记已观测对象,实现观测实例复用。
  3. 对于数组类型,它覆盖了数组的七个原型方法,以实现数组响应式。同时,当shallow=false(默认)它也调用了observeArray方法,递归遍历数组的每一项,为对象类型创建观察者实例。
  4. 对于对象类型,它调用了walk方法,遍历对象的每一个属性,为每个属性创建一个响应式属性。

  接下来详细分析下如何给数组和对象添加响应式处理

2.1 给数组添加响应式处理

  我们先回顾下对数组处理方法的实现

if (isArray(value)) {
  // var hasProto = '__proto__' in {};
  // 覆盖数组默认的七个原型方法,以实现数组响应式
  if (hasProto) {
    protoAugment(value, arrayMethods);
  }
  else {
    copyAugment(value, arrayMethods, arrayKeys);
  }
  if (!shallow) {
    this.observeArray(value);
  }
}

  1. 先判断 value 是否有 __proto__ 属性,如果有,则调用protoAugment方法。这个方法的作用就是将元素本身的数组方法替换掉,替换成传进来的arrayMethods。

function protoAugment(target, src) {
  target.__proto__ = src;
}

  2. 如果没有 __proto__ 属性,则调用copyAugment方法。这个方法的作用就是将arrayMethods上的方法代理到 value 上去。

function copyAugment(target, src, keys) {
    for (var i = 0, l = keys.length; i < l; i++) {
        var key = keys[i];
        def(target, key, src[key]);
    }
}

  不论是protoAugment还是copyAugment,都是为了给数组元素添加上arrayMethods方法,接下来就详细看下这个方法干了啥。

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];
methodsToPatch.forEach(function (method) {
    // cache original method
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator() {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        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);
        // notify change
        if (process.env.NODE_ENV !== 'production') {
            ob.dep.notify({
                type: "array mutation" /* TriggerOpTypes.ARRAY_MUTATION */,
                target: this,
                key: method
            });
        }
        else {
            ob.dep.notify();
        }
        return result;
    });
});

var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
  1. 首先定义了 arrayMethods 为 Object.create(arrayProto) ,也就是继承了数组的原型链

  2. 定义了 methodsToPatch ,这个数组就是我们要拦截的数组方法,如:push、pop、shift、unshift、splice、sort、reverse等。

  3. 遍历 methodsToPatch ,为数组的每一个方法添加代理。原理还是使用 Object.defineProperty ,但是这里我们使用 def ,这个方法就是对 Object.defineProperty 的封装。所以当我们使用数组方法时,会调用到我们添加的代理方法。这个方法里主要做了三件事:

    • 调用原数组方法,获取到返回值。
    • 根据数组方法,获取到插入的元素,并调用 ob.observeArray(inserted) ,对插入的元素进行响应式处理。
    • 通知依赖,调用 ob.dep.notify() ,通知依赖更新。
  4. 最后定义 arrayKeys ,方便后续调用 copyAugment 时,将 arrayMethods 上的方法代理到value上去。

2.2 给对象添加响应式处理

  我们先回顾下对对象的处理方法。在observer类构造函数中,调用了 walk 方法,这个方法遍历对象的每一个属性,对每个属性调用了defineReactive方法。

  defineReactive 的作用就是利用 Object.defineProperty 对数据的读写进行劫持,给属性 key 添加 getter 和 setter ,用于依赖收集和通知更新。如果传进来的值依旧是一个对象,则递归调用 observe 方法,保证子属性都能变成响应式。我们来看看 defineReactive 方法的实现。

function defineReactive(obj, key, val, customSetter, shallow) {
  // 实例化 dep,一个 key 一个 dep
  var dep = new Dep();
  ...
  // 递归调用,处理 val 的值为对象的情况
  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      var value = getter ? getter.call(obj) : val;
      // Dep.target 是 Dep的一个静态属性,保存的是当前的 Watcher 实例。
      // 在 new Watcher 实例化的时候(computed 除外,因为它懒执行)会触发读取造作,被劫持运行这个 get 函数,进行依赖收集。
      // 在实例化 Watcher 最后,Dep.target 设置为 null,避免重复收集。
      if (Dep.target) {
        ...
        // 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (isArray(value)) {
            // 处理数组内还是对象的情况
            dependArray(value);
          }
        }
      }
      return isRef(value) ? value.value : value;
    },
    set: function reactiveSetter(newVal) {
      // 旧的 obj[key] 
      var value = getter ? getter.call(obj) : val;
      // 如果新旧值一样,则直接 return,无需更新
      if (!hasChanged(value, newVal)) {
          return;
      }
      ...
      // 对新值进行观察,让新值也是响应式的
      childOb = !shallow && observe(newVal);
      ...
      // 依赖通知更新
      dep.notify();
    }
  });
  return dep;
} 

  defineReactive 主要是做了以下三个事情

  1. 实例化了一个 Dep ,用来收集依赖。
  2. 判断传入的值是否是一个对象,如果是,则递归调用 observe 方法,对对象的属性进行响应式处理。
  3. 为对象添加 getter 和 setter ,用于依赖收集和通知更新。
    • getter:当读取属性时,会触发 getter ,进行依赖收集。它首先会判断 Dep.target ,Dep.target 是 Dep 的一个静态属性,保存的是当前的 Watcher 实例。在 new Watcher 实例化的时候(computed 除外,因为它懒执行),会触发读取操作,被劫持运行这个 get 函数,进行依赖收集。在实例化 Watcher 最后,Dep.target 设置为 null,避免重复收集。dep.depend 函数会向当前Watcher 实例中添加 dep,同时也会为 dep 添加 Watcher 实例,后续会贴出这个函数的实现。
    • setter:当值发生变化时,会触发 setter ,进行依赖通知更新。它首先会判断 hasChanged ,判断新值和旧值是否相等,如果相等,则直接返回,无需更新。同时对新值做响应式处理。最后通知依赖更新。

3.依赖管理——Dep

在创建 Observer观察者的时候,就有频繁用到 Dep 构造函数,它是一个管理器,用来管理依赖。我们先来看看构造函数的实现。

var Dep = (function () {
function Dep() {
  this.id = uid$2++;
  this.subs = [];
}
// 添加订阅,把 Watcher实例,保存到 subs中
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub);
};
// 移除订阅,把 Watcher实例,从 subs 中移除
Dep.prototype.removeSub = function (sub) {
  remove$2(this.subs, sub);
};
// 向 Watcher 中添加 dep
Dep.prototype.depend = function (info) {
  if (Dep.target) {
    Dep.target.addDep(this);
    if (process.env.NODE_ENV !== 'production' && info && Dep.target.onTrack) {
      Dep.target.onTrack(__assign({ effect: Dep.target }, info));
    }
  }
};
// 通知更新
Dep.prototype.notify = function (info) {
  var subs = this.subs.slice();
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  // 遍历 dep 中存储的 watcher,执行 watcher.update()
  for (var i = 0, l = subs.length; i < l; i++) {
    if (process.env.NODE_ENV !== 'production' && info) {
      var sub = subs[i];
      sub.onTrigger && sub.onTrigger(__assign({ effect: subs[i] }, info));
    }
    subs[i].update();
  }
};
return Dep;
}());

  Dep 构造函数主要用来管理依赖。它主要定义了一个 subs 属性并提供了四个方法:addSub、removeSub、depend 和 notify。

  1. subs: 用来存放依赖,也就是 Watcher 实例。
  2. addSub:添加依赖,把 Watcher 实例添加到 subs 中。这个方法调用是在 Watcher 构造函数中的addDep方法中执行,后续讲 Watcher 的时候会看到
  3. removeSub:移除依赖,把 Watcher 实例从 subs 中移除。
  4. depend:双向添加,depend 函数的实现里会调用 addSub 方法把 Watcher 实例添加到 subs 中。同时也会为 Watcher 实例添加 dep。
  5. notify:通知更新,遍历 subs 中的 Watcher 实例,执行 update 方法进行视图更新。

4.调度中枢——Watcher

  Watcher 在 Vue 的响应式系统中扮演重要角色,负责依赖收集和更新触发。 首先,Watcher 分为三种类型:渲染Watcher、计算属性Watcher 和 监听Watcher。渲染 Watcher 负责组件的渲染,计算属性 Watcher 处理 computed 属性,监听 Watcher 是用户自定义的watch选项。

类型创建场景功能特点
渲染 Watcher组件挂载时创建负责触发组件重新渲染
计算属性 Watchercomputed 属性创建时创建惰性求值,缓存计算结果
监听 Watcherwatch 选项创建监听属性变化,触发回调

  除了功能上的区别,这三种 watcher 也有固定的执行顺序,分别是:computed-render -> normal-watcher -> render-watcher。这样就能尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。

  因为 Watcher 类的实现比较复杂,这里我就挑重点贴下代码。

var Watcher = (function () {
  function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    ...
    if (options) {
      ...
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      ...
    }
    else {
      this.deep = this.user = this.lazy = this.sync = false;
    } 
    this.active = true;
    this.deps = [];
    ...
    // parse expression for getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn;
    }
    else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        ... 
      }
    }
    this.value = this.lazy ? undefined : this.get();
  }
  Watcher.prototype.get = function () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    }
    catch (e) {
      ...
    }
    finally {
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  };
  Watcher.prototype.addDep = function (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
          dep.addSub(this);
      }
    }
  };
  Watcher.prototype.update = function () {
    if (this.lazy) {
      this.dirty = true;
    }
    else if (this.sync) {
      this.run();
    }
    else {
      queueWatcher(this);
    }
  };
  Watcher.prototype.run = function () {
    if (this.active) {
      var value = this.get();
      if (value !== this.value ||isObject(value) ||this.deep) {
        var oldValue = this.value;
        this.value = value;
        if (this.user) {
          var info = "callback for watcher \"".concat(this.expression, "\"");
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
        }
        else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  };
  Watcher.prototype.evaluate = function () {
    this.value = this.get();
    this.dirty = false;
  };
  return Watcher;
}());

  我们来剖析下 Watcher 构造函数都干了些啥:

4.1 Watcher 函数

  在执行new Watcher时,会调用 Watcher 函数,传入参数。上面我们有提到过 Watcher 有三种类型,那么在实例化的时候,不同类型的 Watcher 也会传入不同的参数,后续做响应式处理的操作也会因此而不同。以下的 noop 是个空函数。

  • render watcher: 这里注意到第五个参数为 true。说明 isRenderWatcher=true。wacherOptions 具体的值我就不贴出来了,里面没有lazy 或者 sync 属性

    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
    new Watcher(vm, updateComponent, noop, watcherOptions, true)
    
  • computed watcher: 注意到 lazy: true。

    new Watcher(vm, getter || noop, noop, { lazy: true })
    
  • normal watcher: 这里的参数一般都是设置 immediate 和 deep 属性。

    // expOrFn: 就是key,自定义监听的那个变量
    // cb: handler 回调函数
    // options: 自定义选项,一般也不会传入lazy值
    var watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      var info = "callback for immediate watcher \"".concat(watcher.expression, "\"");
      pushTarget();
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
      popTarget();
    }
    

  在实例化 Watcher 时,下面这行代码非常关键,它决定了依赖收集的触发时机。this.get() 就是进行依赖收集的函数。根据上面传进来的 lazy 参数可以判断 computed watcher 是延迟执行收集依赖。其它两个都是立即执行收集依赖

this.value = this.lazy ? undefined : this.get()

  接下来看看 watcher.get() 是如何工作进行依赖收集的。

4.2 Watcher.get()

Watcher.prototype.get = function () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  }
  catch (e) {
    ...
  }
  finally {
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value;
};
  1. 首先调用 pushTarget(this),这一步就是设置 Dep.target 为当前 Watcher 实例。Dep.target 的作用大家可以回顾下2.2 章节。
  2. 执行this.getter.call(vm, vm),这里就是调用了 getter 函数,也就是我们传进来的 expOrFn。对于不同 Watcher 类型,getter 函数也不同。
    • render watcher: getter 函数为 updateComponent。这个函数主要作用就是虚拟 DOM 生成和 Diff 比对,然后更新视图。也就是说render watcher 的 getter 会更新视图。
    • computed watcher: getter 函数就是我们自己定义在 computed 里的函数
    • normal watcher: getter 函数其实就是我们监听的那个变量的 get 函数
  3. 判断 deep 属性,递归去访问 value,触发它所有子项的 getter 。
  4. 执行 popTarget(),将 Dep.target 设置为 null。

  上述的 getter 在执行过程会触发我们之前利用 Object.defineProperty 定义的 get 函数。我们再来回顾下 get 函数是如何进行依赖收集的。

get: function reactiveGetter() {
  var value = getter ? getter.call(obj) : val;
  if (Dep.target) {
    ...
    // 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
    dep.depend();
    if (childOb) {
      childOb.dep.depend();
      if (isArray(value)) {
        // 处理数组内还是对象的情况
        dependArray(value);
      }
    }
  }
  return isRef(value) ? value.value : value;
},

// 顺便补上Dep.depend函数,省的大家翻来翻去
Dep.prototype.depend = function (info) {
  if (Dep.target) {
    Dep.target.addDep(this);
    if (process.env.NODE_ENV !== 'production' && info && Dep.target.onTrack) {
      Dep.target.onTrack(__assign({ effect: Dep.target }, info));
    }
  }
};

  这里就是先执行 dep.depend(),然后这个函数里调用的 Dep.target.addDep(this) 就是执行 Watcher 的 addDep 方法将当前 Wacher 实例添加到 dep.subs 中。

4.3 Watcher.update()

   update 函数就是执行视图更新的入口。再讲 update 函数之前,我们先来梳理下什么时候执行 update 函数。回顾下2.2 章节。里面定义了 setter 函数,这个函数就是当数据变化时执行的。里面有这么一行代码 dep.notify() 。而 notify 函数里执行了 subs[i].update()。 subs 就是存储 watcher 实例的数组,所以在这里遍历执行了所有 watcher 实例的 update 函数。

Watcher.prototype.update = function () {
  if (this.lazy) {
    this.dirty = true;
  }
  else if (this.sync) {
    this.run();
  }
  else {
    queueWatcher(this);
  }
};

  update 函数里对于不同的情况,执行了不同的操作。

  • 当 this.lazy = true时, 这种情况是针对compute watcher 的。执行 this.dirty = true 标记脏值,这样做就把更新触发时机延迟到了访问时。访问时会执行 computedGetter, 这个函数里面会检查ditry 标记,如果为 true,就会执行 watcher.evaluate 重新收集依赖并更新值。
function computedGetter() {
  var watcher = this._computedWatchers && this._computedWatchers[key];
  if (watcher) {
      if (watcher.dirty) { // 脏检查
          watcher.evaluate();// 重新计算
      }
      if (Dep.target) {
          ...
          watcher.depend(); // 建立渲染依赖
      }
      return watcher.value;
  }
};
  • 当 this.sync = true时, 这种情况基本上只有normal watcher自定义了这个属性才会执行这个分支。立即执行 this.run() , 里面会执行回调函数cb。
  • 最后一种情况就是默认的,执行 queueWatcher(this),也就是将当前 watcher 实例添加到 queue 中。这是因为 Vue 的更新机制是异步的,所以这里会先将 watcher 实例添加到队列中,等下一个事件循环时,再执行渲染操作。

为什么compute watcher 要延迟更新呢?包括之前也是延迟收集依赖?

  1. 性能优化:避免不必要的计算:一个 compute watcher 里面可能有多个计算属性,而且有逻辑运算。如果初始化时就立即执行,可能会收集到永远都不会被访问到的计算依赖,计算了永远都用不到的值。
  2. 缓存机制:与惰性求值协同工作:计算属性的核心特性是缓存,当依赖项发生变化时,会标记 dirty: true。下次访问时,如果有脏值标记,就重新计算,没有就直接返回缓存值。这样做还可以将多次依赖值得变化合并为一次计算。

5.总结整个流程

  1. 首先在初始化时,遍历递归定义在 data 里的属性加上 Observer 观察者,对数组做原型链劫持操作,对对象使用 Object.defineProperty 进行劫持,将数据变成响应式的。
  2. 每个响应式数据都实例化了一个 Dep 实例,用来收集依赖。
  3. 当初始化模板、compute 选项和 watch 选项时,会实例化 Watcher 实例,分别是render watcher、compute watcher 和 normal watcher。其中render watcher 和 normal watcher 会立即执行收集依赖,而 compute watcher 则是延迟收集依赖。收集依赖主要是通过触发 getter 函数,将 Watchr 实例添加到 dep 里的数组中。
  4. 当值发生变更时,会触发 setter 函数,执行 Dep.notify(),通知 dep 数组里所有的 Watcher 实例更新。其中render watcher 和 normal watcher 会立即将 Watcher 实例添加到队列中,而 compute watcher 则是标记脏值,等待下次访问时重新计算。

写在最后

  了解完整个响应式原理,相比大家也清楚了为什么 Vue2 通过修改索引下标没法实现响应式处理(忘记的同学可以再回顾下2.2 章节),是因为它根本就没做、没做、没做。
  那么为什么不做呢?我认为是为了性能,就是给遍历数组的每一项加上 Observer 的性价比高不高,因为这里是要消耗时间和内存的。

  1. 一个对象比较大的话可能也就几百上千个属性,但是一个数组比较大的话,起码就是几万几十万个值了。这性能上的开销绝对不能忽略。

  2. 还需要考虑到一种情况,就是数组的赋值操作。举个例子:

    const arr = []
    arr[100000] = 10
    

    这种情况下,数组长度就是 100001,前面有 100000 个空位。这种时候做不做响应式处理呢?做了那么就会浪费内存,还要消耗大量的时间,性价比是极低的。

    大家如果感兴趣的话可以关注下我的个人博客,里面有我的技术笔记,并且我会不定时发布新的文章。