簡(jiǎn)單實(shí)現(xiàn)vue框架實(shí)例贪嫂,實(shí)現(xiàn)的目的主要看下幾個(gè)知識(shí)點(diǎn)如何進(jìn)行的:
- Vue工作機(jī)制
- Vue響應(yīng)式的原理
- 依賴收集與追蹤
- 編譯compile
以及一些相關(guān)操作耐版,代碼如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>mvue-test-html</title>
</head>
<body>
<div id="app">
<p>{{name}}</p>
<p m-text="name"></p>
<p>{{age}}</p>
<input type="text" m-model="name">
<button @click="changeName">改變</button>
<div m-html="html"></div>
</div>
<script src="./compile.js"></script>
<script src="./mvue.js"></script>
<script>
const app = new MVue({
el: '#app',
data: {
name: 'lily',
age: 18,
html: '<p>html-測(cè)試</p>'
},
created() {
setTimeout(() => {
this.age++
this.name = '劉翔'
}, 1000)
},
methods: {
changeName() {
console.log(8989898)
this.name = 'lucy'
}
}
})
</script>
</body>
</html>
如上祠够, 我們需要實(shí)現(xiàn)幾點(diǎn):
- 根組件初始化,el掛載
- data實(shí)現(xiàn)數(shù)據(jù)雙向綁定粪牲,視圖層響應(yīng)更新古瓤, 如 this.name = '劉翔' 賦值后視圖層自動(dòng)更新
- created生命周期簡(jiǎn)單實(shí)現(xiàn)
- 指令v-text、表達(dá)式{{name}}腺阳、@click落君、v-model雙向數(shù)據(jù)綁定的實(shí)現(xiàn)
- data、方法等掛載到this上亭引,可以直接調(diào)用
這里分兩塊去處理這些東西绎速,一部分是我們vue實(shí)例的處理,還一部分是編譯到html的處理焙蚓。我這里寫了兩個(gè)文件纹冤,先實(shí)現(xiàn)vue實(shí)例,然后又寫了個(gè)compile的js文件购公。
MVue
這個(gè)里面首先包含一個(gè)vue實(shí)例萌京,在constructor中我們做一些初始化的事情,然后執(zhí)行響應(yīng)式處理宏浩,將data中的值都做好攔截及監(jiān)聽(tīng)處理知残,最后調(diào)用compile渲染出指定的el
observe 這個(gè)方法主要做響應(yīng)式處理,遍歷data中的所有鍵名一一調(diào)用defineReactive進(jìn)行數(shù)據(jù)響應(yīng)式處理比庄,然后代理到this實(shí)例上求妹。
defineReactive 這個(gè)方法主要是給每個(gè)屬性的get乏盐、set定義攔截,做一些攔截處理制恍。同時(shí)生成dep和key一一對(duì)應(yīng)丑勤,對(duì)所有的依賴進(jìn)行管理。
proxyData 顧名思義就是把data中的值代理到實(shí)例上面吧趣,方便this.name這樣去調(diào)用。
這里面還有一個(gè)Dep和Watcher兩個(gè)類耙厚,他們主要做依賴收集及管理强挫,Dep里面會(huì)管理所有的watcher
// 定義KVue構(gòu)造函數(shù)
class MVue {
constructor(options) {
// 保存?zhèn)魅氲倪x項(xiàng)
this.$options = options;
// 傳入data
this.$data = options.data;
// 響應(yīng)式處理
this.observe(this.$data);
this.$methods = options.methods;
new Compile(options.el, this)
if (options.created) {
options.created.call(this)
}
}
// 響應(yīng)式處理
observe(data) {
if (!data || typeof data !== "object") {
return;
}
// 遍歷data
Object.keys(data).forEach(key => {
// 響應(yīng)式處理
this.defineReactive(data, key, data[key]);
// 代理data中的屬性
this.proxyData(key);
});
}
defineReactive(data, key, val) {
this.observe(val);
// 定義一個(gè)Dep
const dep = new Dep(); // 每個(gè)dep實(shí)例都與key值一一對(duì)應(yīng)
// 給obj的每個(gè)key定義攔截
Object.defineProperty(data, key, {
get() {
// 依賴收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(v) {
if (v !== val) {
val = v;
dep.notify();
}
}
});
}
// 講$data中的屬性代理到實(shí)例上
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(v) {
this.$data[key] = v;
}
});
}
}
// 創(chuàng)建dep:管理所有的watcher
class Dep {
constructor() {
// 存儲(chǔ)所有的依賴
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
// 通知更新
notify() {
this.deps.forEach(dep => dep.update());
}
}
// 創(chuàng)建watcher: 保存data中的數(shù)值和頁(yè)面中的掛鉤關(guān)系
class Watcher {
constructor(vm, key, cb) {
// 創(chuàng)建實(shí)例時(shí)立刻將該實(shí)例指向Dep.target便于依賴收集
this.vm = vm;
this.cb = cb;
this.key = key;
// 觸發(fā)依賴收集
Dep.target = this;
this.vm[this.key]; // 觸發(fā)依賴收集
Dep.target = null;
}
// 更新
update() {
console.log(this.key + "更新了");
this.cb.call(this.vm, this.vm[this.key])
}
}
compile 主要做一些指令等一系列操作的處理,包括實(shí)例中的el元素經(jīng)過(guò)處理后掛載到dom上等操作
這里主要使用了正則去匹配相應(yīng)的表達(dá)式薛躬、指令等俯渤,然后做出相關(guān)操作處理。具體看代碼操作即可型宝。
// 遍歷dom八匠,解析指令和插值表達(dá)式
class Compile {
// el 待編譯的模板, vm-MVue實(shí)例
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 把模版中的內(nèi)容移到片段操作
this.$fragment = this.node2Fragment(this.$el);
// 執(zhí)行編譯
this.compile(this.$fragment)
// 放回至el中
this.$el.appendChild(this.$fragment)
}
node2Fragment(el) {
// 創(chuàng)建片段
const fragment = document.createDocumentFragment();
let child;
while(child = el.firstChild) {
fragment.appendChild(child)
}
return fragment;
}
compile(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (node.nodeType == 1) {
// 元素
// console.log('編譯元素' + node.nodeName)
this.compileEle(node)
} else if (this.isInter(node)) {
// 只關(guān)心{{XXX}}
// console.log('編譯插值文本' + node.textContent)
this.compileText(node)
}
// 遞歸子節(jié)點(diǎn)
if (node.children && node.childNodes.length > 0) {
this.compile(node)
}
})
}
isInter(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileEle(node) {
const nodeAttr = node.attributes;
// 匹配 m-xxx
Array.from(nodeAttr).forEach(attr => {
// 規(guī)定 m-xxx="yyyy"
const attrName = attr.name;
const exp = attr.value;
if (attrName.indexOf('m-') == 0) {
// 指令
const dir = attrName.substring(2);
// 執(zhí)行
this[dir] && this[dir](node, this.$vm, exp)
} else if (attrName.indexOf('@') == 0) {
// 事件
const method = attrName.substring(1)
this.addEvent(node, this.$vm, exp, method)
}
})
}
// 文本替換
compileText(node) {
// console.log(RegExp.$1);
// console.log(this.$vm[RegExp.$1])
// 表達(dá)式
const exp = RegExp.$1
this.update(node, this.$vm, exp, 'text')
}
update(node, vm, exp, type) {
const updater = this[type + 'Updater']
updater && updater(node, vm[exp])
new Watcher(vm, exp, function(val) {
updater && updater(node, val)
})
}
textUpdater(node, val) {
node.textContent = val
}
htmlUpdater(node, val) {
node.innerHTML = val
}
modelUpdater(node, val) {
node.value = val
}
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
node.addEventListener('input', (e) => {
vm[exp] = e.target.value
})
}
addEvent(node, vm, exp, method) {
const fn = vm.$options.methods && vm.$options.methods[exp]
node.addEventListener(method, fn.bind(vm))
}
}
這只是一個(gè)非常簡(jiǎn)單的vue模仿趴酣,距離框架真正處理差了十萬(wàn)八千里梨树,不過(guò)里面的一些思路還是比較溫和的,僅供vue框架源碼初探岖寞,后面會(huì)具體分析vue的源碼抡四。