深入剖析:Vue核心之MVVM原理其手动实现数据双向绑定

前言

当被问到Vue是如何实现数据的双向绑定,大部分人的回答是:其内部是通过Object.definedProperty()的get和set方法实现的。其核心原理是通过这个API实现,但是还是有必要理解整个过程的实现和其运行原理。

什么是MVVM模式

MVVM模式是Model-View-ViewModel的简写,即模型-视图-视图模型。【模型】指的是数据层。【视图】指的是视图层所看到的页面。【视图模型】MVVM模式的核心,它是连接view和model的桥梁。

数据层和视图层直接是不能直接通信的,那么模型(数据层)如何转换成视图:通过绑定数据。视图如何转换成数据:通过DOM事件监听。当这个两个都实现了,就完成了数据的双向绑定。MVVM模式Model和View是通过ViewModel作为桥梁进行通信的。

Vue是如何利用MVVM原理实现数据的双向绑定

在大致了解了MVVM的原理之后,接下来就一起来探讨vue是如何实现数据的双向绑定。

  • 问题一:什么是数据的双向绑定

如下代码:当数据a发生变化的时候,视图会发生变化;当用户输入视图发生变化时,数据也会发生变化,即这个过程就是数据的双向绑定。

<template>
  <div id="app">
	<input @input="handler" value="a"/>
  </div>
</template>
<script>
export default {
  name: 'app',
	data(){
		return {
				a:1
		}
	},
	methods:{
		handler(){
			
		}
	},
}
</script>
  • 问题二:如何实现数据的双向绑定
    当我们修改a的值比如this.a=10,为什么视图发生了变化?并且这中间Vue做了什么事?
    1、Vue类首先通过Object.defineProperty进行了data选项的代理,当访问this.a实际上是访问this.$data.a
    2、将data传入Observe类将数据通过Object.defineProperty方法的get,set重新定义,实现数据劫持
    3、调用Complier类开启模板初始化编译
    其代码如下:
class Vue {
	constructor(option) {
		//this.$el $data $options
		this.$el = option.el;
		this.$data = option.data;
		this.computed = option.computed;
		this.methods = option.methods;
		//如果存在根元素 就编译模板
		if (this.$el) {
			//把数据 全部转化成用 Object.defineProperty来定义
			new Observe(this.$data);
			for (let key in this.computed) { //根据依赖的数据添加watcher
				Object.defineProperty(this.$data, key, {
					get: () => {
						return this.computed[key]();
					},
					set: (newVal) => {

					}
				})
			}
			//将methods对象代理到vm头上
			for (let key in this.methods) {
				Object.defineProperty(this, key, {
					get: () => {
						return this.methods[key];
					}
				})
			}
			//数据获取操作 vm上的取值操作
			//都代理到 vm.$data
			this.getVmProxy()
			new Compiler(this.$el, this);
		}

	}
	getVmProxy() {
		for (let key in this.$data) {
			Object.defineProperty(this, key, { //实现可以通过vm取到对应的内容
				get: () => {
					return this.$data[key];
				},
				set: (value) => {
					this.$data[key] = value;
				}
			})
		}
	}
}
  • 问题三:如何实现Complier类和Observer类。
    Complier类代码如下
class Compiler {
	constructor(el, vm) {
		this.vm = vm;
		this.el = this.isElementNode(el) ? el : document.querySelector(el);
		//把当前节点中的元素获取到放到内存中

		let fragment = this.node2fragment(this.el)
		//把节点中的内容进行替换

		//编译模板  用数据编译
		this.compile(fragment)
		//把这个内容在塞到页面中
		this.$el.appendChild(fragment);
	}
	isDirective(attrName) {
		return attrName.startWith('v-');
	}
	compileElement(node) {
		let attr = node.attributes; //类数组
		[...attr].forEach(atr => {
			let {
				name,
				value: expr
			} = atr;
			if (this.isDirective(name)) {
				console.log(node) //指令元素
				let [, directive] = name.split('-');
				let [directiveName, eventName] = directive.split('.'); //处理v-on:click指令
				CompilerUtils[directiveName](node, expr, )
				CompilerUtils[directive](node, expr, this.vm, eventName)
			}
		})
	}
	compileText(node) {
		let content = node.textcontent; //节点的文本
		if (/\{\{(.+?)\}\}/.test(content)) {
			console.log(content) //找到所有文本
			CompilerUtils['text'](node, content, this.vm)
		}
	}
	//用来编译内存中的dom节点
	compile(node) {
		let childNodes = node.childNodes; //dom的第一层
		[...childNodes].forEach(child => {
			if (this.isElementNode(child)) {
				console.log('element');
				this.compileElement(child);
				//如果是元素的话,需要遍历子节点
				this.compile(child);
			} else {
				console.log('text');
				this.compileText(child)
			}
		})
	}
	node2fragment(node) {
		//把所有的节点都拿到,创建一个文档碎片。
		let fragment = document.createDocumentFragment();
		let firstChild;
		while (firstChild = node.firstChild) {
			fragment.appendChild(firstChild);
		}
		return fragment;
	}
	isElementNode(node) {
		return node.nodeType === 1
	}
}
//工具类
//处理不同指令不同的功能调用不同的处理方式
CompilerUtils = {
	//获取值
	getValue(vm, expr) {
		let value = expr.split('.').reduce((data, current) => {
			return data[current];
		}, vm.$data)
		return value;
	},
	//设置值
	setValue(vm, expr, value) {
		return expr.split('.').reduce((data, current, index, arr) => {
			if (index === arr.length - 1) {
				return data[current] = value;
			}
			return data[current];
		}, vm.$data)
	},
	model(node, expr, vm) {
		let fn = this.updater['modelUpdater']
		let value = this.getValue(vm, expr);
		new Watcher(vm, expr, (newValue) => {
			fn(node, newValue);
		})
		node.addEventListener('input', (e) => {
			let value = e.target.value;
			this.setValue(vm, expr, value);
		})
		fn(node, value);
	},
	on(node, expr, vm, eventName) {
		node.addEventListener(eventName, (e) => {
			return vm[expr].call(vm, e);
		})
	},
	html() {

	},
	getContentValue(vm, expr) {
		return expr.replace(/\{\{(.+?)\}\}/g, (...agrs) => {
			return this.getValue(vm, args[1]);
		})
	},
	text(node, expr, vm) { //expr {{a}} {{b}} {{c}}
		let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
			new Watcher(vm, args[1], () => {
				let value = this.getContentValue(vm, expr); //返回了一个全的字符串
				fn(node, value)
			})
			return this.getValue(vm, args[1]);
		})
		let fn = this.updater['textUpdater'];
		fn(node, content);
	},
	updater: {
		modelUpdater(node, value) {
			node.value = value;
		},
		htmlUpdater(node, value) {
			node.innerHTML = value;
		},
		textUpdater(node, value) {
			node.textContent = value; //将值放在节点的文本内容上
		}
	}
}

在初始化编译的过程中就是在收集每一个watcher。比如v-model指令:先通过vm获取值挂载到到视图模板,实现视图层数据的展示。然后订阅数据则是通过new Watcher实例传入keyname,回调函数。(Watcher的实现稍许片刻)最后通过事件监听input事件,当数据发生改变时,重新设置数据。在重新设置数据的过程其就是调用Object.defineProperty的set方法,那么在set方法里执行发布每一个收集的依赖,并重新设置值。
Observer类代码如下

class Observe {
	constructor(obj) {
		console.log(obj);
		this.observe(obj);
		//对数组的原生方法进行重写
		let arrayProto = Array.prototype; //先存一份原生的原型
		let proto = Object.create(Array.prototype); //复制一份一模一样原生的原型。
		['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(item => {
			proto[item] = function(...ary) {
				let insert; //数组新增的元素也要进行观察
				switch (item) {
					case 'push':
						insert = ary;
					case 'unshift':
						insert = ary;
					case 'splice':
						insert = ary.slice(2);
					default:
						break;
				}
				this.observer(insert);
				console.log('视图更新')
				return arrayProto[item].call(arrayProto, ...ary);
			}
		})
	}
	observerArray(array) {
		for (let i = 0, len = array.length; i < len; i++) {
			let item = array[i];
			this.observer(item);
		}
	}
	observer(obj) {
		if (typeof obj !== 'object' || obj === null) {
			return obj;
		}
		if (Array.isArray(obj)) {
			Object.setPrototypeOf(obj, proto) //设置数组的原型,进行数组方法重写
			this.observerArray(obj) //对数组已有的元素进行检测
		} else {
			for (let key in obj) {
				this.defineReactive(key, obj[key], obj)
			}
		}
	}
	defineReactive(obj, key, value) {
		Observe(value);
		let dep = new Dep()
		Object.defineProperty(obj, key, {
			get: () => {
				Dep.target && dep.addSubs(Dep.target);
				return value;
			},
			set: (newValue) => {
				this.observe(newValue);
				if (value != newValue) value = newValue;
				dep.notify();
			}
		})
	}
}

Observer类就是对每个在data选项上定义的数据进行劫持检测。通过Object.defineProperty的get和set方法实现watcher实例的添加和发布。

  • 问题四:观察者Watcher
    Watcher类通过在构造器上获取keyName所对应的具体值,在这个过程触发了Object.defineProperty()的get方法,并且将自己(watcher实例)赋值给Dep类的target静态属性。那么,Object.defineProperty()通过new 一个dep实例并通过get方法将dep实例身上的target属性进行watcher实例的收集。
class Watcher {
	constructor(vm, expr, cb) {
		this.vm = vm;
		this.expr = expr;
		this.cb = cb;
		this.oldval = this.get()
	}
	get() {
		Dep.target = this;
		let value = CompilerUtils.getValue(this.vm, this.expr);
		Dep.target = null;
		return value;
	}
	updater() { //更新操作  数据变化后 会调用观察者的update方法
		let newValue = CompilerUtils.getValue(this.vm, this.expr);
		if (newValue !== this.oldval) {
			this.cb(newValue);
		}
	}
}
  • 问题五:被观察者Dep类的实现
    收集完成每一个watcher实例之后,当数据发生改变时又会触发Object.defineProperty()的set方法,那么通过dep实例的发布函数,触发watcher的updater函数,并将新的数据传入回调函数,回调函数触发视图层更新。
class Dep {
	constructor() {
		this.subs = []; //存放watcher
	}
	//订阅
	addSubs(watcher) {
		this.subs.push(watcher)
	}
	//发布
	notify() {
		this.subs.forEach(watcher => {
			watcher.updater();
		})
	}
}

上面的每个类可通过如下图的关系进行进一步理解:
在这里插入图片描述

总结

以上是我对MVVM原理在数据双向绑定运用的理解,通过监听器 Observer 、被观察者 Dep 、观察者 Watcher 和解析器 Complier类的实现,帮助大家了解数据双向绑定的基本原理。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值