目录
- 概述
- compile
- 测试代码
- 结果
- 延伸及重点讲解
- 1. 类数组对象
- 2. RegExp.$1
- 3. nodeType
概述
上篇文章我们已经介绍了Vue的响应式原理,并实现了对数据的监听,监听的目的是为了及时更新视图,所以这篇文章就来介绍下vue解析指令并初始化视图部分。
compile
在Vue的构造函数中对根元素进行编译
class MVue {
constructor (options) {
// 保存options
this.$options = options
this.$data = options.data
// 将data进行响应式处理
observe(this.$data)
// 代理
proxy(this)
// 编译
/**核心部分**/
new Compile(options.el, this)
}
}
class Compile {
constructor (el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
if (this.$el) {
this.compile(this.$el)
}
}
compile (node) {
// 找到该元素的子节点
const childNodes = node.childNodes
// childNodes是类数组对象
Array.from(childNodes).forEach(n => {
// 判断类型
if (this.isElment(n)) {
this.compileElement(n)
// 递归
if (n.childNodes.length > 0) {
this.compile(n)
}
} else if (this.isInter(n)) {
// 动态插值表达式 编译文本
this.compileText(n)
}
})
}
isElment (node) {
return node.nodeType === 1
}
isInter (n) {
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
}
isDir (attrName) {
return attrName.startsWith('m-')
}
isEvent (attrName) {
return attrName.startsWith('@')
}
// 编译元素:遍历它的所有属性,看是否m-开头指令,或者@事件
compileElement (node) {
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
// m-text="XXX"
// name = m-text, value=xxx
const attrName = attr.name
const exp = attr.value
// 是指令
if (this.isDir(attrName)) {
// 执行特定指令处理函数
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
} else if (this.isEvent(attrName)) {
//是事件
const event = attrName.substring(1)
// 通过bind改变this指向为vm实例,以便方法内部访问vm.data中的数据
node.addEventListener(event, this.$vm.$methods[exp].bind(this.$vm))
}
})
}
compileText (n) {
// 获取表达式
// n.textContent = this.$vm[RegExp.$1]
n.textContent = this.$vm[RegExp.$1.trim()]
}
text (node, exp) {
node.textContent = this.$vm[exp] || exp
}
html (node, exp) {
node.innerHTML = this.$vm[exp]
}
model (node, exp) {
node.value = this.$vm[exp]
}
}
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{ counter }}</p>
<p m-text="测试"></p>
<p m-text="counter"></p>
<p m-html="desc"></p>
<input m-model="name" />
</div>
</body>
<script src="./index.js"></script>
<script>
const app = new MVue({
el: '#app',
data: {
counter: 1,
desc: '<span style = "color: red">super wanan</span>',
name: 'wanan'
}
})
// setInterval(() => {
// app.counter++
// }, 1000)
</script>
</html>
结果

延伸及重点讲解
1. 类数组对象
node.childNodes和node.attributes都是一种类数组对象,NodeList用于保存一组有序的节点。可以通过方括号语法来访问NodeList的值,有item方法与length属性。它并不是Array的实例,没有数组对象的方法。
比如我们再compile方法中打印childNodes,会得到这样的结果:

因此需要使用Array.from()方法将类数组对象转换为真正数组。
2. RegExp.$1
RegExp 是javascript中的一个内置对象。为正则表达式。
RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2,RegExp.$3,…RegExp.$99总共可以有99个匹配
其实RegExp这个对象会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面, 所以如果我们在使用正则表达式时, 有用到分组, 那么就可以直接在调用完以后直接使用RegExp.$xx来使用捕获到的分组内容。
所以我们在compileText时可以这样使用:
compileText (n) {
// 获取表达式
// n.textContent = this.$vm[RegExp.$1]
n.textContent = this.$vm[RegExp.$1.trim()]
}
需要注意的是,上述代码中还使用了trim方法去除前后空格,这是为了将{{ counter }}前后的空格去掉,正确匹配到this.vm[‘counter’],否则会读取this.vm[’ counter ']导致读取不到。
3. nodeType
DOM(文档对象模型)可以将任何HTML和XML文档描绘成一个由多层node(节点)构成的结构。
DOM1级定义了一个Node接口,该接口将由DOM中的所有节点类型实现。这个Node接口在JavaScript中作为Node类型实现的。除了IE之外,在其他所有浏览器中都可以访问到这个类型。JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享相同的基本属性和方法。每个节点都有一个nodeType属性,用于表明节点的类型。
nodeType的取值如下:
类型 |
nodeType常数值 |
描述 |
Node.ELEMENT_NODE |
1 |
元素节点 |
Node.ATTRIBUTE_NODE |
2 |
属性节点 |
Node.TEXT_NODE |
3 |
文本节点 |
Node.CDATA_SECTION_NODE |
4 |
字符数据节点(文本不会被解析器解析) |
Node.ENTITY_REFERENCE_NODE |
5 |
实体引用节点 |
Node.ENTITY_NODE |
6 |
实体节点 |
Node.PROCESSING_INSTRUCTION_NODE |
7 |
处理指令节点 |
Node.COMMENT_NODE |
8 |
注释节点 |
Node.DOCUMENT_NODE |
9 |
文档节点(DOM树的根节点) |
Node.DOCUMENT_TYPE_NODE |
10 |
向为文档定义的实体提供接口 |
Node.DOCUMENT_FRAGMENT_NODE |
11 |
表示邻接节点和它们的子树 |
Node.NOTATION_NODE |
12 |
代表一个符号在DTD中的声明 |