共计 4171 个字符,预计需要花费 11 分钟才能阅读完成。
一. 什么是 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)
进行到这一步,页面上初始化应该渲染完成了。如下图: