欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-30 11:51:56
...

前言

当被问到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();
		})
	}
}

上面的每个类可通过如下图的关系进行进一步理解:
深入剖析:Vue核心之MVVM原理其手动实现数据双向绑定

总结

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

相关标签: vue js