vue将模板字符串渲染为DOM的过程
一、模板编译
- parse方法,将 template 中的代码解析成AST抽象语法树;(本文主要实现parse方法)
- optimize 方法,优化AST抽象语法树,防止重复渲染;
generate函数,将AST抽象语法树生成render函数字符串(h函数);
二、渲染DOM
h函数,生成虚拟节点
- patch方法,diff算法,并渲染为真实DOM
实现简版parse方法,将模板字符串转换为AST
简版不考虑以下情况:
- 自结束标签(有兴趣的伙伴可以自行尝试编写)
文本节点位于结束标签与开始标签之间,比如:
文本节点最终效果:
实现思路:
遍历模板字符串,利用栈的思想
- 如果是开始标签,处理属性信息,向栈中推入该标签的信息,如:{ tag:’div’, attrs:[], type:1, children: [] }
- 如果是文本节点,向栈顶元素的children属性中推入文本节点
如果是结束标签,弹栈,并将弹栈的结果,推入到当前栈顶元素的children属性中,如果弹栈后,栈空了,说明已经遍历完一个根元素了
实现代码:
parse.js 解析模板字符串
import parseAttr from "./parseAttr"
// parse函数
export default function (template) {
let i = 0
let lastTem = '' // 记录剩下的模板字符串
let stack = [] // 栈
// 匹配收集开始标签,并收集除标签名以外的attr
let startReg = /^<([a-z]+[1-6]?)(\s.*?)?>/
// 匹配收集结束标签
let endReg = /^<\/([a-z]+[1-6]?)>/
// 匹配收集文字
let wordReg = /^>(.*?)<\//
// 记录结果,模板字符串可以不止一个根标签,都搜集到children中
let res = { tag: 'template', children: [] }
while (i < template.length) {
lastTem = template.substring(i) // 获取剩下的模板字符串
if (startReg.test(lastTem)) { // 如果是开始标签
let match = lastTem.match(startReg)
let startTag = match[1] // 开始标签
let attrs = match[2] // 属性字符串
let attrsArr = [] // 收集属性的数组
if (attrs) {
attrsArr = parseAttr(attrs) // 将属性字符串变为数组
}
// 将标签信息推入栈数组
stack.push({ tag: startTag, attrs: attrsArr, type: 1, children: [] })
i += match[0].length - 1 // length-1,是为了能收集到标签后的文字
} else if (endReg.test(lastTem)) { // 如果是结束标签
let endTag = lastTem.match(endReg)[1] // 结束标签
if (endTag == stack[stack.length - 1].tag) {
let top = stack.pop() // 栈顶元素
if (stack.length == 0) {
// 栈中没有元素时,表示遍历完了一个根元素
res.children.push(top)
} else {
// 将栈顶元素,推入上一个元素的children属性中
stack[stack.length - 1].children.push(top)
}
} else {
// 收集的结束标签与栈顶元素的tag不相等
throw new Error(`${stack[stack.length - 1].tag}没有结束标签`)
}
i += endTag.length + 3
} else if (wordReg.test(lastTem)) { // 文本节点
let word = lastTem.match(wordReg)[1]
// 将文本节点推入栈顶元素的children中
stack[stack.length - 1].children.push({ text: word, type: 3 })
i += word.length
} else { // 如果是其他情况(比如是标签间空格),就不做任何处理
i++
}
}
return res
}
parseAttr.js 解析属性字符串
思路:
遍历属性字符串,使用一个变量记录是否在引号内
- 如果遇到在空格,且不在引号内的空格,从当前位置切断并存储到数组中
- 利用”=”将数组中每个字符串转换为{name:xxx,value:xxx}的对象格式
// parseAttr函数 解析attr
export default function (attrStr) {
let attrs = attrStr.trim() // 去除前后空格
let res = [] // 记录结果
let spaceIn = false // 记录是否在引号内,默认不在引号内
let pos = 0 // 记录断点
for (let i = 0; i < attrs.length; i++) {
let char = attrs[i]
if (char == '"') {
spaceIn = !spaceIn // 遇到引号,spaceIn取反
} else if (char == ' ' && !spaceIn) { // 遇到空格,且不在引号内
// 从引号处切断,并存储到res数组中
res.push(attrs.substring(pos, i).trim())
pos = i // 将断点设置为当前索引
}
}
// 将最后一项推到结果数组中
res.push(attrs.substring(pos).trim())
// 使用map方法,将数组中的每一项变为{name:xxx,value:xxx}的格式
return res.map(item => {
let o = item.match(/(.*?)="(.*?)"/)
return {
name: o[1],
value: o[2]
}
})
}
index.js 测试代码
结果:import parse from "./parse";
// 两个同级的ul标签
let template = `
<ul class="hide box" id="box">
<li>A</li>
<li>B</li>
</ul>
<ul class="show box" id="box2">
<li>C</li>
<li>D</li>
</ul>
`
console.log(parse(template))