簡單實現(xiàn)vue框架實例,實現(xiàn)的目的主要看下幾個知識點如何進行的:
- Vue工作機制
- 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-測試</p>'
},
created() {
setTimeout(() => {
this.age++
this.name = '劉翔'
}, 1000)
},
methods: {
changeName() {
console.log(8989898)
this.name = 'lucy'
}
}
})
</script>
</body>
</html>
如上, 我們需要實現(xiàn)幾點:
- 根組件初始化,el掛載
- data實現(xiàn)數(shù)據(jù)雙向綁定,視圖層響應(yīng)更新, 如 this.name = '劉翔' 賦值后視圖層自動更新
- created生命周期簡單實現(xiàn)
- 指令v-text、表達式{{name}}、@click、v-model雙向數(shù)據(jù)綁定的實現(xiàn)
- data、方法等掛載到this上,可以直接調(diào)用
這里分兩塊去處理這些東西,一部分是我們vue實例的處理,還一部分是編譯到html的處理。我這里寫了兩個文件,先實現(xiàn)vue實例,然后又寫了個compile的js文件。
MVue
這個里面首先包含一個vue實例,在constructor中我們做一些初始化的事情,然后執(zhí)行響應(yīng)式處理,將data中的值都做好攔截及監(jiān)聽處理,最后調(diào)用compile渲染出指定的el
observe 這個方法主要做響應(yīng)式處理,遍歷data中的所有鍵名一一調(diào)用defineReactive進行數(shù)據(jù)響應(yīng)式處理,然后代理到this實例上。
defineReactive 這個方法主要是給每個屬性的get、set定義攔截,做一些攔截處理。同時生成dep和key一一對應(yīng),對所有的依賴進行管理。
proxyData 顧名思義就是把data中的值代理到實例上面,方便this.name這樣去調(diào)用。
這里面還有一個Dep和Watcher兩個類,他們主要做依賴收集及管理,Dep里面會管理所有的watcher
// 定義KVue構(gòu)造函數(shù)
class MVue {
constructor(options) {
// 保存?zhèn)魅氲倪x項
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);
// 定義一個Dep
const dep = new Dep(); // 每個dep實例都與key值一一對應(yīng)
// 給obj的每個key定義攔截
Object.defineProperty(data, key, {
get() {
// 依賴收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(v) {
if (v !== val) {
val = v;
dep.notify();
}
}
});
}
// 講$data中的屬性代理到實例上
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(v) {
this.$data[key] = v;
}
});
}
}
// 創(chuàng)建dep:管理所有的watcher
class Dep {
constructor() {
// 存儲所有的依賴
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
// 通知更新
notify() {
this.deps.forEach(dep => dep.update());
}
}
// 創(chuàng)建watcher: 保存data中的數(shù)值和頁面中的掛鉤關(guān)系
class Watcher {
constructor(vm, key, cb) {
// 創(chuàng)建實例時立刻將該實例指向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 主要做一些指令等一系列操作的處理,包括實例中的el元素經(jīng)過處理后掛載到dom上等操作
這里主要使用了正則去匹配相應(yīng)的表達式、指令等,然后做出相關(guān)操作處理。具體看代碼操作即可。
// 遍歷dom,解析指令和插值表達式
class Compile {
// el 待編譯的模板, vm-MVue實例
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é)點
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])
// 表達式
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))
}
}

這只是一個非常簡單的vue模仿,距離框架真正處理差了十萬八千里,不過里面的一些思路還是比較溫和的,僅供vue框架源碼初探,后面會具體分析vue的源碼。