一. 什麼是mvvm
MVVM是Model-View-ViewModel的簡寫。它本質上就是MVC 的改進版。MVVM 就是將其中的View 的狀態和行爲抽象化,讓我們將視圖 UI 和業務邏輯分開。
要實現一個mvvm的庫,我們首先要理解清楚其實現的整體思路。先看看下圖的流程:
1.實現compile,進行模板的編譯,包括編譯元素(指令)、編譯文本等,達到初始化視圖的目的,並且還需要綁定好更新函數;
2.實現Observe,監聽所有的數據,並對變化數據發佈通知;
3.實現watcher,作爲一箇中樞,接收到observe發來的通知,並執行compile中相應的更新方法。
4.結合上述方法,向外暴露mvvm方法。
二. 實現方法
首先編輯一個html文件,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MVVM原理及其實現</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message">
<div>{{message}}</div>
<ul><li></li></ul>
</div>
<script src="watcher.js"></script>
<script src="observe.js"></script>
<script src="compile.js"></script>
<script src="mvvm.js"></script>
<script>
let vm = new MVVM({
el: '#app',
data: {
message: 'hello world',
a: {
b: 'bbb'
}
}
})
</script>
</body>
</html>
1.實現一個mvvm類(入口)
新建一個mvvm.js,將參數通過options傳入mvvm中,並取出el和data綁定到mvvm的私有變量$el和$data中。
// mvvm.js
class MVVM {
constructor(options) {
this.$el = options.el
this.$data = options.data
}
}
2.實現compile(編譯模板)
新建一個compile.js文件,在mvvm.js中調用compile。compile.js接收mvvm中傳過來的el和vm實例。
// mvvm.js
class MVVM {
constructor(options) {
this.$el = options.el
this.$data = options.data
// 如果有要編譯的模板 =>編譯
if(this.$el) {
// 將文本+元素模板進行編譯
new Compile(this.$el, this)
}
}
}
(1)初始化傳值
// compile.js
export default class Compile {
constructor(el, vm) {
// 判斷是否是元素節點,是=》取該元素 否=》取文本
this.el = this.isElementNode(el) ? el:document.querySelector(el)
this.vm = vm
},
// 判斷是否是元素節點
isElementNode(node) {
return node.nodeType === 1
}
}
(2)先把真實DOM移入到內存中 fragment,因爲fragment在內存中,操作比較快
// compile.js
class Compile {
constructor(el, vm) {
// 判斷是否是元素節點,是=》取該元素 否=》取文本
this.el = this.isElementNode(el) ? el:document.querySelector(el)
this.vm = vm
// 如果這個元素能獲取到 我們纔開始編譯
if(this.el) {
// 1. 先把真實DOM移入到內存中 fragment
let fragment = this.node2fragment(this.el)
}
},
// 判斷是否是元素節點
isElementNode(node) {
return node.nodeType === 1
}
// 將el中的內容全部放到內存中
node2fragment(el) {
let fragment = document.createDocumentFragment()
let firstChild
// 遍歷取出firstChild,直到firstChild爲空
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment // 內存中的節點
}
}
(3)編譯 =》 在fragment中提取想要的元素節點 v-model 和文本節點
// compile.js
class Compile {
constructor(el, vm) {
// 判斷是否是元素節點,是=》取該元素 否=》取文本
this.el = this.isElementNode(el) ? el:document.querySelector(el)
this.vm = vm
// 如果這個元素能獲取到 我們纔開始編譯
if(this.el) {
// 1. 先把真實DOM移入到內存中 fragment
let fragment = this.node2fragment(this.el)
// 2. 編譯 =》 在fragment中提取想要的元素節點 v-model 和文本節點
this.compile(fragment)
// 3. 把編譯好的fragment在放回到頁面中
this.el.appendChild(fragment)
}
}
// 判斷是否是元素節點
isElementNode(node) {
return node.nodeType === 1
}
// 是不是指令
isDirective(name) {
return name.includes('v-')
}
// 將el中的內容全部放到內存中
node2fragment(el) {
let fragment = document.createDocumentFragment()
let firstChild
// 遍歷取出firstChild,直到firstChild爲空
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment // 內存中的節點
}
//編譯 =》 提取想要的元素節點 v-model 和文本節點
compile(fragment) {
// 需要遞歸
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
// 是元素節點 直接調用文本編譯方法 還需要深入遞歸檢查
if(this.isElementNode(node)) {
this.compileElement(node)
// 遞歸深入查找子節點
this.compile(node)
// 是文本節點 直接調用文本編譯方法
} else {
this.compileText(node)
}
})
}
// 編譯元素方法
compileElement(node) {
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
let attrName = attr.name
// 判斷屬性名是否包含 v-指令
if(this.isDirective(attrName)) {
// 取到v-指令屬性中的值(這個就是對應data中的key)
let expr = attr.value
// 獲取指令類型
let [,type] = attrName.split('-')
// node vm.$data expr
compileUtil[type](node, this.vm, expr)
}
})
}
// 這裏需要編譯文本
compileText(node) {
//取文本節點中的文本
let expr = node.textContent
let reg = /\{\{([^}]+)\}\}/g
if(reg.test(expr)) {
// node this.vm.$data text
compileUtil['text'](node, this.vm, expr)
}
}
}
// 解析不同指令或者文本編譯集合
const compileUtil = {
text(node, vm, expr) { // 文本
let updater = this.updater['textUpdate']
updater && updater(node, getTextValue(vm, expr))
},
model(node, vm, expr){ // 輸入框
let updater = this.updater['modelUpdate']
updater && updater(node, getValue(vm, expr))
},
// 更新函數
updater: {
// 文本賦值
textUpdate(node, value) {
node.textContent = value
},
// 輸入框value賦值
modelUpdate(node, value) {
node.value = value
}
}
}
// 輔助工具函數
// 綁定key上對應的值,從vm.$data中取到
const getValue = (vm, expr) => {
expr = expr.split('.') // [message, a, b, c]
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
}
// 獲取文本編譯後的對應的數據
const getTextValue = (vm, expr) => {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return getValue(vm, arguments[1])
})
}
(3) 將編譯後的fragment放回到dom中
let fragment = this.node2fragment(this.el)
this.compile(fragment)
// 3. 把編譯好的fragment在放回到頁面中
this.el.appendChild(fragment)
進行到這一步,頁面上初始化應該渲染完成了。如下圖: