SakuraSnow's blog SakuraSnow's blog
首页
  • JavaScript
  • TypeScript
  • Vue
  • React
  • Git
  • Node
  • Linux
  • 技术文档
  • 博客搭建
  • 数据结构
  • leetcode
  • 关于
  • 友链
  • 收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

SakuraSnow

一只前端咸鱼
首页
  • JavaScript
  • TypeScript
  • Vue
  • React
  • Git
  • Node
  • Linux
  • 技术文档
  • 博客搭建
  • 数据结构
  • leetcode
  • 关于
  • 友链
  • 收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基本使用

  • 实现原理

    • 一步步带你看懂vue2的响应式原理
      • 前言
        • 从入口开始
        • 对象的观测
        • 数据的观测
        • 后记
    • 简单说说计算属性的原理
    • 聊聊$nextTick源码
  • Vue笔记
  • 实现原理
SakuraSnow
2020-05-13

一步步带你看懂vue2的响应式原理

# 前言

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

# 从入口开始

// 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)
}
1
2
3
4
5
6
7
8
9

可以看到,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)
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

然后对数据进行处理的是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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们可以从执行的函数名看出,这里完成了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 */)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

我们可以看到,在经过前面的一堆处理和检测后,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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在经过了一系列判断后,如果这个对象没有被观测过,就会通过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
	})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

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

# 对象的观测

# 第一层

假设我们有以下的代码

let vm = new Vue({
	el : "#app",
	data() {
		return {
			name : "sena",
			age : "16"
		}
	}
})
1
2
3
4
5
6
7
8
9

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

constructor(value : any) {
	this.value = value;
	this.walk(value)
}
1
2
3
4

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

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

然后我们看看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()
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

通过执行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()
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

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

<template>
    <div class="home">
        <p>name : {{name}}</p>
        <p>age : {{age}}</p>
        <p>school.name : {{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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

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

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

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

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

现在我们去源码里把第二行注释掉,也就是childOb = observe(newVal)这一句,刷新页面后点击按钮,修改了school.name后依旧可以正常更新组件

那是不是意味着没有问题了呢,当然不是,我们改一下代码

<template>
    <div class="home">
        <p>{{school.name}}</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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

页面刷新后分别点击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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

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

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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

<template>
    <div class="home">
        <p v-for="(game, index) in gameList" :key="index">{{game.gameName}}</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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

file

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

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

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

可以看到,数组的原型被修改成了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
	})
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

其实原理也很简单,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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

this.dep和value.__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()
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

可以看到在get里加了一个调用childOb.dep.depend的逻辑,那么为什么要这么做呢,我们可以在数组的拦截方法里调用dep.notify,但是问题是,怎么把渲染watcher添加到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>{{arr[0]}}</p>
    </div>
</template>

<script>
    export default {
        name: 'home',
        data() {
            return {
                arr : [[1,2,3],4]
            }
        },
		mounted() {
			window.vm = this;
		},
		beforeDestroy() {
			window.vm = null;
		}
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

然后我们测试一下 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()
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

可以看到,新增的代码在判断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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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

# 后记

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

#vue
上次更新: 2022/03/05, 15:57:30
Vue移动端Rem适配方案(适配Vant UI库)
简单说说计算属性的原理

← Vue移动端Rem适配方案(适配Vant UI库) 简单说说计算属性的原理→

最近更新
01
009-Palindrome Number[回文数]
03-10
02
008-String to Integer (atoi)[字符串转整数]
03-10
03
004-Reverse-integer[整数反转]
03-09
更多文章>
Theme by Vdoing | Copyright © 2019-2022 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×