> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/yuan-ma-jie-xi/vue-yuan-ma-jie-xi/vue2-yuan-ma-jie-xi-xi-lie-xiang-ying-shi-yuan-li.md).

# Vue2源码解析系列-响应式原理

## 综述

Vue的响应式利用变化侦测实现。

所谓的变化侦测，就是在数据被访问的时候收集依赖，在改变的时候通知依赖更新。

总的来说就是Object是getter里收集依赖，setter触发依赖更新；Array是getter收集依赖，拦截器触发依赖更新。

其中数组和对象的实现思路略有区别，后面再详细分析。

上述是思路，具体实现我们还要解决以下问题

* 怎么监听数据被访问和被修改
* 什么是依赖？
* 如何收集依赖？
* 如何通知依赖更新

## Object的变化侦测

### **怎么监听数据被访问和修改**

前面说过所谓的变化侦测，就是在数据被访问的时候收集依赖，在改变的时候通知依赖更新，那么如何知道数据被访问或被修改了呢？

我们知道，对于`Object.defineProperty`可以声明对象属性，并且可以配置属性修饰符。而属性修饰符中的setter和getter访问器函数分别会在属性被修改和被访问时触发，因为我们可以利用`Object.defineProperty`来实现数据的监听。

```jsx
let obj = {}
Object.defineProperty(obj,'key',{
    value: "123",
    set(){
        console.log('数据被修改')
    },
    get(){
        console.log('数据被访问')
    }
})
obj.key
// 数据被访问
// 123
obj.key = 321
// 数据被修改
```

### **什么是依赖**

什么是依赖？我们可以这样理解：某个数据的值需要访问另一个数据，我们就可以说这个数据依赖了另一个数据，例如计算属性 `a(){return b + '123'}`,我们可以说a依赖b。

解决了什么是依赖，那么依赖应该存放在哪呢？我们很容易想到可以用一个数组存着，但是这样很耦合，我们抽象成一个类Dep，为这个类定义一些方法。

```jsx
// vue-src\core\observer\dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []  // 存放依赖
  }

  /*添加一个观察者对象*/
  addSub (sub: Watcher) {
  }

  /*移除一个观察者对象*/
  removeSub (sub: Watcher) {
  }

  /*依赖收集，当存在window.target的时候添加观察者对象*/
  depend () {
  }

  /*通知所有订阅者*/
  notify () {
  }
}
```

还有一个问题，前面我们说过，一个数据依赖另一个数据，那么就将这个依赖存起来，如果将来数据改变就通知依赖更新，那么所有依赖都要被收集吗？我们知道在methods中，如果一个方法里的响应式数据发生改变，这个方法却不会发送任何改变，而computed却会更新，这也就说明，Vue只会选择性的收集依赖，我们将这种会被当做依赖收集的数据抽象为类Watcher，收集只收集Watcher的实例。

现在可以回答前面的问题： 什么是依赖？依赖是Watcher。依赖收集后放哪里？放到Dep里。

### **如何收集依赖**

前面说到依赖收集到Dep，那么具体如何实现呢？

我们可以利用一个Dep和数据都能访问到的唯一变量。当创造Watcher实例时，将这个唯一变量指向实例自身，然后获取一下数据，数据的getter会将这个唯一变量收集到Dep中。

注意：上述的唯一变量在本例中是`window.target`，而实际上是静态变量`Dep.target`，两者效果相同，后者的优点是不会污染全局变量。

```jsx
// watcher将window.target指向自己，然后get一下数据
export default class Watcher{
      constructor (){
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
        }
        this.get()
      }
     get(){
         const vm = this.vm
        // 将实例添加到全局唯一变量中
        window.target = this;
        // 调用数据的getter方法，一是获取数据的值，而是调用getter将watcher实例添加Dep中
        let value = this.getter.call(vm, vm)
        // 清空
        window.target = undefined;
    }
}
// 数据的getter调用后会将window.target添加到Dep之中
Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            /*如果原本对象拥有getter方法则执行*/
            const value = (property && property.get) ? getter.call(obj) : val;

            if (window.target) {
                /*进行依赖收集*/
                let dep = new Dep()
                dep.addSub(window.target)
            }
            return value;
        },
}
```

实际代码加上了很多处理和判断，更复杂，这里进行了简化，理解核心思路即可。

### **如何通知依赖更新**

在setter中通知Dep,Dep遍历存储依赖的数组，依次调用更新方法。

```jsx
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
        if (setter) {
            /*如果原本对象拥有setter方法则执行setter*/
            setter.call(obj, newVal);
        } else {
            val = newVal;
        }
        /*新的值需要重新进行observe，保证数据响应式*/
        childOb = observe(newVal);
        /*dep对象通知所有的观察者*/
        dep.notify();
    },
});
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++  // 设置Dep编号
    this.subs = []
  }
  /*通知所有订阅者*/
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 调用watcher的更新方法
    }
  }
}
export default class Watcher {
  update () {
    // 调用对应的更新方法
  }
}
```

### **总结**

Object的变化侦测在getter收集依赖，存到Dep中，setter里通知Dep中的依赖进行更新操作，依赖就是watcher，Vue在需要依赖响应式数据的地方创造watcher实例，比如计算属性、模板语法或用户的watch。

## Array的变化侦测

我们知道Array其实本质上也是对象，因此也可以使用之前说的`Object.defineProperty`方法，但是实际上Vue并没有采用这种方法，请看下面代码。

```jsx
var obj = []
Object.defineProperty(obj,0,{
    set(){
        console.log('数据被修改')
    }
}
obj[0] = 1
// 数据被修改
// 1
obj.push(2)
// 2
```

可以看到，如果调用`Object.defineProperty`声明数组元素，固然使用中括号方法改变属性时可以执行setter，但是如果使用`push`这类数组方法，却无法触发setter。

事实上，这样写同样也会遇到性能问题，数组有些情况是需要存放大量数据的，如果每个元素都设置setter，对性能会有一定影响，因此Vue采用了另一种巧妙的方式实现数组的响应式。

### **拦截器**

什么是拦截器？举个例子。

```jsx
class A {
    toString() {
        return "我是A";
    }
}
class B extends A {}

let a = new A();
let b = new B();
console.log(a.toString(), b.toString());
// 我是A 我是A
```

类A里声明了`toString`方法，而类B继承于A，自然也有`toString`方法，并且A和B的`toString`方法相同。

这个时候如果我们在类B中再声明一个`toString`方法，那么实例b调用的就是类B自身的`toString`方法，而不是继承过来的类A的`toString`，我们可以理解类B的`toString`被拦截了。

```jsx
class A {
    toString() {
        return "我是A";
    }
}
class B extends A {
    // 新增
    toString() {
        return "我是B";
    }
}

let a = new A();
let b = new B();
console.log(a.toString(), b.toString());
// 我是A 我是B
```

同理，我们声明的数组是Array的实例，调用的`push`等方法其实是调用数组原型对象上的`push`方法，因此我们只需要在我们声明的数组里添加拦截器，那么调用`push`方法就是调用我们自定义的`push`方法，而不是`Array.prototype.push`。

![https://vue-js.com/learn-vue/assets/img/2.b446ab83.png](https://vue-js.com/learn-vue/assets/img/2.b446ab83.png)

那么什么方法才需要使用拦截器？很明显，能够改变数组自身数据的方法都需要拦截，整理后有以下几种：

* push
* pop
* shift
* unshift
* splice
* sort
* reverse

明白这些之后，我们就可以开始写拦截器了。

```jsx
// vue-src\core\observer\array.js
/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象，修改该对象上的数组的七个方法，防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto);

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  /*将数组的原生方法缓存起来，实际操作数组的时候还是需要调用原生方法*/
  const original = arrayProto[method]
  // def函数的对Object.defineProperty的封装
  def(arrayMethods, method, function mutator () {
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args)

    /*ob指向数据的Observer实例，后面再讲*/
    const ob = this.__ob__
    /* 以下方法会新增元素，将新增的元素存起来，然后执行observeArray，
        这是为了把新增元素也变成响应式的
    */
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)

    // notify change
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
```

现在我们得到了一个数组方法拦截器。

### **挂载拦截器**

写好拦截器后下一步就是要将拦截器挂载到响应式数组上。

拦截器本质上是个包含七个特殊方法的对象，并且继承了Array的原型，因此拦截器的挂载可以直接将拦截器作为响应式数组的`prototype`，在浏览器环境下对象的原型被暴露为`__proto__`属性，因此我们可以：

```
// 判断浏览器是否支持__proto__属性
const hasProto = '__proto__' in {}
if(hasProto){
   	target.__proto__ = arrayMethods;
}else {
    // 如果不支持，则调用Object.defineProperty将方法覆盖到目标上
	for (let i = 0, l = arrayMethods.length; i < l; i++) {
		const key = arrayMethods[i];
		def(target, key, src[key]);
	}
}
```

### **收集和通知依赖**

拦截器解决了数组触发依赖更新的问题，那么我们如何收集依赖呢？

答案是getter。请看以下代码：

```jsx
let obj = {
	arr: [1, 2, 3],
};
Object.defineProperty(obj, "_arr", {
	enumerable: true,
	configurable: true,
	get() {
		console.log("数据被访问");
        // 收集依赖
        let dep = new Dep()
        dep.depend();

		return obj.arr;
	},
});

obj._arr[0]; // 数据被访问
obj._arr.pop(); // 数据被访问
```

以上代码，我们首先声明一个包含数组arr的对象(obj)，这是模拟Vue中data的写法，同时配置好修饰符，在getter中收集依赖后返回真实的数据，最后测试使用数组方法和中括号访问是否能触发，很明显这种方法效果很好。

## Vue中的watcher

前面说到vue会将响应式状态的依赖封装成Watcher，那么我们现在就来看看Vue中有哪些Watcher。

```jsx
// lifecycle
export function mountComponent(){
    // ...
    new Watcher(
        vm,
        updateComponent,
        noop,
        {
            before() {
                if (vm._isMounted && !vm._isDestroyed) {
                    callHook(vm, "beforeUpdate");
                }
            },
        },
        true /* isRenderWatcher */
    );
}
// state.js
Vue.prototype.$watch = function(){
	// ...
	const watcher = new Watcher(vm, expOrFn, cb, options);
}
function initComputed(){
    // ...
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    );
}
```

可以看到，vue使用响应式的地方主要有

* 组件
* computed
* $watch()
* 其他

### **组件**

事实上，组件本身其实也是响应式的，例如你使用了一个文本插值（即`{{}}`），然后更新响应式状态，此时UI跟着改变。

```jsx
<template>
	<div>
        {{a}}
    </div>
</template>
<script>
	export default{
        data(){
            return {
                a: 1
            }
        },
        mounted() {
            setInterval(() => {
                this.a++
            }, 1000)
        }
    }
</script>
```

我们来分析一下这个过程。

* 首先vue解析文本插值语法，真正的值取出并渲染
* 当文本插值的响应式状态改变时，通知依赖(组件)更新(即重新渲染)。

而要达到当响应式状态改变通知组件更新这一目的，就需要将组件转成watcher，作为状态的依赖。

* 首先将组件封装成Watcher，将渲染函数作为更新时的回调传入
* 组件第一次渲染时会获取响应式状态的值，这个时候就会将组件作为依赖加入到状态的dep中。
* 当更改响应式状态时，会触发dep.notify()，通知状态的所有依赖更新，而组件的更新回调就是组件的渲染函数，因此就能达到重新渲染UI的目的。

在代码中的流程是这样的(省略了不重要的代码)

```jsx
// 首先将组件 new Watcher，并且标记这是一个RenderWatcher
// updateComponent是组件渲染函数
// 在watcher更新执行，会执行所有beforeUpdate函数(生命周期函数)
new Watcher(
    vm,
    updateComponent,
    noop,
    {
        before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, "beforeUpdate");
            }
        },
    },
    true /* isRenderWatcher */
);

```

接着会将组件会执行渲染后函数`updateComponent`

```jsx
export default class Watcher {
    constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean)
    {
        this.vm = vm
        if (isRenderWatcher) {
           vm._watcher = this
        }
        vm._watchers.push(this)
        if (typeof expOrFn === 'function') {
          // getter指向了组件的渲染函数
          this.getter = expOrFn
        }
        // 执行get方法
        this.value = this.lazy
          ? undefined
          : this.get()
    }
    get(){
        pushTarget(this)
		let value
    	const vm = this.vm
        // 这里首次执行了渲染函数，vue将模板渲染成UI
      	value = this.getter.call(vm, vm)
        return value
    }

}
// pushTarget方法在dep.js里
export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}
```

先将Dep.target执行组件的watcher，然后执行`updateComponent`来触发状态的getter，状态的getter函数会将`Dep.target`加入到状态的deps中，这样就成功的将组件作为依赖传入到所有用到的状态的deps中。如果有某一个状态改变，就会触发setter，然后触发deps.notify来通知所有依赖执行回调，对于组件的wather来说，回调就是渲染函数，因此就会重新执行渲染。

### **computed**

computed可以看做是一个特殊的响应式状态，它的值取决于对其他的响应式状态的计算，computed可以是一个函数，也可以是一个包含set或get的对象，如果是函数，那么等同于包含get函数的对象。computed真正强大之处在于它的值会被缓存，只有当其他响应式状态改变时才会重新计算，这对性能很有好处。

vue首先会在`initComputed`中为每个computed创建watcher

```jsx
function initComputed(vm: Component, computed: Object) {
    const watchers = (vm._computedWatchers = Object.create(null));
    for (const key in computed) {
        const userDef = computed[key];
        const getter = typeof userDef === "function" ? userDef : userDef.get;
        if (!isSSR) {
          // 为每个computed创建watcher
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            {lazy: true}   // 开启缓存
          );
        }
        defineComputed(vm, key, userDef);
    }
}
```

然后会使用一个中间层函数`createComputedGetter`，这个函数会判断是否需要重新计算，如果需要才会调用computed真实的函数。`createGetterInvoker`则是在无法使用缓存的情况下使用。

```jsx
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  // SSR
  const shouldCache = !isServerRendering();
  if (typeof userDef === "function") {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    // 根据shouldCache && userDef.cache !== false条件
    // 来判断是否要使用缓存
    sharedPropertyDefinition.get = userDef.get
      ? (shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get))
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }

  Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      // watcher.dirty是一个用于判断数据是否发生了变化的标志
      if (watcher.dirty) {
        // 执行watcher.get()方法，并设置dirty为false
        watcher.evaluate();
      }
      // 收集依赖
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value;
    }
  };
}
function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this);
  };
}
```

vue对computed的处理和对state的处理很类似，具体流程是：首先vue将每个computed创建watcher(传递lazy来表明要使用缓存)，以便作为依赖被使用到的响应式状态添加到deps中，接着通过Object.defineProperty创建属性，但是get函数加了一个中间层来判断是否要重新计算(即createComputedGetter函数)。

computed每次获取时都会触发getter，也就是 `createComputedGetter`函数，这里会收集依赖，并且会通过`watcher.dirty`标记来判断是否要重新计算。如果computed的依赖更新，那就会通知依赖(也就是computed的watcher)进行更新，此时就会将`watcher.dirty`标记设为true，那么就会重新计算。

### **$watch()**

`$watch()`方法实际上就是将Watcher主动暴露给用户使用，用户指定监听的响应式状态和回调，然后该状态会将watcher作为依赖收集到deps，更新时就会触发执行回调函数。

## 响应式API

Vue2.0中有三个常用响应式API，分别是`vm.#watch`、`vm.$set`、`vm.$delete`

### **Vue.$watch**

`vm.$watch` 能监听一个表达式或computed函数，触发时执行特定函数。本质上也是利用Watcher。在使用vm.$watch时会为目标数据创建一个watcher，watcher的构造函数里会读取一下数据，这样就将这个watcher添加到dep中了，如果数据更新了，就会触发对应的回调函数。

```jsx
// vue-src\core\instance\state.js
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    /*有immediate参数的时候会立即执行*/
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    /*返回一个取消观察函数，用来停止触发回调*/
    return function unwatchFn () {
      /*将自身从所有依赖收集订阅列表删除*/
      watcher.teardown()
    }
  }
```

`vm.$watch`第一个参数能接收表达式或computed函数

如果是函数，直接作为getter，如果是keypath，则需要处理

```jsx
if (typeof expOrFn === 'function') {
    this.getter = expOrFn
} else {
    this.getter = parsePath(expOrFn)
}

// 下面是处理keypath的函数，其实就是遍历一层一层的查找
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
```

`vm.$watch`会返回一个函数，执行函数会取消监听。我们看到前面代码里返回了一个函数，函数里就执行了`watcher.teardown()`语句，因此取消监听重点是在`watcher.teardown()`上，它的作用是将watcher自身从所有依赖收集订阅列表删除。

要实现将watcher自身从所有依赖收集订阅列表删除，首先需要在Watcher中记录自己都订阅了谁，也就是watcher实例被收集进了哪些Dep里。然后当Watcher不想继续订阅这些Dep时，循环自己记录的订阅列表来通知它们（Dep）将自己从它们（Dep）的依赖列表中移除掉。

这个简单，我们首先为每个dep定义一个编号，每次将watcher添加到dep时就将这个dep的编号存到一个数组里。

```jsx
class Watcher{
  /*添加一个依赖关系到Deps集合中*/
  addDep (dep: Dep) {
    const id = dep.id
    // 如果当前Watcher已经订阅了这个Dep则跳过
    if (!this.depIds.has(id)) {
        // 记录当前Watcher已经订阅了这个Dep
        this.depIds.add(id)
        // 记录自己都订阅了哪些Dep
        this.deps.push(dep)
        // 将自己订阅到Dep
        dep.addSub(this)
    }
  }
}
```

然后再看`watcher.teardown()`

```jsx
class Watcher{
/*将自身从所有依赖收集订阅列表删除*/
  teardown () {
    // this.active表示当前watcehr实例是否正在被使用
    if (this.active) {
      /*从vm实例的观察者列表中将自身移除,由于该操作比较耗费资源，所以如果vm实例正在被销毁则跳过该步骤。*/
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

class Dep{
  /*移除一个观察者对象*/
  removeSub (sub: Watcher) {
    if (this.subs) {
        const index = this.subs.indexOf(sub)
        if (index > -1) {
          return this.subs.splice(index, 1)
        }
  }
}
```

`watcher.teardown()`会遍历`this.deps`依次执行`removeSub`方法，将自身从dep上删除。

### **Vue.$set**

```jsx
Vue.prototype.$set = set
export function set(target: Array<any> | Object, key: any, val: any): any {
	/*如果传入数组则在指定位置插入val*/
	if (Array.isArray(target) && typeof key === "number") {
		target.length = Math.max(target.length, key);
		target.splice(key, 1, val);
		/*因为数组不需要进行响应式处理，数组会修改七个Array原型上的方法来进行响应式处理*/
		return val;
	}
	/*如果是一个对象，并且已经存在了这个key则直接返回*/
	if (hasOwn(target, key)) {
		target[key] = val;
		return val;
	}
	/*获得target的Oberver实例*/
	const ob = (target: any).__ob__;
	/*
    _isVue 一个防止vm实例自身被观察的标志位 ，_isVue为true则代表vm实例，也就是this
    vmCount判断是否为根节点，存在则代表是data的根节点，Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)
  */
	if (target._isVue || (ob && ob.vmCount)) {
		/*
      Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)。
      https://cn.vuejs.org/v2/guide/reactivity.html#变化检测问题
    */
		return val;
	}
	if (!ob) {
		target[key] = val;
		return val;
	}
	/*为对象defineProperty上在变化时通知的属性*/
	defineReactive(ob.value, key, val);
	ob.dep.notify();
	return val;
}
```

### **Vue.$delete**

```jsx
Vue.prototype.$delete = del
export function del(target: Array<any> | Object, key: any) {
	if (Array.isArray(target) && typeof key === "number") {
		target.splice(key, 1);
		return;
	}
	const ob = (target: any).__ob__;
    // Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)。
	if (target._isVue || (ob && ob.vmCount)) {
		return;
	}
    // 如果没有这个属性则跳过
	if (!hasOwn(target, key)) {
		return;
	}
	delete target[key];
    // 如果数据不是响应式的则结束，否则还要通知依赖
	if (!ob) {
		return;
	}
	ob.dep.notify();
}
```
