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 = 0let lastTem = '' // 记录剩下的模板字符串let stack = [] // 栈// 匹配收集开始标签,并收集除标签名以外的attrlet 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函数 解析attrexport 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))

