Vue 是國(guó)內(nèi)目前最火的前端框架,它功能強(qiáng)大而又上手簡(jiǎn)單,基本成為前端工程師們的標(biāo)配,但很多同學(xué)都只是停留在如何使用上,知其然不知所以然,對(duì)它內(nèi)部的實(shí)現(xiàn)原理一知半解,今天就帶領(lǐng)大家動(dòng)手寫(xiě)一個(gè)類Vue的迷你庫(kù),進(jìn)一步加深對(duì)Vue的理解,在前端進(jìn)階的道路上如虎添翼!
內(nèi)容摘要
- MVVM 簡(jiǎn)介和流程分析
- 核心入口-MyVue類的實(shí)現(xiàn)
- 觀察者 - Watcher 類的實(shí)現(xiàn)
- 發(fā)布訂閱 - Dep 類的實(shí)現(xiàn)
- 數(shù)據(jù)劫持 - Observer 類的實(shí)現(xiàn)
- 大功告成,運(yùn)行 MyVue
MVVM 簡(jiǎn)介和流程分析
作為前端最火的框架之一,Vue是MVVM設(shè)計(jì)模式實(shí)現(xiàn)的典型代表,什么是MVVM呢?MVVM是Model-View-ViewModel的簡(jiǎn)寫(xiě),M - 數(shù)據(jù)模型(Model),V - 視圖層(View),VM - 視圖模型(ViewModel),它本質(zhì)上就是MVC 的改進(jìn)版。
MVVM 就是將其中的View 的狀態(tài)和行為抽象化,讓我們將視圖 UI 和業(yè)務(wù)邏輯分開(kāi)。
mvvm 實(shí)現(xiàn)原理的可以用下圖簡(jiǎn)略表示·

mvvm 實(shí)現(xiàn)流程分析

本案例我們通過(guò)實(shí)現(xiàn) vue 中的插值表達(dá)式解析和指令 v-model 功能來(lái)探究vue的基本運(yùn)行原理。
1. 核心入口-MyVue類的實(shí)現(xiàn)
實(shí)現(xiàn)數(shù)據(jù)代理
先了解些必備知識(shí),Object.defineProperty(obj, prop, descriptor),該方法可以定義或修改對(duì)象的屬性描述符
MyVue 的創(chuàng)建
class MyVue {
constructor(option) {
this.$el = document.querySelector(option.el);
this.$data = option.data;
if (this.$el) {
// 1, 代理數(shù)據(jù)
this.proxyData();
// 2, 數(shù)據(jù)劫持
// new Observer(this.$data);
// 3, 編譯數(shù)據(jù)
// new Compile(this);
}
}
// 代理數(shù)據(jù),用于監(jiān)聽(tīng)對(duì) data 數(shù)據(jù)的訪問(wèn)和修改
proxyData() {
for (const key in this.$data) {
Object.defineProperty(this, key, {
enumerable: true, // 設(shè)為false后,該屬性無(wú)法被刪除。
configurable: false, // 設(shè)為true后,該屬性可以被 for...in或Object.keys 枚舉到。
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
}
2. 編譯模板 - Compile 類的實(shí)現(xiàn)
class Compile {
constructor(vm) {
// 要編譯的容器
this.el = vm.$el
// 掛載實(shí)例對(duì)象,方便其他實(shí)例方法訪問(wèn)
this.vm = vm
// 通過(guò)文檔片段來(lái)編譯模板
// 1,createDocumentFragment()方法,是用來(lái)創(chuàng)建一個(gè)虛擬的節(jié)點(diǎn)對(duì)象
// 2,DocumentFragment(以下簡(jiǎn)稱DF)節(jié)點(diǎn)不屬于文檔樹(shù),它有如下特點(diǎn):
// 2-1 當(dāng)把該節(jié)點(diǎn)插入文檔樹(shù)時(shí),插入的不是該節(jié)點(diǎn)自身,而是它所有的子孫節(jié)點(diǎn)
// 2-2 當(dāng)添加多個(gè)dom元素時(shí),如果先將這些元素添加到DF中,再統(tǒng)一將DF添加到頁(yè)面,會(huì)減少
// 頁(yè)面的重排和重繪,進(jìn)而提升頁(yè)面渲染性能。
// 2-3 使用 appendChild 方法將dom樹(shù)中的節(jié)點(diǎn)添加到 DF 中時(shí),會(huì)刪除原來(lái)的節(jié)點(diǎn)
// 1,獲取文檔片段
const fragment = this.nodeToFragment(this.el)
// 2,編譯模板
this.compile(fragment)
// 3,將編譯好的子元素重新追加到模板容器中
this.el.appendChild(fragment)
// console.log(fragment, fragment.nodeType, this.el.nodeType)
}
// dom元素轉(zhuǎn)為文檔片段
nodeToFragment(element) {
// 1,創(chuàng)建文檔片段
const f = document.createDocumentFragment()
// 2, 遷移子元素
while(element.firstChild) {
f.appendChild(element.firstChild)
}
// 3,返回文檔片段
return f
}
// 編譯方法
compile(fragment) {
// 1,獲取所有的子節(jié)點(diǎn)
const childNodes = fragment.childNodes;
// 2,遍歷子節(jié)點(diǎn)數(shù)組
childNodes.forEach(node => {
// 分別處理元素節(jié)點(diǎn)(nodeType: 1)和文檔節(jié)點(diǎn)(nodeType:3)
const ntype = node.nodeType
if (ntype === 1) {
// 如果是元素節(jié)點(diǎn),解析指令
this.compileElement(node)
} else if (ntype === 3) {
// 如果是文檔節(jié)點(diǎn),解析雙花括號(hào)
this.compileText(node)
}
// 如果存在子節(jié)點(diǎn)則遞歸調(diào)用 compile
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 編譯元素節(jié)點(diǎn)
compileElement(node){
// 獲取元素的所有屬性
const attrs = node.attributes;
Array.from(attrs).forEach(atr => {
const {name, value} = atr
if (name.startsWith('v-')) {
// 對(duì)指令做處理
// name == v-model
const [, b] = name.split('-')
if (b === 'model') {
node.value = this.vm[value]
}
}
})
}
// 編譯文檔節(jié)點(diǎn)
compileText(node) {
const con = node.textContent;
const reg = /\{\{(.+?)\}\}/g;
if (reg.test(con)) {
const value = con.replace(reg, (...args) => {
// console.log(args)
return this.vm[args[1]]
})
// 更新文檔節(jié)點(diǎn)內(nèi)容
node.textContent = value
}
}
}
3. 觀察者 - Watcher 類的實(shí)現(xiàn)
class Watcher {
// 當(dāng)觀察者對(duì)應(yīng)的數(shù)據(jù)發(fā)生變化時(shí),使其可以更新視圖
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 保存舊值
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = this.vm[this.key]
Dep.target = null
return oldVal
}
// 更新視圖
update() {
this.cb()
}
}
// 接下來(lái)處理:1,誰(shuí)來(lái)通知觀察者去更新視圖;2,在什么時(shí)機(jī)更新視圖
4. 發(fā)布訂閱 - Dep 類的實(shí)現(xiàn)
// 收集依賴
class Dep {
constructor() {
// 初始化觀察者列表
this.subs = []
}
// 收集觀察者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知觀察者去更新視圖
notify() {
this.subs.forEach(w => w.update())
}
}
5. 數(shù)據(jù)劫持 - Observer 類的實(shí)現(xiàn)
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 實(shí)例化依賴收集器,專門收集所有的觀察者對(duì)象
const dep = new Dep();
for (const key in data) {
let val = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
// 當(dāng)觀察者實(shí)例化的時(shí)候會(huì)訪問(wèn)對(duì)應(yīng)屬性,進(jìn)而觸發(fā)get函數(shù),然后添加訂閱者
// 之后,數(shù)據(jù)的變化就會(huì)觸發(fā) set 函數(shù),而 set 函數(shù)觸發(fā)就會(huì)執(zhí)行 dep.notify()
// 從而實(shí)現(xiàn),數(shù)據(jù)變化驅(qū)動(dòng)視圖更新。
Dep.target && dep.addSub(Dep.target);
return val
},
set(newVal) {
val = newVal
// 既然數(shù)據(jù)更新,視圖也應(yīng)該隨之更新
dep.notify()
}
})
}
}
}
6. 大功告成,運(yùn)行 MyVue
對(duì)之前代碼進(jìn)行調(diào)整
- 首先是在 MyVue 中
class MyVue {
constructor(option) {
this.$el = document.querySelector(option.el);
this.$data = option.data;
if (this.$el) {
// 1, 代理數(shù)據(jù)
this.proxyData();
// 2, 數(shù)據(jù)劫持
- // new Observer(this.$data);
+ new Observer(this.$data);
// 3, 編譯數(shù)據(jù)
- // new Compile(this);
+ new Compile(this);
}
}
}
- 然后是在 Compile 中
class Compile {
...
// 編譯元素節(jié)點(diǎn)
compileElement(node){
// 獲取元素的所有屬性
const attrs = node.attributes;
Array.from(attrs).forEach(atr => {
const {name, value} = atr
if (name.startsWith('v-')) {
// 對(duì)指令做處理
// name == v-model
const [, b] = name.split('-')
if (b === 'model') {
// 將v-model 的綁定的數(shù)據(jù)解析到輸入框中
node.value = this.vm[value]
+ // 輸入框內(nèi)容變化,則修改數(shù)據(jù)(這一步是從視圖到數(shù)據(jù)的變化,需要手動(dòng)添加)
+ node.addEventListener('input', (e) => {
+ this.vm.$data[value] = e.target.value;
+ });
+ // 通過(guò)添加一個(gè)觀察者實(shí)例對(duì)象,當(dāng)數(shù)據(jù)發(fā)生任何變化則自動(dòng)更新視圖
+ new Watcher(this.vm, value, () => {
+ node.value = this.vm.$data[value];
+ });
}
}
})
}
// 編譯文檔節(jié)點(diǎn)
compileText(node) {
const con = node.textContent;
const reg = /\{\{(.+?)\}\}/g;
if (reg.test(con)) {
const value = con.replace(reg, (...args) => {
// console.log(args)
+ // 通過(guò)添加一個(gè)觀察者實(shí)例對(duì)象,當(dāng)數(shù)據(jù)發(fā)生任何變化則自動(dòng)更新視圖
+ new Watcher(this.vm, args[1], () => {
+ node.textContent = con.replace(reg, (...args) => {
+ return this.vm[args[1]]
+ })
+ })
return this.vm[args[1]]
})
// 更新文檔節(jié)點(diǎn)內(nèi)容
node.textContent = value
}
}
}
引入前面創(chuàng)建的五個(gè)文件,就可以 new 一個(gè)自己的vue實(shí)例對(duì)象了,趕緊去試試吧!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyVue</title>
</head>
<body>
<div id="app">
<!-- v-model 指令功能的實(shí)現(xiàn) -->
<input type="text" v-model="msg">
<div>
<!-- 插值表達(dá)式 -->
<p>{{msg}} --- {{info}}</p>
</div>
</div>
<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/MyVue.js"></script>
<script>
var mv = new MyVue({
el: "#app",
data: {
msg: "Hello",
info: "World"
}
})
</script>
</body>
</html>