深入剖析:Vue核心之MVVM原理其手动实现数据双向绑定__Vue.js
发布于 3 年前 作者 banyungong 979 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

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

什么是MVVM模式

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

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

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

  • 什么是数据的双向绑定
  • 实现数据的双向绑定
  • 实现Complier类和Observer类。
  • 实现观察者Watcher类
  • 实现被观察者Dep类
  1. 什么是数据的双向绑定

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

    <template>
      <div id="app">
    	<input @input="handler" value="a"/>
      </div>
    </template>
    <script>
    export default {
            name: 'app',
    	data(){
    		return {
    		    a:1
    		}
    	},
    	methods:{
    		handler(){
    			
    		}
    	},
    }
    </script>
    
  2. 实现数据的双向绑定

当我们修改a的值比如this.a=10,为什么视图发生了变化?并且这中间Vue做了什么事?

  • Vue类首先通过Object.defineProperty进行了data选项的代理,当访问this.a实际上是访问this.$data.a

  • 将data传入Observe类将数据通过Object.defineProperty方法的get,set重新定义,实现数据劫持

  • 调用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.computedkey; }, 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;
      			}
      		})
      	}
      }
    

    }

3.如何实现Complier类和Observer类

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,回调函数。最后通过事件监听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方法实现,利用Dep类进行watcher实例的添加和发布。

4.观察者Watcher

Watcher类定义一个get方法用来获取值并将实例绑定到Dep的target属性上。在这个过程也触发了Object.defineProperty的get方法。那么,Object.defineProperty通过new 一个dep实例,在获取值的时候,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);
		}
	}
}

5.被观察者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类的实现,也帮助大家了解数据双向绑定的基本原理。

本人是近日才用掘金,博客的地址是:

https://blog.csdn.net/Miss_hhl/article/details/107333270

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: lin1997 原文链接:https://juejin.im/post/6850418114808119303

回到顶部