至此我们已经将数据都存储好,而且将 data
中的属性都转换为响应式的了,接下来我们需要做的就是编译插值表达式和指令,将 html
中的 {{}}
、 v-text
和 v-model
都解析成 data
中的属性。
Compiler 类结构
我们定义一个 Compiler
类来完成编译插值表达式和指令,这个类里我们定义 el(vm.$el是一个Dom对象)
和 vm(vue实例)
两个属性,定义 compile
方法来进行编译。
compile(el) 方法遍历 el
这个 Dom
对象的所有节点,调用 isTextNode(node) 和 isElementNode(node) 方法判断这些节点类型,如果是文本节点则调用 compileText(node) 解析插值表达式, 如果是元素节点则调用 compileElement(node) 解析指令。在 compileElement
内部调用 isDirective(attrName),判断 attrName
这个属性是否是指令。
我们按照以上所说的功能先描绘出 Compiler
类的大致结构:
js
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
compile(el) {}
compileElement(node) {}
compileText(node) {}
isTextNode(node) {
return node.nodeType === 3 // 3 为文本节点
}
isElementNode(node) {
node.nodeType === 1 // 1 为元素节点
}
isDirective(attrName) {
return attrName.startsWith('v-') // 指令以 v- 开头
}
}
接下来需要完善 compile
、compileElement
、compileText
方法。
使用 Compiler
在完善方法之前,我们先引入 Compiler
在 Vue 类中创建实例
js
new Observer(this.$data)
+ new Compiler(this)
在 html 中引入 compiler 文件
compile 方法
在 compile
方法中首先要遍历 el
中的所有子节点,如果是文本节点按照插值表达式处理,如果是元素节点按照指令处理,如果子节点中还有子节点,则需要更深层次的遍历。
js
compile(el) {
// 遍历 el 这个 Dom 对象的所有节点
// el.children 只返回所有子元素节点,不包括文本节点和注释节点。而 el.childNodes 返回所有子节点
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
this.compileElement(node)
}
// 处理深层次的子节点,如果 node 还有子节点要继续遍历
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
compileText 方法
compileText
用于编译文本节点,处理插值表达式,可以利用 nodeValue
和 textContent
属性获取文本节点的内容,然后利用正则表达式匹配插值表达式 {{}}
取出里面的变量,再用对应的值替代插值表达式。
js
compileText(node) {
console.dir(node) // 以对象的形式打印出文本节点
let value = node.textContent
let reg = /\{\{(.+?)\}\}/
if (reg.test(value)) {
node.textContent = value.replace(reg, (match, p1) => this.vm[p1.trim()])
}
}
当完善这个方法后,我们可以在模拟实例中看到插值表达式的 msg
和 count
都显示成了 data
中的值。
compileElement 方法
这个方法我们倒着来看怎么写,首先这个方法是处理指令的,不同的指令有不同的操作,在这个模拟实例上我们暂时只写了 v-text
和 v-model
指令,所以我们先来写一写这两个指令的代码。
v-text
js
textUpdater (node, value, key) {
node.textContent = value
}
v-model
暂时对于 v-model
只考虑显示,不考虑输入的功能。
js
modelUpdater (node, value, key) {
node.value = value
}
update
我们注意到对于 textUpdater
和 modelUpdater
两个方法的命名,是根据指令名称去掉 v-
前缀再拼接 Updater
得到的,这样做的好处是,无论有再多的指令我们只需要加一个按照这个规律命名的方法即可,不需要修改其他代码。
那我们在统一的 update
方法中处理这个逻辑:
js
update (node, key, attrName) {
attrName = attrName.substr(2) // 去除前缀 v-
let updateFn = this[attrName + 'Updater']
updateFn && updateFn(node, this.vm[key])
}
compileElement
指令的处理逻辑已经在上面写好了,那接下就是我们如何在 Dom
中找出指令,并将指令的名称和值传递给 update
方法。在模拟实例中,v-text
的使用如下:
html
<div v-text="msg"></div>
这个元素节点会进入 compileElement
的处理逻辑,而我们需要遍历这个元素节点的属性节点(node.attributes
),并判断这个属性是否为 v-
指令,如果是指令则获取指令的名称(attr.name
)和指令的值(attr.value
)
js
compileElement (node) {
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if (this.isDirective(attrName)) {
let key = attr.value
this.update(node, key, attrName)
}
})
}
完整代码
js
class Compiler {
constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile (el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if (this.isDirective(attrName)) {
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
attrName = attrName.substr(2)
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
textUpdater (node, value, key) {
node.textContent = value
}
modelUpdater (node, value, key) {
node.value = value
}
// 编译文本节点,处理差值表达式
compileText (node) {
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
node.textContent = value.replace(reg, (match, p1) => this.vm[p1.trim()])
}
}
// 判断元素属性是否是指令
isDirective (attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}