compile的作用:
- 解析指令(属性节点)与插值表达式(文本节点),并替换模板数据,初始化视图;
- 将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;
什么是DocumentFragments?
DocumentFragment
,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的Document
使用,用于存储已排好版的或尚未打理好格式的 XML 片段。最大的区别是因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
实现virtualNode,将挂载点内的所有节点存储到DocumentFragment中
virtualNode(node) {
let fragment = document.createDocumentFragment()
// 把el中所有的子节点挨个添加到文档片段中
let childNodes = node.childNodes
// 由于childNodes是一个类数组,所以我们要把它转化成为一个数组,以使用forEach方法
Array.from(childNodes).forEach(node => {
// 把所有的字节点添加到fragment中
fragment.appendChild(node)
})
return fragment
}
接下来我们要做的事情就是解析fragment里面的节点:compile(fragment)
。
我们要递归遍历fragment里面的所有子节点,根据节点类型进行判断,如果是文本节点则按插值表达式进行解析,如果是属性节点则按指令进行解析。在解析属性节点的时候,我们还要进一步判断:是不是由v-开头的指令,或者是特殊字符,如@、:开头的指令。
class Compile {
constructor(el, vm) {
this.el = typeof el === "string" ? document.querySelector(el) : el
this.vm = vm
// 解析模板内容
if (this.el) {
const fragment = this.virtualNode(this.el)
this.compile(fragment)
this.el.appendChild(fragment)
}
}
// 解析fragment里面的节点
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
// 如果是元素节点,则解析指令
if (this.isElementNode(node)) {
this.compileElementNode(node)
}
// 如果是文本节点,则解析差值表达式
if (this.isTextNode(node)) {
this.compileTextNode(node)
}
// 递归解析
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
}
接下来我们要做的就只剩下解析指令,并通过watcher把解析后的结果通知给视图了。
这里有两个问题大家需要注意一下:
如果是复杂数据的情形,例如插值表达式:
{{dad.son.name}}
或者<p v-text='dad.son.name'></p>
,我们拿到v-text的属性值是字符串dad.son.name
,我们是无法通过vm.$data['dad.son.name']
拿到数据的。因此,如果数据是复杂数据的情形,我们需要实现getVMData()
和setVMData()
方法进行数据的获取与修改。
在vue中,methods里面的方法里面的this是指向vue实例,因此,在我们通过v-on指令给节点绑定方法的时候,我们需要把该方法的this指向绑定为vue实例。
let CompileUtils = {
getVMData(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
},
setVMData(vm, expr,value) {
let data = vm.$data
let arr = expr.split('.')
arr.forEach((key,index) => {
if(index < arr.length -1) {
data = data[key]
} else {
data[key] = value
}
})
},
// 解析插值表达式
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
let expr = RegExp.$1
node.textContent = txt.replace(reg, this.getVMData(vm, expr))
new Watcher(vm, expr, newValue => {
node.textContent = txt.replace(reg, newValue)
})
}
},
// 解析v-text
text(node, vm, expr) {
node.textContent = this.getVMData(vm, expr)
new Watcher(vm, expr, newValue => {
node.textContent = newValue
})
},
// 解析v-html
html(node, vm, expr) {
node.innerHTML = this.getVMData(vm, expr)
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue
})
},
// 解析v-model
model(node, vm, expr) {
let that = this
node.value = this.getVMData(vm, expr)
node.addEventListener('input', function () {
// 下面这个写法不能深度改变数据
// vm.$data[expr] = this.value
that.setVMData(vm,expr,this.value)
})
new Watcher(vm, expr, newValue => {
node.value = newValue
})
},
// 解析v-on
eventHandler(node, vm, eventType, expr) {
// 处理methods里面的函数fn不存在的逻辑
// 即使没有写fn,也不会影响项目继续运行
let fn = vm.$methods && vm.$methods[expr]
try {
node.addEventListener(eventType, fn.bind(vm))
} catch (error) {
console.error('抛出这个异常表示你methods里面没有写方法\n', error)
}
}
}
网友评论