经过第一步解析标签名,解析的结果如下:
match = { tagName: "div",start: 0 }
此时 html 也经 advance 成了 id="index" class="content">
。
接着经过第二步解析属性键值对,解析的结果变成:
match = { tagName: "div",attrs: [ { "name": "id","value": "index" },{ "name": "class","value": "content" } ],start: 0 }
此时 html 经过多次 advance 成了 >
。
然后经过第三步解析开始标签闭合部分,并且生成了一个 AST 节点,最终的变量状态如下:
match = { tagName: "div",start: 0,end: 32,unarySlash: "",}root = element
stack = [element]
currentParent = element
此时 html 经过 advance 已经变成了空字符串,解析完毕。
什么是栈? 类似于数组,是一种常用的线性表数据结构,可以使用数组轻松地实现。后进先出的操作方式特别适合 html 标签的这种嵌套语法结构。
解析结束标签的关键点是找到与之对应的开始标签。
先看方法定义:
function parseEndTag () { const end = html.match(endTag); if (end) { advance(end[0].length)let tagName = end[1],lowerCasedTagName = tagName.toLowerCase() let pos // 从栈顶往栈底找,直到找到栈中离的最近的同类型标签 for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } // 如果找到了就取出对应的开始标签 if (pos >= 0) { stack.length = pos currentParent = stack[stack.length - 1] } }
}
可以看到,在解析结束标签时,会去找栈中离的最近的同类型标签。在找到后会取出找到的节点并更新 currentParent
指向,也就是说假设 stack 现在为 ['div','p','a']
,经过 parseEndTag
之后可能就会变成 ['div','p']
,currentParent
也从指向 a
变成了指向栈顶的 p
。
文本为什么需要解析?别忘了,Vue 是支持在文本中插值的,即
的 {{msg}}
。文本解析就是解析这些混在文本中的表达式。
建议先了解一下,本段代码在遍历时使用了它,注意它与字符串的 match 方法不同。
先看方法定义:
const defaultTagRE = /{{((?:.|n)+?)}}/gfunction parseText(text){
if (defaultTagRE.test(text)) {
// tokens 用于分割普通文本和插值文本
const tokens = []
let lastIndex = defaultTagRE.lastIndex = 0
let match,index
while ((match = defaultTagRE.exec(text))) {
index = match.index// push 普通文本 if (index > lastIndex) { tokens.push(JSON.stringify(text.slice(lastIndex,index))) } // push 插值表达式 tokens.push(`_s(${match[1].trim()})`) // 游标前移 lastIndex = index + match[0].length } // 将剩余的普通文本压入 tokens 中 if (lastIndex < text.length) { tokens.push(JSON.stringify(text.slice(lastIndex))) } // 生成 ASTExpression 节点 currentParent.children.push({ type: 2,expression: tokens.join('+'),text }) }else{ // 生成 ASTText 节点 currentParent.children.push({ type: 3,text }); }
}
可以看到,并没有什么特别的地方,只是遍历传入的字符串并将所有插值摘出来。例如 hello,{{msg}}
会被分割成 ['"hello"','_s(msg)']
,注意普通文本是被 JSON.stringify
了的,这样在后面 tokens.join('+')
时才会变成 "hello"+_s(msg)
这种所期望的格式,也就是最简单的字符串和变量拼接。
文本通常就是叶子节点了,因此文本和表达式的节点定义(ASTText和ASTExpression)中并没有 parent
和 children
字段。
终于到最后了,这是咱这几年写过的最长文章了o(╥﹏╥)o
html 文档的结构基本上就是
这类标签的各种嵌套,套来套去套出一个页面。上面解析各部分(开始标签、结束标签、文本)的方法都已经有了,接下来就是使用上面的方法将整块 html 模板一层一层剥开,从而构建出整棵 AST。
先看方法定义:
let htmlfunction parseHTML(_html){
html = _htmlwhile (html) { let textEnd = html.indexOf('<') if (textEnd === 0) { //-- 匹配开始标签 -- const startTagMatch = html.match(startTagOpen) if (startTagMatch) { parseStartTag() continue } //-- 匹配结束标签 -- const endTagMatch = html.match(endTag) if (endTagMatch) { parseEndTag() continue } } //-- 匹配文本 -- let text,rest if (textEnd >= 0) { rest = html.slice(textEnd) text = html.substring(0,textEnd) advance(textEnd) } if (textEnd < 0) { text = html html = '' } text && parseText(text) } return root
}
可以看到,parseHTML
是循环一截一截把整块 html 蚕食掉的。返回值 root
就是对生成的 AST 的引用,其实就是一个被精心组织的 JSON 对象,已经提到,使用 JSON 描述树形结构具有天然优势。
现在看看忙活了半天的成果:
let tpl = `hello,{{msg}} by DOM哥` console.info(parseHTML(tpl))
控制台输出截图如下:
Vue 解析 HTML 的主流程基本上就是这样,由于是基于 HTML,还是比较简单的。
戳这儿查看本文的完整代码
Vue 的实际实现做了大量的兼容性处理,有针对某些浏览器(IE:看我干什么)的,也有针对 HTML 标签的,比如 标签既可以有结束标签,也可以没有结束标签,因此需要特殊处理。另外还要考虑注释的解析,特殊 html 标签如 Doctype 的处理。总之需要考虑的地方很多,因此实际实现比上面要复杂的多,但处理的思路基本上是一样的。
Vue 代码分割的很严重,因此上面的实现代码不可能全部集成在一个文件里,而是分成了好几个小模块,比如生成 AST 节点的模块是抽出来的,处理文本的模块也是单独抽出来的。
如果想要锱铢必较地咀嚼每一行代码,这是非常困难的,而且寸步难行,甚至最后会半途而废。研究源码最主要的是去学习其中的思路,而不要纠结在一字一句。
还记得 Vue 编译器编译成 render 函数的 3 个步骤吗,生成 AST,优化 AST,生成 render 函数。本篇暂告一段落,将在下篇继续研究 Vue 是如何优化 AST 的以及如何根据 AST 生成 render 函数。

(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!