vue實現(xiàn)原理
1、了解Object的屬性defineProperty
const Book = {}
let name = ''
Object.defineProperty(Book, 'name', {
set: (value) => {
name = value
},
get: () => {
return `《${name}》`
}
})
Book.name = 'vue實現(xiàn)原理'
console.log(Book.name) // 《vue實現(xiàn)原理》
2、vue中mvvm的實現(xiàn): 數(shù)據(jù)變化更新視圖,視圖變化更新數(shù)據(jù)
- 在非MVVM中的視圖更新,是通過事件觸發(fā)決定dom層該如何渲染,是一種事件行為操作。
- 在MVVM中,通過劫持數(shù)據(jù),監(jiān)聽數(shù)據(jù)變化,來決定一系列的dom操作,是數(shù)據(jù)驅(qū)動操作。
- 在vue2.0中是通過Object.defineProperty監(jiān)聽對象屬性值是否發(fā)生變化,來驅(qū)動dom渲染操作。
3、 實現(xiàn)過程
- 實現(xiàn)一個監(jiān)聽器Observer,用來劫持并監(jiān)聽data所有屬性,如果有變動的,就通知訂閱者。
- 實現(xiàn)一個訂閱器Dep,用于收集訂閱者,當屬性變化,執(zhí)行對應的訂閱者函數(shù)。
- 實現(xiàn)一個訂閱者Watcher,可以收到屬性的變化通知并執(zhí)行相應的函數(shù),從而更新視圖。
- 實現(xiàn)一個解析器Compile,可以掃描和解析每個節(jié)點的相關指令,并根據(jù)初始化模板數(shù)據(jù)以及初始化相應的訂閱器.
3.1 監(jiān)聽器Observer(遞歸處理)
/**
* @author Pero
* @date 2019/2/13
* @Description: Observer實例
*/
class Observer {
constructor (data) {
this.data = data
this.init(data)
}
init (data) {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
/**
* 監(jiān)聽data中子屬性的值
* @param data
*/
observerChildProperty (data) {
if (!data || typeof data !== 'object') {
return;
} else {
this.init(data)
}
}
/**
* @param data: 對象
* @param key: 當前對象的鍵
* @param val: 當前對象的值
* 監(jiān)聽vue中data的屬性,變化時執(zhí)行對應的訂閱者處理器
*/
defineReactive (data, key, val) {
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
/**
* @author Pero
* @date 2019/2/14
* @Description: 取值時添加訂閱器
*/
get: () => {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
/**
* 當data數(shù)據(jù)發(fā)生改變,執(zhí)行訂閱器存儲的函數(shù)(watcher.update方法)
* @param newVal
*/
set: (newVal) => {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
/*監(jiān)聽對象的子屬性*/
this.observerChildProperty(val);
}
}
3.2 訂閱器 Dep
/**
* @author Pero
* @date 2019/2/13
* @Description: Dep訂閱者收集器subs
*/
class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub);
}
notify () {
this.subs.forEach((sub) => {
sub.update();
});
}
}
Dep.target = null;
3.3 訂閱者 Watcher
class Watcher {
/**
* @param vm vue實例
* @param exp 監(jiān)聽的值
* @param cb 當值改變時的回調(diào)函數(shù)
*/
constructor (vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 初始添加到訂閱器
this.value = this.get();
}
update () {
const value = this.vm.data[this.exp];
const oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
get () {
Dep.target = this;
// 強制執(zhí)行監(jiān)聽器里的get函數(shù)
const value = this.vm.data[this.exp]
Dep.target = null;
return value;
}
}
3.4 解析器 Compile
/**
* @author Pero
* @date 2019/2/14
* @Description: Compile解析器
*/
class Compile {
constructor (el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
/**
* 初始化dom樹,解析dom結(jié)構(gòu)
*/
init () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
}
}
/**
* 將node添加到一個dom容器
* @param el
* @returns {DocumentFragment}
*/
nodeToFragment (el) {
const fragment = document.createDocumentFragment();
let child = el.firstChild;
while (child) {
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}
/**
* 解析dom樹結(jié)構(gòu)
* @param el
*/
compileElement(el) {
const childNodes = el.childNodes;
[].slice.call(childNodes).forEach((node) => {
const reg = /\{\{(.*)\}\}/;
const text = node.textContent;
if (this.isElementNode(node)) {
this.compile(node);
} else if (this.isTextNode(node) && reg.test(text)) {
this.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
}
/**
* 分發(fā)指令
* @param node
*/
compile (node) {
const nodeAttrs = node.attributes;
Array.prototype.forEach.call(nodeAttrs, (attr) => {
const attrName = attr.name;
if (this.isDirective(attrName)) {
const exp = attr.value;
const dir = attrName.substring(2);
if (this.isEventDirective(dir)) { // 事件指令
this.compileEvent(node, this.vm, exp, dir);
} else { // v-model 指令
this.compileModel(node, this.vm, exp, dir);
}
node.removeAttribute(attrName);
}
});
}
/**
* 解析textNode
* @param node
* @param exp
*/
compileText (node, exp) {
const initText = this.vm[exp];
this.updateText(node, initText);
new Watcher(this.vm, exp, (value) => {
this.updateText(node, value);
});
}
/**
* 解析事件
* @param node
* @param vm
* @param exp
* @param dir
*/
compileEvent (node, vm, exp, dir) {
const eventType = dir.split(':')[1];
const cb = vm.methods && vm.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false);
}
}
/**
* 解析v-model
* @param node
* @param vm
* @param exp
* @param dir
*/
compileModel (node, vm, exp, dir) {
let val = this.vm[exp];
this.modelUpdater(node, val);
new Watcher(this.vm, exp, (value) => {
this.modelUpdater(node, value);
});
node.addEventListener('input', (e) => {
const newValue = e.target.value;
if (val === newValue) {
return;
}
this.vm[exp] = newValue;
val = newValue;
});
}
/**
* 更新textNode的值
* @param node
* @param value
*/
updateText (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
/**
* 更新v-model的值
* @param node
* @param value
* @param oldValue
*/
modelUpdater (node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
isDirective (attr) {
return attr.indexOf('v-') == 0;
}
isEventDirective (dir) {
return dir.indexOf('on:') === 0;
}
isElementNode (node) {
return node.nodeType == 1;
}
isTextNode (node) {
return node.nodeType == 3;
}
}
3.5 實例Vue
/**
* @author Pero
* @date 2019/2/13
* @Description: Vue實例
*/
class Vue {
/**
* @param options
*/
constructor (options) {
this.data = options.data;
this.methods = options.methods;
Object.keys(this.data).forEach((key) => {
this.proxyKeys(key);
});
new Observer(this.data);
new Compile(options.el, this);
options.mounted.call(this);
}
/**
* this.data[xx]可以直接通過this[xx]修改屬性值
* @param key
*/
proxyKeys (key) {
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: () => {
return this.data[key];
},
set: (newVal) => {
this.data[key] = newVal;
}
});
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vue</title>
</head>
<style>
#app {
text-align: center;
}
</style>
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<div>
<button v-on:click="clickMe">click me!</button>
</div>
</div>
</body>
<script src="js/dep.js"></script>
<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 type="text/javascript">
new Vue({
el: '#app',
data: {
title: 'hello world',
name: 'abc'
},
methods: {
clickMe: function () {
this.title = 'hello world2';
}
},
mounted: function () {
window.setTimeout(() => {
this.title = '你好';
}, 1000);
}
});
</script>
</html>