【Vue2源码分析与实现】(一)变化侦测

97 阅读3分钟

(一)变化侦测

  • 初始化
  • Object变化侦测
  • Array变化侦测

observe流程图

image.png

1、初始化

  • 定义Vue构造函数
  • 向Vue原型混入操作方法,方便后期扩展
  • 在初始化函数中进行 state初始化 -> data初始化
// index.js
import {initMixin} from "init.js"
const Vue = function(options){
	// 选项初始化
	this._init(options);
}
// 向Vue原型混入操作方法
initMixin(Vue);
...

export default Vue;
// init.js
import {initState} from "state.js"
export function initMixin(Vue){
	Vue.prototype._init = function(options){
		const vm = this;
		vm.$options = options;
		// 初始化状态
		initState(vm);
	}
}
// state.js 初始化状态
import {observe} from 'observe/index.js'

export function initState(vm){
	const opt = vm.$options;
	if(opt.data){
		// 初始化data
		initData(vm);
	}
}

function initData(vm){
	let data = vm.$options.data;
	// 判断data是否为函数
	data = typeof data === 'function' ? data.call(vm) : data;
	// 对统一后的data对象重新挂载在vm实例上
	vm._data = data
 	// 数据侦测与劫持
	observe(data);
}

2、Object变化侦测

Object.defineProperty()缺点

  • 不能侦测新增与删除属性

2.1 数据劫持

// observe/index.js
// 对数据进行侦测/重写 返回可侦测对象
export function observe(data){
	// 非对象类型不进行劫持
	if(typeof data != 'object' || data == null) return;
	return new Observe(data);
}

// 数据侦测类
class Observe {
	constructor(data){
		this.walk(data);
	}
	// 对象数据劫持 - 相当于重写 性能瓶颈
	walk(){
		Object.keys.forEach(key => {
			defineReactive(data,key,data[key])
		})
	}
}

// 数据劫持公共方法
export const defineReactive(target,key,value){
	Object.defineProperty(target,key,{
		enumerable:true,	// 默认也为true
		configurable:true,	// 同上
		get(){
			return value; 	// 闭包
		},
		set(newVal){ 
			if(newVal === value) return;
			value = newVal;
		}
	})
}

此时我们可以通过observe方法对传入的对象进行数据侦测,劫持数据的取值更改 但是数据是在_data上的,为了开发模式语法尽量简洁,这里需要数据代理

2.2 数据代理

// state.js 对initData进行补充
function initData(vm){
	//...other code
	for(let key in data){
		proxy(vm,'_data',key);
	}
}
function proxy(vm,target,key){
	Object.defineProperty(vm,key,{
		get(){
			return vm[target][key];
		},
		set(newVal){
			vm[target][key] = newVal;
		}
	})
}
const vm = new Vue({
	data(){
		return {
			name:"foo",
			age:11
		}
	}
})

当我们执行以上代码时可以在vm上读取到name属性,并且name与age都拥有gettersetter

2.3 深度侦测与新值侦测

当data中的值是嵌套的对象,以及对data属性设置对象值时,我们希望仍然对其进行侦测, 并且对于已经侦测的数据不再进行重写

// obseve/index.js
export function observe(data){
	...
	// data已经存在了Observe的一个实例 说明已经被侦测过
	if(data.__ob__ instanceof Observe) return data.__ob__
	...
}
class Observe {
	constructor(data){
		Object.defineProperty(data,"__ob__",{
			value:this, // 直接使用实例赋值 后面数组侦测需要该属性
			enumerable:true // 防止深度侦测时 递归爆栈
		})
		...
	}
}
export const defineReactive = function(target,key,value){
	// 深度侦测
	observe(value);

	Object.defineProperty(target,key,{
		...
		set(newVal){
			...
			// 新值侦测
			observe(value);
			...
		}
	})
}

3、Array变化侦测

上述observe流程图中的hasMethod判断是指数组调用的方法是否在被重写列表中

image.png

数组一般数据元素较多,如果逐个下标进行侦测,会浪费性能,因为相较于对下标的修改我们更常使用的是数组方法修改

注意并不是**Object.defineProperty**不能侦测,而是Vue在设计时抛弃了侦测下标这种方式

  • 数组中引用数据类型的元素依然使用observe进行侦测
  • 对能修改原数组的方法进行切面补充(原型链继承的方式)
// observe/index.js
import {newArrayProto} from "observe/array.js"

...
class Observe {
	constructor(data){
		...
		//数组类型判断
		if(Array.isArray(data)){
			// 设置data的原型对象
			Object.setPrototypeOf(data,newArrayProto)
			this.observeArray(data)
		}else {
			
		}
	}
	// 数组劫持
	observeArray(data){
		// 元素为数组/对象进行递归劫持
		data.forEach(item => observe(item))
	}
}
...
// observe/array.js
let originArrayProto = Array.prototype
// 创建一个以originArrayProto为原型的对象
export let newArrayProto = Object.create(originArrayProto);
// 修改数组的7种方法
const methods = ["push","pop","unshift","shift","sort","splice","reverse"]

methods.forEach(method => {
	newArrayProto[method] = function(..args){
		// 调用原始方法
		const result = originArrayProto[method].apply(this,args);
		const ob = this.__ob__; // __ob__为上述添加的Observe实例
		// 数据劫持新数据
		let inserted;
		switch(method){
			case 'push':
			case 'unshift':
				inserted = args;
				breake;
			case 'splice':
				inserted = args.slice(2);// 第三个参数为新数据
				break;
		}
		// 新数据侦测
		if(inserted){
			ob.observeArray(inserted);
		}
		return result;
	}
})

通过原型链继承将中间层对象设置为原数据的原型对象,是一种面向切面编程的方式,只重写部分方法

__ob__是原数据的一个属性,值为Observe的实例,可以通过它劫持新增的元素数组