在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
上一篇文章Vue3 编译流程-源码解析中,我们从 1、生成 AST 抽象语法树首先我们来重温一下 export function baseCompile( template: string | RootNode, options: CompilerOptions = {} ): CodegenResult { /* 忽略之前逻辑 */ const ast = isString(template) ? baseParse(template, options) : template transform( ast, {/* 忽略参数 */} ) return generate( ast, extend({}, options, { prefixIdentifiers }) ) } 因为我已经将咱们不需要关注的逻辑注释处理,所以现在看函数体内的逻辑会非常清晰:
这里我们主要关注 ast 的生成。可以看到 ast 的生成有一个三目运算符的判断,如果传进来的 export function baseParse( content: string, options: ParserOptions = {} ): RootNode { const context = createParserContext(content, options) // 创建解析的上下文对象 const start = getCursor(context) // 生成记录解析过程的游标信息 return createRoot( // 生成并返回 root 根节点 parseChildren(context, TextModes.DATA, []), // 解析子节点,作为 root 根节点的 children 属性 getSelection(context, start) ) } 在 2、创建 AST 的根节点export function createRoot( children: TemplateChildNode[], loc = locStub ): RootNode { return { type: NodeTypes.ROOT, children, helpers: [], components: [], directives: [], hoists: [], imports: [], cached: 0, temps: 0, codegenNode: undefined, loc } } 看 3、解析子节点function parseChildren( context: ParserContext, mode: TextModes, ancestors: ElementNode[] ): TemplateChildNode[] { const parent = last(ancestors) // 获取当前节点的父节点 const ns = parent ? parent.ns : Namespaces.HTML const nodes: TemplateChildNode[] = [] // 存储解析后的节点 // 当标签未闭合时,解析对应节点 while (!isEnd(context, mode, ancestors)) {/* 忽略逻辑 */} // 处理空白字符,提高输出效率 let removedWhitespace = false if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {/* 忽略逻辑 */} // 移除空白字符,返回解析后的节点数组 return removedWhitespace ? nodes.filter(Boolean) : nodes } 从上文代码中,可以知道 在 while 中解析器会判断文本数据的类型,只有当 第一种情况就是判断是否需要解析 Vue 模板语法中的 “ 接下来会判断,如果第一个字符是 “<” 并且第二个字符是 '!'的话,会尝试解析注释标签 之后会判断当第二个字符是 “/” 的情况,“</” 已经满足了一个闭合标签的条件了,所以会尝试去匹配闭合标签。当第三个字符是 “>”,缺少了标签名字,会报错,并让解析器的进度前进三个字符,跳过 “</>”。 如果“</”开头,并且第三个字符是小写英文字符,解析器会解析结束标签。 如果源模板字符串的第一个字符是 “<”,第二个字符是小写英文字符开头,会调用 当这个判断字符串字符的分支条件结束,并且没有解析出任何 node 节点,那么会将 node 作为文本类型,调用 parseText 进行解析。 最后将生成的节点添加进 这就是 while 循环体内的逻辑,且是 while (!isEnd(context, mode, ancestors)) { const s = context.source let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined if (mode === TextModes.DATA || mode === TextModes.RCDATA) { if (!context.inVPre && startsWith(s, context.options.delimiters[0])) { /* 如果标签没有 v-pre 指令,源模板字符串以双大括号 `{{` 开头,按双大括号语法解析 */ node = parseInterpolation(context, mode) } else if (mode === TextModes.DATA && s[0] === '<') { // 如果源模板字符串的第以个字符位置是 `!` if (s[1] === '!') { // 如果以 '<!--' 开头,按注释解析 if (startsWith(s, '<!--')) { node = parseComment(context) } else if (startsWith(s, '<!DOCTYPE')) { // 如果以 '<!DOCTYPE' 开头,忽略 DOCTYPE,当做伪注释解析 node = parseBogusComment(context) } else if (startsWith(s, '<![CDATA[')) { // 如果以 '<![CDATA[' 开头,又在 HTML 环境中,解析 CDATA if (ns !== Namespaces.HTML) { node = parseCDATA(context, ancestors) } } // 如果源模板字符串的第二个字符位置是 '/' } else if (s[1] === '/') { // 如果源模板字符串的第三个字符位置是 '>',那么就是自闭合标签,前进三个字符的扫描位置 if (s[2] === '>') { emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2) advanceBy(context, 3) continue // 如果第三个字符位置是英文字符,解析结束标签 } else if (/[a-z]/i.test(s[2])) { parseTag(context, TagType.End, parent) continue } else { // 如果不是上述情况,则当做伪注释解析 node = parseBogusComment(context) } // 如果标签的第二个字符是小写英文字符,则当做元素标签解析 } else if (/[a-z]/i.test(s[1])) { node = parseElement(context, ancestors) // 如果第二个字符是 '?',当做伪注释解析 } else if (s[1] === '?') { node = parseBogusComment(context) } else { // 都不是这些情况,则报出第一个字符不是合法标签字符的错误。 emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1) } } } // 如果上述的情况解析完毕后,没有创建对应的节点,则当做文本来解析 if (!node) { node = parseText(context, mode) } // 如果节点是数组,则遍历添加进 nodes 数组中,否则直接添加 if (isArray(node)) { for (let i = 0; i < node.length; i++) { pushNode(nodes, node[i]) } } else { pushNode(nodes, node) } } 4、解析模板元素 Element在 我先把 function parseElement( context: ParserContext, ancestors: ElementNode[] ): ElementNode | undefined { // 解析起始标签 const parent = last(ancestors) const element = parseTag(context, TagType.Start, parent) // 如果是自闭合的标签或者是空标签,则直接返回。voidTag例如: `<img>`, `<br>`, `<hr>` if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { return element } // 递归的解析子节点 ancestors.push(element) const mode = context.options.getTextMode(element, parent) const children = parseChildren(context, mode, ancestors) ancestors.pop() element.children = children // 解析结束标签 if (startsWithEndTagOpen(context.source, element.tag)) { parseTag(context, TagType.End, parent) } else { emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start) if (context.source.length === 0 && element.tag.toLowerCase() === 'script') { const first = children[0] if (first && startsWith(first.loc.source, '<!--')) { emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT) } } } // 获取标签位置对象 element.loc = getSelection(context, element.loc.start) return element } 首先我们会获取当前节点的父节点,然后调用 parseTag 函数会按的执行大体是以下流程:
在获取到 然后我们会尝试解析 const parent = last(ancestors) 再回头看看 最后匹配结束标签,设置 element 的 loc 位置信息,返回解析完毕的 5、示例:模板元素解析请看下方我们要解析的模板,图片中是解析过程中,保存解析后节点的栈的存储情况, <div> <p>Hello World</p> </div> 图中的黄色矩形是一个栈,当开始解析时, 将这个文本类型的 p 标签对应的 node 节点生成,并在 div 标签在接收到 p 标签的 node 后,添加进自身的 children 属性中,出栈。此时祖先栈中就空空如也了。而 div 的标签完成闭合解析的逻辑后,返回 最终 到此这篇关于 |
请发表评论