前言
在上一篇文章中,我们梳理了大致的vue编译过程。但是没有深入去看vue核心的几个方法。首先,在编译过程中,vue有三个重要的包。@vue/compiler-core、@vue/compiler-dom、@vue/compiler-sfc。我们今天主要讲讲@vue/compiler-core中的baseParse()
和parse()
方法,浅析vue是到底是如何根据.vue文件生成AST抽象语法树的。
baseParse是在何时调用的?
上文中讲到。在加载模块时,会执行transform
钩子函数,在transform中,会执行transformMain
,而在transformMain中,主要进行了四个操作。创建descriptor,生成js代码,render函数,css代码。而ast就是在创建descriptor中完成的。descriptor中有一个template属性,ast就在template中。
baseParse的位置
我们首先找到createDescriptor方法。代码如下:
1 2 3 4 5 6 7 8 9 10 11
| function createDescriptor(filename, source, { root, isProduction, sourceMap, compiler, template }, hmr = false) { const { descriptor, errors } = compiler.parse(source, { filename, sourceMap, templateParseOptions: template?.compilerOptions }); const normalizedPath = vite.normalizePath(path__default.relative(root, filename)); descriptor.id = getHash(normalizedPath + (isProduction ? source : "")); (hmr ? hmrCache : cache).set(filename, descriptor); return { descriptor, errors }; }
|
发现它调用了一个compiler.parse来进行解析。此时的compiler.parse是vue/compiler-sfc中的。我们再进入其中。下面是简化后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| function parse$1(source, options = {}) { const { compiler = CompilerDOM__namespace, } = options; const descriptor = { }; const ast = compiler.parse(source, { parseMode: "sfc", prefixIdentifiers: true, ...templateParseOptions, onError: (e) => { errors.push(e); } }); ast.children.forEach((node) => { if (node.type !== 1) { return; } if (ignoreEmpty && node.tag !== "template" && isEmpty(node) && !hasSrc(node)) { return; } switch (node.tag) { case "template": case "script": case "style": default: descriptor.customBlocks.push(createBlock(node, source, pad)); break; } }); const result = { descriptor, errors }; parseCache$1.set(sourceKey, result); return result; }
|
我们能发现,它又调了一次compiler.parse。来生成ast。而此时的compiler是@vue/compiler-dom包下的。
ps:没想到这么能套
OK,我们再次进入compiler.parse内部。代码如下:
1 2 3 4 5
| var compilerCore = require('@vue/compiler-core');
function parse(template, options = {}) { return compilerCore.baseParse(template, shared.extend({}, parserOptions, options)); }
|
终于到了,baseParse
这来了。赶紧进去仔细端详端详。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| function baseParse(input, options) { reset(); currentInput = input; currentOptions = shared.extend({}, defaultParserOptions); if (options) { let key; for (key in options) { if (options[key] != null) { currentOptions[key] = options[key]; } } } { if (currentOptions.decodeEntities) { console.warn( `[@vue/compiler-core] decodeEntities option is passed but will be ignored in non-browser builds.` ); } } tokenizer.mode = currentOptions.parseMode === "html" ? 1 : currentOptions.parseMode === "sfc" ? 2 : 0; tokenizer.inXML = currentOptions.ns === 1 || currentOptions.ns === 2; const delimiters = options && options.delimiters; if (delimiters) { tokenizer.delimiterOpen = toCharCodes(delimiters[0]); tokenizer.delimiterClose = toCharCodes(delimiters[1]); } const root = currentRoot = createRoot([], input); tokenizer.parse(currentInput); root.loc = getLoc(0, input.length); root.children = condenseWhitespace(root.children); currentRoot = null; return root; }
|
先来说说这个方法的参数。
接着往下看代码。看到tokenizer.parse(currentInput);
的时候。我们再次进入parse中。不多说,看代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
parse(input) { this.buffer = input; while (this.index < this.buffer.length) { const c = this.buffer.charCodeAt(this.index); if (c === 10) { this.newlines.push(this.index); } switch (this.state) { case 1: { this.stateText(c); break; } case 2: { this.stateInterpolationOpen(c); break; } case 3: { this.stateInterpolation(c); break; } case 4: { this.stateInterpolationClose(c); break; } case 31: { this.stateSpecialStartSequence(c); break; } case 32: { this.stateInRCDATA(c); break; } case 26: { this.stateCDATASequence(c); break; } case 19: { this.stateInAttrValueDoubleQuotes(c); break; } case 12: { this.stateInAttrName(c); break; } case 13: { this.stateInDirName(c); break; } case 14: { this.stateInDirArg(c); break; } case 15: { this.stateInDynamicDirArg(c); break; } case 16: { this.stateInDirModifier(c); break; } case 28: { this.stateInCommentLike(c); break; } case 27: { this.stateInSpecialComment(c); break; } case 11: { this.stateBeforeAttrName(c); break; } case 6: { this.stateInTagName(c); break; } case 34: { this.stateInSFCRootTagName(c); break; } case 9: { this.stateInClosingTagName(c); break; } case 5: { this.stateBeforeTagName(c); break; } case 17: { this.stateAfterAttrName(c); break; } case 20: { this.stateInAttrValueSingleQuotes(c); break; } case 18: { this.stateBeforeAttrValue(c); break; } case 8: { this.stateBeforeClosingTagName(c); break; } case 10: { this.stateAfterClosingTagName(c); break; } case 29: { this.stateBeforeSpecialS(c); break; } case 30: { this.stateBeforeSpecialT(c); break; } case 21: { this.stateInAttrValueNoQuotes(c); break; } case 7: { this.stateInSelfClosingTag(c); break; } case 23: { this.stateInDeclaration(c); break; } case 22: { this.stateBeforeDeclaration(c); break; } case 25: { this.stateBeforeComment(c); break; } case 24: { this.stateInProcessingInstruction(c); break; } case 33: { this.stateInEntity(); break; } } this.index++; } this.cleanup(); this.finish(); }
|
前面注释的意思是:遍历缓冲区,调用与当前状态对应的函数。更有可能被命中的状态位于更高的位置,以提高性能。(专业)
Tokenizer.parse的原理
这个parse方法是一个状态机(State Machine)实现的词法分析器,它是对逐个字符解析,实现精确的词法分析,不同状态下对相同字符有不同处理逻辑,并且记录换行符位置,用于错误提示
什么是状态机?
状态机是一个抽象的机器,它具有以下几个核心要素:
加上注释后的parse
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| parse(input) { this.buffer = input; while (this.index < this.buffer.length) { const c = this.buffer.charCodeAt(this.index); if (c === 10) { this.newlines.push(this.index); }
switch (this.state) { case 1: this.stateText(c); break; case 2: this.stateInterpolationOpen(c); break; } this.index++; } }
|
状态转换流程:
以解析 <div class="test">
为例:
初始状态(1)
- 遇到<,进入标签状态(5)
- 解析标签名div(6)
- 解析属性(11-21)
- 遇到>,回到文本状态(1)