# 响应式原理

响应式是指数据的变化能自动触发视图的更新

VUE 中响应式原理的实现,主要有三大要素:ObserverWatcherDep

  • Observer-监听器

    要实现响应式那么就得知道数据监听数据何时发生的变化, Observer 的作用就是监测数据发生变化的时机。在 VUE 中是使用 Object.defineProperty() 方法实现 Observer

  • Watcher(事件)

    这里的事件就是当数据变化后要做的事件,比如是渲染 DOM 方法

  • Dep-事件收集器(依赖收集)

    收容事件的容口,每个数据都自己的一个容器用来收集依赖它的事件,当数据发生变化时,从容器中取出这些事件并执行。Vue 的容器是使用订阅-发布者的设计模式来实现的

# 源码分析

初始化时对 data 做监听处理

源码流程:

先分析下如何给 data 上的属性添加监听

initMixin() => initState(vm: Component) => initData(vm: Component) => observe(data) => new Observer(value) => this.walk(obj: Object) => defineReactive(obj, key) => 使用 Object.defineProperty

查看关键代码:

// new Observer(value)
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this) // this.data.__ob__ 就是这么来的
    if (Array.isArray(value)) {
      // ...
    } else {
      this.walk(value)
    }
  }

//  this.walk(value)
walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

// defineReactive
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      dep.notify()
    }
  })
}

从上面代码可以看到,对于每个属性都会实例一个收集容器:const dep = new Dep(), dep.depend() 就是收集事件的方法,并且可以看到 Vue 是在读取 data 属性时才开始做的收集工作

Dep 的实现如下:

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

分析一下分集逻辑

if (Dep.target) {
    dep.depend()
}

Dep.target 是一个全局属性,实际上赋给它的值是一个 Warcher 实例,上面也说了一个 Warcher 就表示一个事件,当执行 dep.depend() 时,执行的是以下代码:

// dep.depend()
if (Dep.target) {
    Dep.target.addDep(this)
}

这里的 this 是指向当前的 dep 实例, 这里执行 Dep.target.addDep() 方法,实际上是执行的 Watcher.addDep()方法,并把 dep 做为参数传入, Watcher.addDep() 的代码实现:

// Dep.target.addDep() = Watcher.addDep()
addDep(dep) {
  this.newDeps = dep
  dep.addSub(this)
}

这里的 dep 就是传进来的 dep 实例,然后执行这个 dep 实例的 addSub 方法,并把 this(指向当前的 Watcher 实例)作为参数,再回到 dep.addSub 的定义:

 addSub (sub: Watcher) {
    this.subs.push(sub)
  }

这里就收集了一个 Watcher 了, 感觉绕了一大圈

回到开头那么什么时候会触发 get 方法去收集依赖呢? 有两个情况

  1. 渲染组件的时候,当需要用到 data 上的属性时,就会读取,此时就会触发 get 方法

  2. 开发者在 Vue 方法中读取属性的时候

那么问题来了,对一个属性的获取会有发生多次,那怎么避免重新收集依赖呢? 这个问题下文再说~

# Dep-事件收集(依赖收集)

Dep 是个收集事件的容器,本质是一个观察者模式,每个可以添加响应机制的 data 的属性都有自己的一个 Dep 容器

import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

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

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

# Watcher-事件

Watcher 是 Dep 具体收集的事件,在 Vue 中它可能是渲染组件的方法,也可以是一个 watchhandler, 总之数据变化后要执行的东西

  var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

  /**
   * Add a dependency to this directive.
   */
  Watcher.prototype.addDep = function addDep (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);
      }
    }
  };

  /**
   * Clean up for dependency collection.
   */
  Watcher.prototype.cleanupDeps = function cleanupDeps () {
    var i = this.deps.length;
    while (i--) {
      var dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    var tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  };

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        var oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  };

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };

  /**
   * Depend on all deps collected by this watcher.
   */
  Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  };

  /**
   * Remove self from all dependencies' subscriber list.
   */
  Watcher.prototype.teardown = function teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      var i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  };

以渲染组件为例,来分析下实例化一个渲染 Watcher 的时做了哪些事情

Vue 在初始化工作完成之后通过执行 vm.$mount(vm.$options.el) 方法来开始渲染组件, vm.$mount 的作用就是生成当前组件的 render 方法,render 方法就是使用就是生成 VNode ,这里先不细讲这个东西。之后执行 mount.call(this, el, hydrating) => mountComponent(this, el, hydrating) 方法, mountComponent 的重点是几行代码:

var updateComponent;
updateComponent = function () {
  vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
  before: function before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate');
    }
  }
}, true /* isRenderWatcher */)

来了来了!终于看到了 new Watcher 了,传入了三个参数:

  • vm: 当前 Vue 实例

  • updateComponent: 内部执行的 vm._update(vm._render(), hydrating)vm._render() 是生成 VNode的,vm._update 使用则是将 VNode 生成真实的 DOM

  • noop: 空函数,先不管它

执行 Watcher 的构造函数:

// 以下代码做了删减
this.vm = vm;
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
  this.getter = expOrFn; // 保存传进来的_update方法
}
this.value = this.lazy
  ? undefined
  : this.get(); // 执行 get 方法

下面是 Watcher.get() 方法的定义:

/**
 * Evaluate the getter, and re-collect dependencies.
 */
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

get 方法中,首先执行 pushTarget(this) 方法,它的定义如下:

function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  }

在上文有讲过 Dep.target 赋值的是一个 Watcher , 在这里出现了。 执行完 pushTarget 之后就是执行 this.getter.call(vm, vm) 方法了,也是执行生成 DOM 的 vm._update(vm._render(), hydrating)方法。所以总结来说当前这个 Watcher 主要做了以下两件事件

  1. 将当前 Watcher 实例赋值到全局Dep.target,方便之后被收集器 dep 收集

  2. 触发当前 Vue 实例的渲染方法,渲染的时候就会访问到 data 属性,从而触发 Object.definedProperty() 中的 get 方法,将当前全局环境中保存到 Dep.target 中的 Watcher 实例添加到容器 dep

至此 Vue 中响应式原理的过程就跑通了

# 关于深度对象的监听

Vue 也可以监听 Object 类型的数据,那么内部是怎么处理的呢? 以下面的数据为例:

data(){
  return {
    tree: {
      child: {
        name: 'lan'
      }
    }
  }
},
watch: {
  tree: {
    handler: function(){},
    deep: true
  }
}

源码处理流程: initData (vm) => observe (value, asRootData) => new Observer(value) => this.walk(value) => defineReactive$$1(obj, keys[i])

看下 defineReactive 的定义:

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

以上面例子为例,当首次进入到 defineReactive() 方法时接收到的参数如下:

  • obj: {tree: { child: { name: 'lan' }}}

  • key: tree

  • val: undefined

  • shallow: false

当代码执行到:

if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
}
var childOb = !shallow && observe(val);
// 得到 val 的值为: { child: { name: 'lan' }}

上面有一行语句:var childOb = !shallow && observe(val) ,其实在上面的流程中也有执行到 observe(val) 方法, 看下其定义:

function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

此时进入到这个方法时传进来的参数为:

  • value: { child: { name: 'lan' }}

  • asRootData: undefined

在函数内的第一行可以看到只有当 value 不是对象时才会退行程序的执行, 如果当前还是对象的话就会继续一轮代码执行: observe (value, asRootData) => new Observer(value) => this.walk(value) => defineReactive$$1(obj, keys[i])

重复以上步骤,就完成了整个对象的监听。说白了就是遍历对象对子元素使用 Object.definedProperry() 处理

# 关于对数组的监听

Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

  2. 当你修改数组的长度时,例如:vm.items.length = newLength

但是对于其它数组方法是可以实现监听的,比如: push, pop, splice

接下来看 Vue 中相关源码分析下是如何处理的

  var arrayProto = Array.prototype;
  var arrayMethods = Object.create(arrayProto);

  var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];

  /**
   * Intercept mutating methods and emit events
   */
  methodsToPatch.forEach(function (method) {
    debugger
    // cache original method
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      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
      ob.dep.notify();
      return result
    });
  });
  // def
  function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    });
  }

上面的代码步骤:

  1. 使用 arrayProto 保存数组原型方法

  2. 新建 arrayMethods 对象,并且将它的原型指向arrayProto

  3. 遍历 methodsToPatch 数据,里面是数组方法名

def(arrayMethods, method, function mutator (){}}, 使用 defarrayMethods 的属性代理到 mutator 方法中,这样当我们访问 arrayMethods.pusharrayMethods.splice 等方法时,将会执行 mutator 方法

以调用 push() 方法为例看下 mutator 方法的作用:

function mutator () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      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
      ob.dep.notify();
      return result
    }
  1. 当调用 push 方法时,首先执行 original.apply(this, args)方法,就是执行数组上的 push 方法并得到结果

  2. var ob = this.__ob__,取出 __ob__,如果执行的 pushunshiftsplice,将执行 ob.observeArray(inserted) 方法尝试给这些子属添加响应监听

  3. 然后执行 ob.dep.notify();,触发依赖更新

__ob__属性

在初始化数据时对于子元素如果是对象或者数组类型的值也会执行 Observer 方法,在这里会子属性添加 __ob__ 指像当前属性的 Observer 对象

如果需要在操作数组类型的值时执行 mutator 方法,说明 Vue 需要将数组类型的值代理到 arrayMethods 中,看下 Vue 在哪做的处理

  var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };

上面代码可以看到如果值是数组类型将执行 protoAugment(value, arrayMethods) 方法,并修改的原型指向

//  protoAugment(value, arrayMethods);
  function protoAugment (target, src) {
    /* eslint-disable no-proto */
    target.__proto__ = src;
    /* eslint-enable no-proto */
  }

在 Vue 项目验证一下:

data:(){
  return {
    arr: []
  }
},
mounted(){
  const arr2 = []
  const arr3 = []
  console.log(this.arr.__proto__ === arr2.__proto__) // false
  console.log(arr2.__proto__ === arr3.__proto__) // true
  console.log(this.arr.__proto__.__proto__ === arr2.__proto__) // true
  Object.getPrototypeOf(this.arr) // [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]
  Object.getPrototypeOf(arr2) // Array {push: ƒ, pop: ƒ, shift: ƒ, unshift: ƒ, splice: ƒ, …}
},

# 总结

  1. Vue 在初始化的时候会遍历 data 数据,对于每个属性执行 defineReactive 方法,这里方法主要做以下两个事情:
  • 实例化一个事件器 Dep

  • 使用 Obejct.definedPropery() 方法代理属性并添加 setget 方法, get 中添加了收集 Watcher 的逻辑,而 set 则是包含了执行 Watcher 的逻辑

  1. 在组件渲染的时候,会实例化一个 Watcher 实例,并将当前 Watcher 实例到全局属性 Dep.target中,之后在执行渲染操作的时候会读取 data 的属性, 并被 get 劫持,get 方法中将保存在全局的 Watcher 收集到 dep

  2. 之后当 data 属性被更改时,会被 set 劫持,然后执行 Watcher 中的更新方法