Object.defineProperty响应式原理 | 8月更文挑战

107 阅读2分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

vue2.0的Object.defineProperty()响应式原理:监听数据变化,并在视图中更新(模板引擎和渲染)

一、监听数据变化:

首先说下vue在实现响应式原理的时候工作流程是什么:

  • new Vue()首先执行初始化,对data中数据进行响应化处理,源码中是在Observer
  • 对模板进行编译,找到动态绑定的数据,在data中获取并初始化视图,在Compile里执行
  • 创建Dep管理多个Watcher,对data数据中每个key进行管理,后面数据如果有变化,就通知watcher执行更新函数。

遍历需要响应化的对象进行初始化Observer,下面是手写实例:

//对data进行初始化,进行响应处理
function observe(obj) {
	//判断对象还是数组等,源码更详细
 	if (typeof obj !== 'object' || obj == null) {
                //如果新增对象或修改删除对象无法检测
	 	return;
 	}
 	new Observer(obj)
}
//对每个对象进行响应化处理
function defineReactive(obj, key, val) {
	class Vue {
	 	constructor(options) {
	 		this.$options = options;
	 		this.$data = options.data;
	 		observe(this.$data);
	 	}
	}
	class Observer {
	 	constructor(value) {
	 		this.value = value
	 		this.walk(value);
 		}
 		walk(obj) {
	 		Object.keys(obj).forEach(key => {
	 			defineReactive(obj, key, obj[key]);//递归解决对象嵌套的问题
			})
	 	}
	}
}
//创建vue实例
class Vue {
 	constructor(options) {
		//……
 		proxy(this)
 	}
}
//对data进行代理,设置get,set方法
function proxy(vm) {
 	Object.keys(vm.$data).forEach(key => {
 		Object.defineProperty(vm, key, {
 			get() {
 				return vm.$data[key];
 			},
			set(newVal) {
 				vm.$data[key] = newVal;
 			}
 		});
 	})
}

二、依赖收集Compile

data对象中会有key,一个key可能会出现多次,所以每次都要收集出来用一个Watcher维护,这个过程叫依赖收集,多个Watcher都要用Dep来管理,等后面要更新的时候由Dep统一通知页面更新渲染。

//声明Dep
class Dep {
 	constructor () {
 		this.deps = []
 	}
 	addDep (dep) {
 		this.deps.push(dep)
 	}
 	notify() {
 		this.deps.forEach(dep => dep.update());//update是通知页面更新,这里不详细写了
 	}
}
//创建watcher并触发getter
class Watcher {
 	constructor(vm, key, updateFn) {
	 	Dep.target = this;
	 	this.vm[this.key];
	 	Dep.target = null;
 	}
}
// 依赖收集,创建Dep实例
defineReactive(obj, key, val) {
 	this.observe(val);
 	const dep = new Dep()
 	Object.defineProperty(obj, key, {
 		get() {
 			Dep.target && dep.addDep(Dep.target);
 			return val;
 		},
 		set(newVal) {
	 	if (newVal === val) return
		 	dep.notify()
	 	}
 	})
}

三、模板引擎:

  • 使用插值:{{}},两个大括号来包括变量名

  • 使用指令:v-model、v-bind、v-on、v-for、v-if、v-show,使用这些指令来渲染,下面试着实现vue的模板引擎渲染,并获取指令:

    class Compile { constructor(el, vm) { this.vm=vm;this.vm = vm; this.el = document.querySelector(el); if(this.el) { this.compile(this.el); } } compile(el) { const childNodes = el.childNodes; Array.from(childNodes).forEach(node => { if(this.isElement(node)) {//编译元素 this.compileElement(node); }else if(this.isInterpolation(node)) {//编译插值⽂本 node.textContent = this.vm[RegExp.vm[RegExp.1]; } if(node.childNodes && node.childNodes.length > 0) { this.compile(node); } }); } isElement(node) { return node.nodeType == 1; } isInterpolation(node) { return node.nodeType == 3 && /{{(.*)}}/.test(node.textContent); } //编译元素 compileElement(node) { let nodeAttrs = node.attributes; Array.from(nodeAttrs).forEach(attr => { let attrName = attr.name; let exp = attr.value; if (this.isDirective(attrName)) {//通过指令编译元素属性名 let dir = attrName.substring(2); this[dir] && this[dir](node, exp); } }); } //获取指令,v-开头,源码中更详细,会有冒号等正则进行判断,这里简单写 isDirective(attr) { return attr.indexOf("v-") == 0; } }

最后把模板引擎转换为虚拟dom(就是vdom)再转换为真实dom,首先获取指令(v-开头)然后根据是什么类型进行不同的渲染,例如v-text或者v-model就要进行不同的逻辑这里自己写的很简单,详细的可以自己看下源码。