vuejs全家桶原理

一、Vue工作機制

1.1 什麼是mvvm?

Vue就是基於MVVM模式實現的一套構建用戶界面的漸進式框架。MVVM是Model-View-ViewModel的簡寫。其中,Model指的是js中的數據,如對象,數組等等,View指的是頁面視圖,ViewModel指的是vue實例化對象,是連接view和model的橋樑。ViewModel的主要作用是實現Model和View之間的轉換。即將後端傳遞的數據轉化成所看到的頁面,或者將所看到的頁面轉化成後端的數據。我們也稱之爲“數據的雙向綁定”。

1.2 Vue的組成

Observer:監控Vue實例屬性的變化;
Compile:解析指令,負責把數據模型解析成視圖;
Dep:依賴管理器,負責管理所有訂閱者,以及通知所有訂閱者執行更新操作;
Watcher:訂閱者,Dep和Updater的橋樑,用於接收Dep的通知,然後通知Updater執行更新操作;
Updater:更新器。不同類型的指令對應不同的更新器,更新器負責執行更新視圖的操作;

1.3 Vue工作流程

在這裏插入圖片描述
從圖中可以看出,當執行 new Vue() 時,Vue 就進入了初始化階段。初始化階段主要完成兩個工作:
1)Vue 會遍歷 data 屬性,並通過Object.defineProperty 方法爲每一個屬性添加setter和getter方法。這兩個方法實現了數據變化監聽功能;
2)另一方面,Compile(編譯器)對元素節點的指令(如v-for、@click等)進行解析。解析完成後訂閱Watcher 來更新視圖, 此時Wather 會將自己添加到Dep(依賴管理器)中,完成初始化。

當數據發生變化時,Observer 中的 setter方法被觸發,setter 會立即調用Dep.notify()方法,該方法會遍歷所有訂閱的Watcher,並調用 update 方法執行更新操作。另外,查看vue原代碼,發現在vue初始化實例時, 有一個proxy代理方法,它的作用就是遍歷data中的屬性,把它代理到vue的實例上。

下面模擬Vue的核心功能,實現MVVM的數據雙向綁定。

二、自定義MVVM框架

2.1 Object.defineProperty用法

Object.defineProperty函數的作用就是直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性。通過Object.defineProperty()定義屬性,通過描述符的設置可以進行更精準的控制對象屬性。

命令格式:

Object.defineProperty(obj, prop, desc)

obj:需要定義屬性的當前對象
prop:當前需要定義的屬性名
desc:屬性描述符

其中,屬性描述符有以下這些:
在這裏插入圖片描述
1)value:屬性的值。
2)writable:代表該屬性是否可以被改變。writable默認爲false,即屬性的值默認不可以被改變。
3)configrable:是否可配置,以及可否刪除。當configrable爲false,不能刪除當前屬性,且不能重新配置當前屬性的描述符,但是在writable爲true的情況下,可以改變value的值;當configrable爲true時,可以刪除當前屬性,也可以配置當前屬性所有描述符;
4)enumerable:是否會出現在for in 或者 Object.keys()的遍歷中;
5)get:一個給屬性提供getter的方法,如果沒有getter則爲undefined。該方法返回值被用作屬性值。默認爲undefined。
6)set:一個給屬性提供setter的方法,如果沒有setter則爲undefined。該方法將接受唯一參數,並將該參數的新值分配給該屬性。默認值爲undefined。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
	</head>
	<body>
		<div id="app">
			<p>您好,<span id="name"></span></p>	
		</div>
		<script>
			var obj = {};
			Object.defineProperty(obj, 'name', {
				get() {
					return document.getElementById("name").innerHTML;
				},
				set(inner) {
					document.getElementById("name").innerHTML = inner;
				},
			});
			
			console.log(obj.name); // 空
			obj.name = 'zhong';
			console.log(obj.name); // zhong
		</script>
	</body>
</html>

2.2 依賴管理器

依賴管理器主要負責管理所有訂閱者,以及通知所有訂閱者執行更新操作。

第一步:新建一個js文件,並命名爲kvue.js文件;

第二步:新建一個類,並命名爲Dep;

class Dep {

}

第三步:定義constructor方法,該方法初始化watchers數組,該數組用於存儲所有訂閱的watcher;

constructor() {
	this.watchers = [];
}

第四步:定義addWatcher方法,用於往數組添加watcher;

addWatch(w) {
	this.watchers.push(w);
}

第五步:定義notify方法,該方法用於通知watcher執行更新操作;

notify() {
	// 通知所有watch執行更新操作
	this.watchers.forEach(w => {
		w.update();
	});
}

2.3 定義Watcher

第一步:定義一個類,並命名爲Watcher;

class Watcher {

}

第二步:定義constructor方法;

constructor(vue, key, fn) {
	Dep.target = this;  // 將當前wathcer實例添加到Dep.target屬性中
	
	this.vue = vue;
	this.key = key;
	this.fn = fn;
}

vue:代表當前KVue實例;
key:代表data對象中的某個屬性;
fn:更新器函數,負責更新視圖;

第三步:定義一個update方法,模擬視圖更新操作;

update() {
	console.log('視圖更新啦...');
}

2.4 定義KVue實例

第一步:新建一個class,並命名爲KVue;
第二步:定義constructor方法,該方法包含一個options參數,該參數封裝了KVue對象的所有選項;

class KVue {
	constructor(options) {
		this.$methods= options.methods;
		this.$data = options.data;
		this.observe(this.$data);
	}
}

從上面代碼看到,options中應該包含data和methods屬性。

第三步:定義observe方法,該方法用於監聽KVue實例中所有屬性;

observe(obj) {
	if (!obj || typeof obj !== 'object') {
		return;
	}
	// 遍歷對象中所有屬性,並且爲每一個屬性添加setter和getter方法
	Object.keys(obj).forEach(key => {
		this.defineReactive(obj, key, obj[key]);
	});
}

defineReactive(obj, key, val) {
	// 創建依賴管理器
	const dep = new Dep();
	// 爲對象屬性添加setter和getter方法
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get() {
			// 如果Dep.target屬性存在,則將Dep.target屬性中的watcher添加到依賴管理器中存儲起來
			Dep.target && dep.addWatch(Dep.target); 
			return val;
		},
		set(newVal) {
			// 如果修改數據沒有發生變化,則直接返回;否則才進行修改操作
			if (newVal === val) {
				return;
			}
			val = newVal;
			// 修改完成後,通知訂閱watcher執行更新操作
			dep.notify();
		}
	});
}

2.5 定義編譯器

第一步:新建一個js文件,並命名爲compile.js;

第二步:定義一個類,並命名爲Compile;

class Compile {

}

第三步:定義constructor方法;

constructor(el, vue) {
	// 將KVue對象以及el選擇器對應的DOM元素添加到$vue和$el屬性中
	this.$vue = vue; 
	this.$el = document.querySelector(el);
	
	// 如果dom元素存在,則執行編譯操作
	if (this.$el) {
		// 將dom元素轉換爲Fragment,以提高執行效率 
		this.$fragment = this.node2Fragment(this.$el);
		// 執行編譯
		this.compile(this.$fragment);
		// 將編譯後的結果重新添加到宿主元素中
		this.$el.appendChild(this.$fragment);
	}
}

第四步:定義node2Fragment方法,該方法將DOM元素轉換爲fragment對象;

node2Fragment(el) {
	// 創建一個新的Fragment
	const fragment = document.createDocumentFragment();
	let child;
	// 將原生節點移動到Fragment
	while (child = el.firstChild) {
		fragment.appendChild(child);
	}
	return fragment;
}

第五步:定義compile方法,執行編譯操作,即把模版中的關鍵字替換成實際值;

compile(fragment) {
	// 獲取fragment中所有孩子節點
	let childNodes = fragment.childNodes;
	// 遍歷所有孩子節點
	Array.from(childNodes).forEach(child => {
		// 判斷孩子節點的類型,根據不同類型做相應的處理
		if (this.isElementNode(child)) {
			// 如果是元素節點
			this.compileElement(child);
		} else if (this.isTextNode(child) && /\{\{(.*)\}\}/.test(child.textContent)) {
			// 如果是文本節點,只關心{{xx}}格式
			this.compileText(child, RegExp.$1); // RegExp.$1用於獲取正則分組的數據
		}
		// 遞歸遍歷可能存在的子節點
		if (child.childNodes && child.childNodes.length) {
			this.compile(child);
		}
	});
}

第六步:定義isElementNode、isTextNode、compileElement、compileText方法;

// 判斷是否是元素節點,如果是返回true,否則返回false
isElementNode(node) {
	return node.nodeType == 1;
}

// 判斷是否是文本節點,如果是返回true,否則返回false
isTextNode(node) {
	return node.nodeType == 3;
}

// 編譯元素節點
compileElement(el) {
	// 例如:<div k-text="test" @click="clickHandler">
	console.log('編譯元素節點...');
	// 獲取所有屬性節點
	const attrs = el.attributes;
	// 遍歷元素屬性
	Array.from(attrs).forEach(attr => {
		const name = attr.name; // 屬性名,如:k-text 或 @click
		const expr = attr.value; // 屬性值:如:test 或 clickHandler
		if (this.isDirective(name)) {
			// 如果是指令,獲取指令名稱
			const dir = name.substr(2);
			// 調用指令對應的處理函數
			this[dir] && this[dir](el, this.$vue, expr);
		} else if (this.isEventDirective(name)) {
			// 如果是事件,獲取事件名
			const dir = name.substr(1);
			// 調用事件處理函數
			this.eventHandler(el, this.$vue, expr, dir);
		}
	});
}

// 編譯文本節點
compileText(node, textContent) {
	console.log('編譯文本節點...');
	this.text(node, this.$vue, textContent);
}

第七步:定義isDirective、isEventDirective、eventHandler方法;

// 判斷是否是指令,如果是則返回true,否則返回false
isDirective(attr) {
	return attr.indexOf('k-') == 0;
}

// 判斷是否是事件,如果是則返回true,否則返回false
isEventDirective(attr) {
	return attr.indexOf('@') == 0;
}

// 執行事件
eventHandler(node, vue, exp, dir) { // dir = click exp = onClick
	let fn = vue.$methods && vue.$methods[exp];
	if (dir && fn) {
		// 第一個參數是監聽的事件名稱
		// 第二個參數fn.bind(vue)就是把vue實例綁定到fn函數中,那麼fn函數就可以通過this訪問vue實例啦。
		// 第三個參數表示是否啓用捕獲階段。
		node.addEventListener(dir, fn.bind(vue), false); 
	}
}

第八步:定義text、html、model方法,它們負責執行視圖的更新操作;

text(node, vue, exp) {
	this.update(node, vue, exp, 'text');
}

html(node, vue, exp) {
	this.update(node, vue, exp, 'html');
}

model(node, vue, exp) {
	this.update(node, vue, exp, 'model');
	// 監聽模node元素的input事件
	node.addEventListener('input', e => {
		vue[exp] = e.target.value; // 把input元素的輸入內容設置到模型的value屬性中
	});
}

// 更新dom
update(node, vue, exp, type) {
	let updaterFn = this[type + 'Updateor'];
	updaterFn && updaterFn(node, vue[exp]); // 執行更新
	new Watcher(vue, exp, function(value) {
		updaterFn && updaterFn(node, value); 
	});
}

第九步:定義更新處理函數;

textUpdateor(node, value) {
	node.textContent = value;
}

htmlUpdateor(node, value) {
	node.innerHTML = value;
}

modelUpdateor(node, value) {
	node.value = value;
}

2.6 改造KVue實例

第一步:在constructor方法中構建Compile實例;

constructor(options) {
	...
	new Compile(options.el, this);
}

第二步:修改observe方法,給data屬性設置代理。

observe(obj) {
	// 如果val不存在,或者val不是對象,則不需要執行響應式,直接返回
	if (!obj || typeof obj !== 'object') {
		return;
	}
	
	// 遍歷val對象所有屬性
	Object.keys(obj).forEach(key => {
		// 爲每一個key定義響應式
		this.defineReactive(obj, key, obj[key]);
		// 爲vue的data屬性做代理,相當於把所有key添加到vue實例中
		this.proxyData(key); 
	});
}

proxyData(key) {
	Object.defineProperty(this, key, {
		get() {
			return this.$data[key];
		},
		set(newVal) {
			this.$data[key] = newVal;
		}
	});
}

2.7 改造Watcher

第一步:修改constructor方法,在構建Watcher實例時將當前watcher實例添加到Dep中;

constructor(vue, key, fn) {
	...
		
	// 執行該行代碼的主要作用是觸發一下get方法,在get方法把當前watcher添加到dep依賴管理器中
	this.vue[this.key];
	// 把Dep.target屬性清空是爲了避免不必要的重複添加
	Dep.target = null;
}

第二步:修改update方法,調用fn函數執行視圖的更新操作;

update() {
	// 執行回調函數,更新視圖
	this.fn.call(this.vue, this.vue[this.key]);
}

2.8 測試

構建測試頁:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<script type="text/javascript" src="js/kvue.js"></script>
		<script type="text/javascript" src="js/compile.js"></script>
	</head>
	<body>
		<div id="app">
			{{test}}
			<p k-text="test"></p>
			<p k-html="html"></p>
			<p>
				<input k-model="test" />
			</p>
			<p>
				<button @click="onClick">按鈕</button>
			</p>
		</div>
		<script>
			const o = new KVue({
				el: '#app',
				data: {
					test: 'hehe',
					foo: {
						bar: 'bar123',
					},
					html: '<h1>奇蹟來了。。。</h1>'
				},
				methods: {
					onClick() {
						alert('balabala');
					}
				}
			});
		</script>
	</body>
</html>

運行效果:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章