手写MVVM

// 创建一个Mvvm构造函数
// 这里用es6方法将options赋一个初始值,防止没传,等同于options || {}
function Mvvm(options = {}) {
	// vm.$options Vue上是将所有属性挂载到上面
	// 所以我们也同样实现,将所有属性挂载到了$options
	console.log(options)
	this.$options = options;
	// this._data 这里也和Vue一样
	let data = this._data = this.$options.data;

	// 数据劫持
	observe(data);
	// this 代理了this._data
	for(let key in data) {
		Object.defineProperty(this, key, {
			configurable: true,
			get() {
				return this._data[key]; // 如this.a = {b: 1}
			},
			set(newVal) {
				this._data[key] = newVal;
			}
		});
	}
	// 初始化computed,将this指向实例
	initComputed.call(this);
	// 编译    
	new Compile(options.el, this);
	// 所有事情处理好后执行mounted钩子函数
	options.mounted.call(this); // 这就实现了mounted钩子函数
}
// 创建一个Observe构造函数
// 写数据劫持的主要逻辑
function Observe(data) {
	let dep = new Dep();
	// 所谓数据劫持就是给对象增加get,set
	// 先遍历一遍对象再说
	for(let key in data) { // 把data属性通过defineProperty的方式定义属性
		let val = data[key];
		observe(val); // 递归继续向下找,实现深度的数据劫持
		Object.defineProperty(data, key, {
			configurable: true,
			get() {
				Dep.target && dep.addSub(Dep.target);
				return val;
			},
			set(newVal) { // 更改值的时候
				if(val === newVal) { // 设置的值和以前值一样就不理它
					return;
				}
				val = newVal; // 如果以后再获取值(get)的时候,将刚才设置的值再返回去
				observe(newVal); // 当设置为新值后,也需要把新值再去定义成属性
				dep.notify(); // 让所有watcher的update方法执行即可
			}
		});
	}
}

// 外面再写一个函数
// 不用每次调用都写个new
// 也方便递归调用
function observe(data) {
	// 如果不是对象的话就直接return掉
	// 防止递归溢出
	if(!data || typeof data !== 'object') return;
	return new Observe(data);
}

// 创建Compile构造函数
function Compile(el, vm) {
	// 将el挂载到实例上方便调用
	vm.$el = document.querySelector(el);
	console.log(vm.$el)
	// 在el范围里将内容都拿到,当然不能一个一个的拿
	// 可以选择移到内存中去然后放入文档碎片中,节省开销
	let fragment = document.createDocumentFragment();
	console.log(fragment)
	console.log(vm.$el.firstChild)
	while(child = vm.$el.firstChild) {
		fragment.appendChild(child); // 此时将el中的内容放入内存中
	}
	console.log(fragment.childNodes)
	// 对el里面的内容进行替换
	function replace(frag) {
		Array.from(frag.childNodes).forEach(node => {
			console.log(node, node.textContent)
			let txt = node.textContent;
			let reg = /{{(.*?)}}/g; // 正则匹配{{}}

			if(node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}
				//				console.log(RegExp.$1); // 匹配到的第一个分组 如: a.b, c
				//				let arr = RegExp.$1.split('.');
				//				console.log(arr)
				//				let val = vm;
				//				arr.forEach(key => {
				//					val = val[key]; // 如this.a.b
				//				});
				//				// 用trim方法去除一下首尾空格
				//				node.textContent = txt.replace(reg, val).trim();
				function replaceTxt() {
					node.textContent = txt.replace(reg, (matched, placeholder) => {
						console.log(placeholder); // 匹配到的分组 如:song, album.name, singer...
						new Watcher(vm, placeholder, replaceTxt); // 监听变化,进行匹配替换内容

						return placeholder.split('.').reduce((val, key) => {
							return val[key];
						}, vm);
					});
				};
				// 替换
				replaceTxt();

			}
			if(node.nodeType === 1) { // 元素节点
				let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
				console.log(node.attributes)
				Array.from(nodeAttr).forEach(attr => {
					let name = attr.name; // v-model  type
					let exp = attr.value; // c        text
					if(name.includes('v-')) {
						node.value = vm[exp]; // this.c 为 2
					}
					console.log(attr.value)
					//					debugger
					// 监听变化
					new Watcher(vm, exp, function(newVal) {
						node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
					});

					node.addEventListener('input', e => {
						debugger
						let newVal = e.target.value;
						// 相当于给this.c赋了一个新值
						// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
						vm[exp] = newVal;
						console.log(vm)
					});
				});
			}

			// 如果还有子节点,继续递归replace
			if(node.childNodes && node.childNodes.length) {
				replace(node);
			}
		});
		// 监听变化
		// 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
		new Watcher(vm, RegExp.$1, newVal => {
			node.textContent = txt.replace(reg, newVal).trim();
		});

	}

	replace(fragment); // 替换内容

	vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}

// 发布订阅模式  订阅和发布 如[fn1, fn2, fn3]
function Dep() {
	// 一个数组(存放函数的事件池)
	this.subs = [];
}
Dep.prototype = {
	addSub(sub) {
		this.subs.push(sub);
	},
	notify() {
		// 绑定的方法,都有一个update方法
		this.subs.forEach(sub => sub.update());
	}
};
// 监听函数
// 通过Watcher这个类创建的实例,都拥有update方法
function Watcher(vm, exp, fn) {
	this.fn = fn; // 将fn放到实例上
	this.vm = vm;
	this.exp = exp;
	// 添加一个事件
	// 这里我们先定义一个属性
	Dep.target = this;
	console.log(exp)
	let arr = toString(exp).split('.');
	let val = vm;
	arr.forEach(key => { // 取值
		val = val[key]; // 获取到this.a.b,默认就会调用get方法
	});
	Dep.target = null;
}
Watcher.prototype.update = function() {
	// notify的时候值已经更改了
	// 再通过vm, exp来获取新的值
	console.log(this.exp)
	let arr = toString(this.exp).split('.');
	let val = this.vm;
	arr.forEach(key => {
		val = val[key]; // 通过get获取到新的值
	});
	console.log(this)
	this.fn(val);
};

let watcher = new Watcher(() => console.log(111)); // 
let dep = new Dep();
dep.addSub(watcher); // 将watcher放到数组中,watcher自带update方法, => [watcher]
dep.addSub(watcher);
dep.notify(); //  111, 111
function initComputed() {
	let vm = this;
	let computed = this.$options.computed; // 从options上拿到computed属性   {sum: ƒ, noop: ƒ}
	// 得到的都是对象的key可以通过Object.keys转化为数组
	Object.keys(computed).forEach(key => { // key就是sum,noop
		Object.defineProperty(vm, key, {
			// 这里判断是computed里的key是对象还是函数
			// 如果是函数直接就会调get方法
			// 如果是对象的话,手动调一下get方法即可
			// 如: sum() {return this.a + this.b;},他们获取a和b的值就会调用get方法
			// 所以不需要new Watcher去监听变化了
			get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
			set() {}
		});
	});
}

  

二:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>双向绑定实现</title>
	<style>
		#app {
			text-align: center;
		}
	</style>
</head>
<body>
	<div id="app">
		<h2>{{title}}</h2>	
		<input v-model="name">
		<h1>{{name}}</h1>
		<button v-on:click="clickMe">点击!</button>
	</div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script>

	new Vue({
		el: '#app',
		data: {
			title: 'vue code',
			name: '练习'
		},
		methods: {
			clickMe() {
				this.title = 'vue code click';
			}
		},
		mounted() {
			window.setTimeout(() => {
				this.title = '1秒';
			}, 1000);
		}
	});

</script>
</html>

index.js

function Vue(options) {
	var self = this;
	this.data = options.data;
	this.methods = options.methods;

	Object.keys(this.data).forEach(function(key) {
		console.log(key);	// title  name
		self.proxyKeys(key);
	});

	observe(this.data);
	new Compile(options.el, this);
	options.mounted.call(this);		// 所有事情处理好后执行mounted函数
}

Vue.prototype = {
	proxyKeys: function(key) {
		var self = this;
		Object.defineProperty(this, key, {
			enumerable: false,
			configurable: true,
			get: function() {
				return self.data[key];
			},
			set: function(newVal) {
				self.data[key] = newVal;
			}
		});
	}
};

  

 

 observer.js

function Observer(data) {
	this.data = data;
	this.walk(data);
}

Observer.prototype = {
	walk: function(data) {
		var self = this;
		Object.keys(data).forEach(function(key) {
			self.defineReactive(data, key, data[key]);
		});
	},
	defineReactive: function(data, key, val) {
		var dep = new Dep();
		
		Object.defineProperty(data, key, {
			enumerable: true,
			configurable: true,
			get: function getter() {
				if (Dep.target) {
					console.log(Dep.target)
					dep.addSub(Dep.target);
				}
				return val;
			},
			set: function setter(newVal) {
				if (newVal === val) {
					return;
				}
				val = newVal;
				dep.notify();
			}
		});
	}
};

function observe(val, vm) {
	if (!val || typeof val !== 'object') {
		return;
	}
	return new Observer(val);
}

function Dep() {
	this.subs = [];
}
Dep.prototype = {
	addSub: function(sub) {
		this.subs.push(sub);
	},
	notify: function() {
		this.subs.forEach(function(sub) {
			console.log(sub.update)
			sub.update();
		});
	}
};
Dep.target = null;

  watcher.js

function Watcher(vm, exp, cb) {
	this.vm = vm;
	this.exp = exp;
	this.cb = cb;
	this.value = this.get();	// 将自己添加到订阅器的操作
}

Watcher.prototype = {
	update: function() {
		this.run();
	},
	run: function() {
		var value = this.vm.data[this.exp];
		var oldVal = this.value;
		if (value !== oldVal) {
			this.value = value;
			this.cb.call(this.vm, value, oldVal);
		}
	},
	get: function() {
		Dep.target = this;	// 缓存自己
		var value = this.vm.data[this.exp];		// 强制执行监听器里的get函数
		Dep.target = null;	// 释放自己
		return value;
	}
}

  compile.js

function Compile(el, vm) {
	this.vm = vm;
	this.el = document.querySelector(el);
	this.fragment = null;
	this.init();
}

Compile.prototype = {
	init: function() {
		if (this.el) {
			this.fragment = this.nodeToFragment(this.el);
			this.compileElement(this.fragment);
			this.el.appendChild(this.fragment);
		} else {
			console.log('Dom元素不存在');
		}
	},	
	nodeToFragment: function(el) {
		var fragment = document.createDocumentFragment();
		var child = el.firstElementChild;

		while (child) {
			// 将Dom元素移入fragment中
			fragment.appendChild(child);
			child = el.firstElementChild;
		}

		return fragment;
	},
	compileElement: function(el) {
		var self = this;
		var childNodes = el.childNodes;

		[].slice.call(childNodes).forEach(function(node) {
			var reg = /{{(.*)}}/;
			var text = node.textContent;

			if (self.isElementNode(node)) {
				self.compile(node);
			} else if (self.isTextNode(node) && reg.test(text)) {
				self.compileText(node, reg.exec(text)[1]);
			}

			if (node.childNodes && node.childNodes.length) {
				self.compileElement(node);
			}
		});
	},
	compile: function(node) {
		var self = this;
		var nodeAttrs = node.attributes;

		Array.prototype.forEach.call(nodeAttrs, function(attr) {
			var attrName = attr.name;
			if (self.isDirective(attrName)) {
				var exp = attr.value;
				var dir = attrName.substring(2);	// model  on:click
				if (self.isEventDirective(dir)) {	// 事件命令
					self.compileEvent(node, self.vm, exp, dir);
				} else {	// v-model指令
					self.compileModel(node, self.vm, exp, dir);
				}
				node.removeAttribute(attrName);
			}
		});
	},
	compileText: function(node, exp) {
		var self = this;
		var initText = this.vm[exp];
		this.updateText(node, initText);
		new Watcher(this.vm, exp, function(value) {
			self.updateText(node, value);
		});
	},
	compileEvent: function(node, vm, exp, dir) {
		var eventType = dir.split(':')[1];
		var cb = vm.methods && vm.methods[exp];

		if (eventType && cb) {
			node.addEventListener(eventType, cb.bind(vm), false);
		}
	},
	compileModel: function(node, vm, exp, dir) {
		var self = this;
		var val = this.vm[exp];		// name
		this.modelUpdater(node, val);

		new Watcher(this.vm, exp, function(value) {
			self.modelUpdater(node, value);
		});

		node.addEventListener('input', function(e) {
			var newVal = e.target.value;

			if (val === newVal) {
				return;
			}

			self.vm[exp] = newVal;
			val = newVal;
		});
	},
	updateText: function(node, value) {
		node.textContent = typeof value === 'undefined' ? '' : value;
	},
	modelUpdater: function(node, value, oldVal) {
		node.value = typeof value === 'undefined' ? '' : value;
	},
	isDirective: function(attr) {
		return attr.indexOf('v-') === 0;
	},
	isEventDirective: function(dir) {
		return dir.indexOf('on:') === 0;
	},
	isElementNode: function(node) {
		return node.nodeType === 1;
	},
	isTextNode: function(node) {
		return node.nodeType === 3;
	}
};

  

原文地址:https://www.cnblogs.com/ygunoil/p/15173829.html