当我们在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 上的属性相同。
  • 通过 proxyvm._data.xxx 的访问代理到 vm.xxx 上。
  • 调用 observedata 上的数据变成响应式。
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. 首先定义了 arrayMethodsObject.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 添加 gettersetter ,用于依赖收集和通知更新。如果传进来的值依旧是一个对象,则递归调用 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. 为对象添加 gettersetter ,用于依赖收集和通知更新。
    • getter:当读取属性时,会触发 getter ,进行依赖收集。它首先会判断 Dep.targetDep.targetDep 的一个静态属性,保存的是当前的 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 属性并提供了四个方法:addSubremoveSubdependnotify

  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 组件挂载时创建 负责触发组件重新渲染
计算属性 Watcher computed 属性创建时创建 惰性求值,缓存计算结果
监听 Watcher watch 选项创建 监听属性变化,触发回调

  除了功能上的区别,这三种 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=truewacherOptions 具体的值我就不贴出来了,里面没有lazy 或者 sync 属性

    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 比对
    • 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 watchercompute watchernormal watcher。其中render watchernormal watcher 会立即执行收集依赖,而 compute watcher 则是延迟收集依赖。收集依赖主要是通过触发 getter 函数,将 Watchr 实例添加到 dep 里的数组中。
  4. 当值发生变更时,会触发 setter 函数,执行 Dep.notify(),通知 dep 数组里所有的 Watcher 实例更新。其中render watchernormal watcher 会立即将 Watcher 实例添加到队列中,而 compute watcher 则是标记脏值,等待下次访问时重新计算。

# 写在最后

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

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

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

    const arr = []
    arr[100000] = 10
    

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