# 前言

不知道写什么,所以没有前言
我们从入口开始讲起

# 从入口开始

// vue/src/core/instance/index.js
function Vue(options) {
	if (process.env.NODE_ENV !== 'production' &&
		!(this instanceof Vue)
	) {
		warn('Vue is a constructor and should be called with the `new` keyword')
	}
	this._init(options)
}

可以看到,vue 调用了一个 _init 方法,并传入了 options ,那我们转到_init 方法

// vue/src/core/instance/init.js
export function initMixin(Vue: Class<Component>) {
	Vue.prototype._init = function (options?: Object) {
		vm._self = vm;
		initLifecycle(vm);
		initEvents(vm);
		initRender(vm);
		callHook(vm, 'beforeCreate');
		initInjections(vm);  // resolve injections before data/props
		initState(vm);		// 对数据进行处理
		initProvide(vm); // resolve provide after data/props
		callHook(vm, 'created');
		// 如果传入了 el, 进行挂载
		if (vm.$options.el) {
			vm.$mount(vm.$options.el)
		}
	}
}

然后对数据进行处理的是 initState 函数,那我们转到 initState

// vue/src/core/instance/state.js
export function initState(vm: Component) {
	vm._watchers = [];
	const opts = vm.$options;
	if (opts.props) initProps(vm, opts.props);
	if (opts.methods) initMethods(vm, opts.methods);
	if (opts.data) {
		initData(vm);
	} else {
		observe(vm._data = {}, true /* asRootData */)
	}
	if (opts.computed) initComputed(vm, opts.computed);
	if (opts.watch && opts.watch !== nativeWatch) {
		initWatch(vm, opts.watch)
	}
}

我们可以从执行的函数名看出,这里完成了 props,methods,data,computed,watch 的初始化,我们来看看 data 的初始化,我们转到 initData

function initData(vm: Component) {
	let data = vm.$options.data;
	// 如果 data 是一个函数,就执行函数
	// 并且把 data 保留到 vm._data 上
	data = vm._data = typeof data === 'function'
		? getData(data, vm)
		: data || {};
	// 判断是否是平面对象
	//let obj = {} 这种就是平面对象
	if (!isPlainObject(data)) {
		data = {};
		process.env.NODE_ENV !== 'production' && warn(
			'data functions should return an object:\n' +
			'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
			vm
		)
	}
	// proxy data on instance
	const keys = Object.keys(data);
	const props = vm.$options.props;
	const methods = vm.$options.methods;
	let i = keys.length;
	// 这个循环是检测是否有重名和占据保留字的
	while (i--) {
		const key = keys[i];
		if (process.env.NODE_ENV !== 'production') {
			// 检测是否和 methods 传入的字段重名
			if (methods && hasOwn(methods, key)) {
				warn(
					`Method "${key}" has already been defined as a data property.`,
					vm
				)
			}
		}
		// 检测是否和 methods 传入的字段重名
		if (props && hasOwn(props, key)) {
			process.env.NODE_ENV !== 'production' && warn(
				`The data property "${key}" is already declared as a prop. ` +
				`Use prop default value instead.`,
				vm
			)
		}
		// 这是检测是否占据了保留字
		//vue 把_和 $ 开头的识别为保留字
		else if (!isReserved(key)) {
		// 把 vm_data 上的数据代理到 vm 上
		// 以后访问 vm.xxx 等同于访问 vm._data.xxx
			proxy(vm, `_data`, key)
		}
	}
	// observe data
	// 这里开始正式观测数据
	observe(data, true /* asRootData */)
}

我们可以看到,在经过前面的一堆处理和检测后, observe 函数被调用了,并且把 data 作为参数传了进去,从函数名我们也可以看出,这就是实现数据响应式的函数。
然后我们看看 observe 函数的实现

export function observe(value: any, asRootData: ?boolean): Observer | void {
	// 判断是不是对象
	if (!isObject(value) || value instanceof VNode) {
		return
	}
	// Observe
	let ob: Observer | void;
	// 判断 value 是否已经被观测
	// 如果已经观测,value.__ob__就是 Observer 对象
	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
	) {
		// 如果判断通过就进行观测
		// 观测是通过 new Observer 进行的
		ob = new Observer(value)
	}
	if (asRootData && ob) {
		ob.vmCount++
	}
	return ob;
}

在经过了一系列判断后,如果这个对象没有被观测过,就会通过 new Observer 的方式进行观测,那我们来看看 Observe 做了什么。

export class Observer {
	value: any;
	dep: Dep;
	vmCount: number; // number of vms that has this object as root $data
	// 这里的 value 就是你传入的 data 对象
	constructor(value : any) {
		this.value = value;
		// 这个 dep 是个数组用的
		this.dep = new Dep();
		this.vmCount = 0;
		// 定义__ob__
		// 这里把定义了访问器属性 value.__ob__,值是 this,也就是 observer
		def(value, '__ob__', this);
		// 判断 value 是否是数组
		if (Array.isArray(value)) {
			const augment = hasProto
				? protoAugment
				: copyAugment;
			// 这是把 value 的原型改成 arrayMethods
			augment(value, arrayMethods, arrayKeys);
			// 调用这个方法来观测数组
			this.observeArray(value)
		} else {
			// 如果 value 不是数组,就调用 walk 方法观测 value
			this.walk(value)
		}
	}
	/**
	 * Walk through each property and convert them into
	 * getter/setters. This method should only be called when
	 * value type is Object.
	 */
	 // 进行普通对象的观测
	walk(obj: Object) {
		// 获取对象的 keys
		const keys = Object.keys(obj);
		for (let i = 0; i < keys.length; i++) {
			// 调用 defineReactive 进行某个值的观测
			defineReactive(obj, keys[i])
		}
	}
	/**
	 * Observe a list of Array items.
	 */
	 // 观测数组
	observeArray(items: Array<any>) {
		for (let i = 0, l = items.length; i < l; i++) {
			observe(items[i])
		}
	}
}
// 把某个数据弄成访问器属性
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
	Object.defineProperty(obj, key, {
		value: val,
		enumerable: !!enumerable,
		writable: true,
		configurable: true
	})
}

在上面我已经把源码放了出来,可以看出,vue 会把数组和对象作为两种情况讨论,所以下面我们把情况分为对对象和对数组来进行讨论。
不过我们要先花一点时间了解对一些流程做一个整体的了解。首先我们要知道,在 vue 里,一个组件对应一个渲染 watcher ,这个 watcher 上的 update 方法执行时,就会重新渲染这个组件,那么这个组件怎么知道什么时候要重新渲染呢,其实是这样的,在这个组件渲染时,这个组件会把它对应的渲染 watcher 推到 Dep.target 上,而在访问渲染需要的数据时,会触发定义的 getter 方法,这个时候,数据会从 Dep.target 上拿到这个 watcher ,然后把它保存到一个地方,这个地方就是 dep 实例, dep.subs 会缓存对应的 watcher 对象,在这个数据的值刷新时,定义的 setter 方法就会被触发, setter 方法会调用 dep.notify ,这个方法会遍历保存在 dep 里的 watcher ,调用 watcherupdate 方法,对渲染 watcher 来说,调用 update 方法就相当于调用了重新渲染组件的方法。这就是数据响应式的一个大致流程,接下来我们来看看源码的实现。

# 对象的观测

# 第一层

假设我们有以下的代码

let vm = new Vue({
	el : "#app",
	data() {
		return {
			name : "sena",
			age : "16"
		}
	}
})

那么在执行的时候发生了什么呢,我们来看看,首先是调用了 new Observer() ,然后调用了 walk 方法,需要关注的代码就只有这些

constructor(value : any) {
	this.value = value;
	this.walk(value)
}

然后在 walk 方法里,遍历所有属性,用 defineReactive 定义响应式

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

然后我们看看 defineReactive (PS:删除了一部分这种情况不会执行 / 不重要的代码,之后也会逐步根据不同情况添加代码)

export function defineReactive(
	obj: Object,
	key: string,
	val: any
) {
	const dep = new Dep();
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			//getter 收集依赖
			// 获取属性对应的值
			const value = val;
			// Dep.target 会存放当前组件的渲染 Watcher
			if (Dep.target) {
				// 这句话会把 Dep.target 添加到 dep 的中
				// 也就是保存了 watcher
				dep.depend();
			}
			return value
		},
		set: function reactiveSetter(newVal) {
			//setter 派发更新
			const value = val;
			// 判断新旧值是否相等,相等就不触发派发更新
			if (newVal === value || (newVal !== newVal && value !== value)) {
				return
			}
			val = newVal;
			// 这里通知渲染 watcher 执行 update 方法,进行组件更新
			dep.notify()
		}
	})
}

通过执行 defineReactive ,vue 的数据得以和 watcher 关联起来,由此完成了响应式。
显然,如果只完成第一层的观测,代码是很简单的,但是正常情况下要观测的数据比这复杂的多,那我们来看看多层的实现吧

# 多层实现

实际上,多层实现也没有太复杂,依旧是先调用 walk 方法,执行 defineReactive ,但是这里多了一些处理

export function defineReactive(
	obj: Object,
	key: string,
	val: any
) {
	const dep = new Dep();

+	// 新增代码1 : 递归调用进行观测
+	let childOb = observe(val);

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			// getter收集依赖
			// 获取属性对应的值
			const value = val;
			// Dep.target会存放当前组件的渲染Watcher
			if (Dep.target) {
				// 这句话会把Dep.target添加到dep的中
				// 也就是保存了watcher
				dep.depend();
			}
			return value
		},
		set: function reactiveSetter(newVal) {
			// setter派发更新
			const value = val;
			// 判断新旧值是否相等, 相等就不触发派发更新
			if (newVal === value || (newVal !== newVal && value !== value)) {
				return
			}
			val = newVal;
+			// 新增代码2 : 如果新的值也是对象,也会进行观测
+			childOb = observe(newVal);
			// 这里通知渲染watcher执行update方法,进行组件更新
			dep.notify()
		}
	})
}

要完成观测多重嵌套,只要添加两行代码就可以了,我接下来会用一个组件,然后去源码中分别注释这两行,然后观察效果

<template>
    <div class="home">
        <p>name : </p>
        <p>age : </p>
        <p>school.name : </p>
        <button @click="click">click me</button>
    </div>
</template>
<script>
    export default {
        name: 'home',
        data() {
            return {
				name : "sena",
				age : "16",
				school : {
					name : "school name"
				}
            }
        },
        methods : {
            click() {
                this.school.name = "another school name";
				console.log("click");
				console.log(this);
            }
        }
    }
</script>

首先我们注释的是新增代码 1,也就是 let childOb = observe(val) 这一句,注释后组件可以正常渲染

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/05/image-1589471652804.png" preview="1">
</div>

然后我们点击一下按钮,可以看到,尽管 school.name 已经更新,但是组件没有刷新,这是因为 school.name 没有被观测

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/05/image-1589471746520.png" preview="1">
</div>

我们把注释去掉,然后点击按钮

<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/05/image-1589471907356.png" preview="1">
</div>

这次 school.name 在改变的同时也触发了更新,我们也可以见到,school.name 变成了一个访问器属性,这证明 school.name 已经通过 defineReactive 获得了收集依赖和派发更新的能力。
所以我们可以知道,这一行代码的用处是递归处理深层的数据,给深层数据定义响应式。

现在我们去源码里把第二行注释掉,也就是 childOb = observe(newVal) 这一句,刷新页面后点击按钮,修改了 school.name 后依旧可以正常更新组件
<div class="vue-app">
<img src="https://sakurablog.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2020/05/image-1589471907356.png" preview="1">
</div>
那是不是意味着没有问题了呢,当然不是,我们改一下代码

<template>
    <div class="home">
        <p></p>
        <button @click="school = {
        	name : 'another school name'
        }">button1</button>
        <button @click="click">button2</button>
    </div>
</template>
<script>
    export default {
        name: 'home',
        data() {
            return {
				name : "sena",
				age : "16",
				school : {
					name : "school name"
				}
            }
        },
        methods : {
            click() {
                this.school.name = "school name";
				console.log("click");
				console.log(this);
            },
        }
    }
</script>

页面刷新后分别点击 button1 和 button2,可以看到这样的现象
file

诶,为什么点击 button2 时没有更新页面呢,其实很简单,我们直接修改了 school 的值,那么 school 就是一个全新的对象,这个新的对象自然是没有被观测过的,我们从图中可以看到 school.name 已经不是一个访问器属性了,也就是说修改它时 setter 不会被触发,也就不能触发组件更新的逻辑。顺带一提,这个时候 school 仍然是一个访问器属性,因为 getter 和 setter 是对应在对象的键上的。

然后我们把注释去掉,就能正常更新了,school.name 也在 school 被重新赋值后被观测
file

# 数据的观测

# 第一层

new Observer(value ) 时,如果 value 是一个数组,要处理的流程就和是对象的情况不一样了

constructor(value: any) {
	this.value = value;
+	// 这个dep是数组专用的,具体使用后面会说
+	this.dep = new Dep();
+	// 定义__ob__, 这个属性会在之后用上
+	// 这里把定义了访问器属性value.__ob__,值是this,也就是observer
+	def(value, '__ob__', this);
+	if (Array.isArray(value)) {
+		// 修改原型
+		value.__proto__ = arrayMethods;
+		this.observeArray(value)
	} else {
		this.walk(value)
	}
}

可以看到,如果要观测的对象是数组类型,会调用 observeArray 方法,那我们来看看这个方法干了什么。

observeArray(items: Array<any>) {
	for (let i = 0, l = items.length; i < l; i++) {
		observe(items[i])
	}
}

observeArray 遍历了数组的每一项,并且调用了 observe 方法去观测他们,我们来看看 observe 方法

export function observe(value: any, asRootData: ?boolean): Observer | void {
	// 判断是不是对象
	if (!isObject(value) || value instanceof VNode) {
		return
	}
	// Observe
	let ob: Observer | void;
	// 判断 value 是否已经被观测
	// 如果已经观测,value.__ob__就是 Observer 对象
	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
	) {
		// 如果判断通过就进行观测
		// 观测是通过 new Observer 进行的
		ob = new Observer(value)
	}
	if (asRootData && ob) {
		ob.vmCount++
	}
	return ob;
}

可以看到, observe 只会对没有观测过的数据和对象类型的数据进行观测,所以 observeArray 实际上是观测数组上的对象,并不观测下标,我们可以验证一下。

<template>
    <div class="home">
        <p v-for="(game, index) in gameList" :key="index"></p>
        <button @click="click">button</button>
    </div>
</template>
<script>
    export default {
        name: 'home',
        data() {
            return {
				name : "sena",
				age : "16",
				school : {
					name : "school name"
				},
				gameList : [{
					gameName : "GTA6"
                }, {
					gameName : "MONSTER HUNTER : WORLD"
                }]
            }
        },
        methods : {
            click() {
                console.log(this);
            }
        },
		mounted() {
			window.vm = this;
		},
		beforeDestroy() {
			window.vm = null;
		}
    }
</script>

file

很明显,数组里的 [0], [1] 都不是访问器属性的,但是数组里的对象被观测了,这就是为什么我们平时通过下标修改数组是不会触发更新的。

那么数组的变化侦测是怎么实现的呢,虽然你可能现在不清楚,但你一定知道我们平时想要在修改数组时触发更新,一般都是通过调用 push,shift 这样的函数,这是怎么实现的呢?
在 new Observer 时有这样几句话

if (Array.isArray(value)) {
+		// 修改原型
+	value.__proto__ = arrayMethods;
+	this.observeArray(value)
}

可以看到,数组的原型被修改成了 arrayMethods ,那我们来看看 arrayMethods 的实现

// 数组原来的原型
const arrayProto = Array.prototype;
// 响应式数组原型
export const arrayMethods = Object.create(arrayProto);
// 这些是会修改数组本身的方法
const methodsToPatch = [
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
];
/**
 * Intercept mutating methods and emit events
 * 拦截数组方法
 */
methodsToPatch.forEach(function (method) {
	// cache original method
	const original = arrayProto[method];
	def(arrayMethods, method, function mutator(...args) {
		// 调用原来的方法
		const result = original.apply(this, args);
		// 这是保存在被观测对象上的 Observer
		const ob = this.__ob__;
		// 这是新插入数组的值
		let 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
	})
});

其实原理也很简单,vue 对数组的方法做了拦截,如果调用这些会修改数组本身的方法,就会走到被拦截的方法里,然后在观测新添加的值后调用 ob.dep.notify() , 通知组件 watcher 更新,顺带讲点细节问题,还记得 Observr 的构造函数吗

constructor(value: any) {
	this.value = value;
	// 这个 dep 是数组专用的,在数组的拦截方法里调用 ob.dep.notify (); 实际上就是调用了这个 dep
	this.dep = new Dep();
	// 定义__ob__, 这个属性会在之后用上
	// 这里把定义了访问器属性 value.__ob__,值是 this,也就是 observer
	def(value, '__ob__', this);
	if (Array.isArray(value)) {
		// 修改原型
		value.__proto__ = arrayMethods;
		this.observeArray(value)
	} else {
		this.walk(value)
	}
}

this.depvalue.__ob__ 都是有目的的,前者是为了能在数组的拦截方法中访问到 dep ,用于触发组件更新,后者是为了能在数组的拦截方法中访问到 Observer ,当然也可以标识这个对象是否已经被观测。

除了上面的地方,我们还要知道,针对 value 是数组的情况,vue 也在 defineReactive 这里做了一些处理,我们来看一下

export function defineReactive(
	obj: Object,
	key: string,
	val: any
) {
	const dep = new Dep();

	//  递归调用进行观测
	let childOb = observe(val);

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			// getter收集依赖
			// 获取属性对应的值
			const value = val;
			// Dep.target会存放当前组件的渲染Watcher
			if (Dep.target) {
				// 这句话会把Dep.target添加到dep的中
				// 也就是保存了watcher
				dep.depend();
+				if (childOb) {
+                   childOb.dep.depend();
+               }
			}
			return value
		},
		set: function reactiveSetter(newVal) {
			// setter派发更新
			const value = val;
			// 判断新旧值是否相等, 相等就不触发派发更新
			if (newVal === value || (newVal !== newVal && value !== value)) {
				return
			}
			val = newVal;
			// 果新的值也是对象,也会进行观测
			childOb = observe(newVal);
			// 这里通知渲染watcher执行update方法,进行组件更新
			dep.notify()
		}
	})
}

可以看到在 get 里加了一个调用 childOb.dep.depend 的逻辑,那么为什么要这么做呢,我们可以在数组的拦截方法里调用 dep.notify ,但是问题是,怎么把渲染 watche r 添加到 dep 中,也许你会说,在 get 中使用 dep.depend() ,然而并不是这样的,在 get 中调用的 dep 是依赖这个属性的,在数组拦截的方法中能获得的 dep,是 observer.dep 这个属性,那怎么办呢,vue 的做法也很简单,首先递归调用 observe ,把返回值保存在 childOb 上,然后在 get 中添加上面的几行, childOb 如果存在, value 一定是数组或者对象类型,那么就会调用 childOb.dep.depend 把渲染 watcher 保存起来, childOb.dep 就是在数组拦截的函数中能获取到的 dep ,这些方法也会用这个 dep 触发渲染 watcher 重新渲染组件。

我们来做个对比,如果,在源码中注释掉新增的这几行
然后就会变成这样

可以从图中看到,就算通过 push 方法修改了数组也没有触发组件更新,这就是因为 observe.dep 这个位置上没有保存渲染 watcher

然后把注释去掉
file
这次在 push 后,组件成功刷新,原因就是我上面说的 dep 里保存了渲染 watcher

你以为到这里就结束了?不不不,我们还有最后一个问题要解决,那就是数组套数组的问题

# 多层数组

我们修改下代码

<template>
    <div class="home">
        <p></p>
    </div>
</template>
<script>
    export default {
        name: 'home',
        data() {
            return {
                arr : [[1,2,3],4]
            }
        },
		mounted() {
			window.vm = this;
		},
		beforeDestroy() {
			window.vm = null;
		}
    }
</script>

然后我们测试一下
file
很明显,生产的数组没有保存渲染 watcher,那要怎么做呢,我们还要对 defineReactive 稍加改进

export function defineReactive(
	obj: Object,
	key: string,
	val: any
) {
	const dep = new Dep();

	//  递归调用进行观测
	let childOb = observe(val);

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			// getter收集依赖
			// 获取属性对应的值
			const value = val;
			// Dep.target会存放当前组件的渲染Watcher
			if (Dep.target) {
				// 这句话会把Dep.target添加到dep的中
				// 也就是保存了watcher
				dep.depend();
				if (childOb) {
                   childOb.dep.depend();
+				   if (Array.isArray(value)) {
+                       dependArray(value);
+                  }
               }
			}
			return value
		},
		set: function reactiveSetter(newVal) {
			// setter派发更新
			const value = val;
			// 判断新旧值是否相等, 相等就不触发派发更新
			if (newVal === value || (newVal !== newVal && value !== value)) {
				return
			}
			val = newVal;
			// 果新的值也是对象,也会进行观测
			childOb = observe(newVal);
			// 这里通知渲染watcher执行update方法,进行组件更新
			dep.notify()
		}
	})
}

可以看到,新增的代码在判断 value 是否为 Array 类型后,调用了一个叫做 dependArray 的方法,那么这个方法做了什么呢

function dependArray(value) {
	// 遍历数组
    for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
        e = value[i];
		// 看看数组这一项上有没有__ob__这个属性(就是说这一项是不是数组或者对象)
		// 有就调用 dep.depend ()
        e && e.__ob__ && e.__ob__.dep.depend();
        if (Array.isArray(e)) {
			// 如果这个项是 Array 类型就继续递归
            dependArray(e);
        }
    }
}

可以看到,vue 的处理是,触发深层所有数组保存的 observer.dep.depend() 方法,从而让所有的子数组都能保存渲染 watcher。
我们去掉源码中的注释,然后再测试一下
file
现在就好了,深层的数组也能被观测了

# 后记

好啦这次的分享就到这里了,如果有错误的地方,欢迎各位大佬指正,那么下次见(咕咕咕)。顺带祝各位大佬们 520 快乐。