一、构建Compile类
- 大致过程就是获取el元素的内容,对内容进行编译后重新插入到页面中
1、获取el元素的内容
class Compile {
constructor (el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 采用文档碎片编译el.innerHTML,减少对页面的频繁操作,防止重排重绘影响性能
const fragment = this.nodeFragment(this.el)
this.el.appendChild(fragment)
}
isElementNode (node) {
return node.nodeType === 1
}
nodeFragment (el) {
const f = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild) {
f.appendChild(firstChild)
}
return f
}
}
- while循环的判断条件中,每一次循环时判断语句都会执行,即
firstChild = el.firstChild
在每次while判断中都会执行一次 appendChild
是剪切操作,因此每循环一次el
的firstChild
均会被剪切后添加到文档碎片f
中,循环结束后el
的子节点为空
2、编译
2.1 compile函数
class Compile {
constructor (el, vm) {
...
this.compile(fragment)
...
}
compile (fragment) {
const child = fragment.childNodes;
[...childNodes].forEach(child => {
if(this.isElementNode(child)) {
// 处理元素节点
this.compileElement(child)
} else {
// 处理文本节点
this.compileText(child)
}
// child中还有子节点继续遍历处理
if(child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
}
2.2 compileElement函数
// 指令对应处理方法
const compileUtil = {
text (node, expr, vm) {
const value = this.getVal(expr, vm) // 取值
this.updater.textUpdater(node, value) // 设置节点的内容
},
html (node, expr, vm) {
const value = this.getVal(expr, vm) // 取值
this.updater.htmlUpdater(node, value) // 设置节点的内容
},
model (node, expr, vm) {
const value = this.getVal(expr, vm)
this.updater.modelUpdater(node, value)
},
getVal (expr, vm) {
return expr.split('.').reduce((data, curVal) => {
return data[curVal]
}, vm.$data)
},
updater: {
textUpdater (node, value) {
node.textContent = value
},
htmlUpdater (node, value) {
node.innerHTML = value
},
modelUpdater (node, value) {
node.value = value
}
}
}
class Compile {
...
compileElement (node) {
const attributes = node.attributes; // 获取元素属性
[...attributes].forEach(attr => {
const {name, value} = attr
if(this.isDirective(name)) { // 判断是否是指令
const [, directive] = name.split('-') // derective:text html model on:click bind:xxx
const [dirName, eventName] = directive.split(':') // dirName:text html model on bind
compileUtil[dirName](node, value, this.vm, eventName) // 根据具体指令具体处理
node.removeAttribute('v-' + directive) // 删除属性
}
});
}
}
2.3 compileText函数
class Compile {
...
compileText (node) {
const content = node.textContent
// 匹配{{xxx}},.+?的意思是匹配除\n外的任意字符1个到多个,?是非贪婪匹配,碰到第一个}就停止
if(/\{\{(.+?)\}\}/.test(content)) { // 内容有{{}}形式,把所有内容进行处理
compileUtil['text'](node, content, this.vm)
}
}
}
这里我们把compileUtil['text']
修改一下:
const compileUtil = {
text (node, expr, vm) {
let value
let reg = /\{\{(.+?)\}\}/g
if(reg.test(expr)) { // 处理文本
value = expr.replace(reg, (...args) => {
return this.getVal(args[1], vm)
})
} else {
value = this.getVal(expr, vm) // 处理v-text
}
this.updater.textUpdater(node, value)
},
...
}
2.4 事件绑定
v-on
: 添加compileUtil
的on
处理方法即可
const compileUtil = {
...
on (node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName, fn.bind(vm), false) // 方法中的this要指向vue实例
},
...
}
@
:修改compileElement
函数
class Compile {
...
compileElement (node) {
...
[...attributes].forEach(attr => {
...
if(this.isDirective(name)) { // 判断是否是v-指令
...
} else if (this.isEventName(name)) { // 处理@
let [, eventName] = name.split('@')
compileUtil['on'](node, value, this.vm, eventName) // 根据具体指令具体处理
node.removeAttribute('@' + eventName)
}
});
}
...
isEventName (attrName) {
return attrName.startsWith('@')
}
}
2.5 v-bind
和:
v-bind
:添加compileUtil
的bind
处理方法即可
const compileUtil = {
...
bind (node, expr, vm, attrName) {
const value = this.getVal(expr, vm)
this.updater.bindUpdater(node, attrName, value)
},
...
updater: {
...
bindUpdater (node, attrName, value) {
node.setAttribute(attrName, value)
}
}
}
:
:修改compileElement
函数
class Compile {
...
compileElement (node) {
...
[...attributes].forEach(attr => {
...
if(this.isDirective(name)) { // 判断是否是v-指令
...
} else if (this.isAttrName(name)) { // 处理:
let [, AttrName] = name.split(':')
compileUtil['bind'](node, value, this.vm, AttrName)
node.removeAttribute(':' + AttrName)
}
});
}
...
isAttrName (attrName) {
return attrName.startsWith(':')
}
}