系列文章目录
Vue3源码 第一篇-总览 Vue3源码 第二篇-Reactive API Vue3源码 第三篇-Vue3是如何实现响应性 Vue3源码 第四篇-Vue3 setup
前言
上一篇文章介绍了setup的源码,顺着源码解读,在setup初始化完毕后,vue会进一步进行模版编译,这篇文章我们将继续学习模版编译相关的代码。
一、源码阅读
这篇文章主要是对模版生成AST过程中的主要函数进行了源码阅读,在文章的最后通过一个简单模版的示例,介绍了vue3如何推进模版字符串并进行解析的过程,方便大家更好地理解模版生成AST的一个过程。话不多说,开冲~~
1、registerRuntimeCompiler
注册运行时的模版编译函数,并最终生成模版函数。
let compile;
function registerRuntimeCompiler(_compile) {
compile = _compile;
}
registerRuntimeCompiler(compileToFunction);
function compileToFunction(template, options) {
if (!isString(template)) {
if (template.nodeType) {
template = template.innerHTML;
}
else {
warn(`invalid template option: `, template);
return NOOP;
}
}
const key = template;
const cached = compileCache[key];
if (cached) {
return cached;
}
if (template[0] === '#') {
const el = document.querySelector(template);
if (!el) {
warn(`Template element not found or is empty: ${template}`);
}
template = el ? el.innerHTML : ``;
}
const { code } = compile$1(template, extend({
hoistStatic: true,
onError(err) {
{
const message = `Template compilation error: ${err.message}`;
const codeFrame = err.loc &&
generateCodeFrame(template, err.loc.start.offset, err.loc.end.offset);
warn(codeFrame ? `${message}\n${codeFrame}` : message);
}
}
}, options));
const render = (new Function(code)()
);
render._rc = true;
return (compileCache[key] = render);
}
1.1、compile$1
具体执行的编译模版的函数
function compile$1(template, options = {}) {
return baseCompile(template, extend({}, parserOptions, options, {
nodeTransforms: [
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend({}, DOMDirectiveTransforms, options.directiveTransforms || {}),
transformHoist: null
}));
}
1.2、 baseCompile(生成AST语法树)
function baseCompile(template, options = {}) {
const ast = isString(template) ? baseParse(template, options) : template;
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
transform(ast, extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {}
)
}));
return generate(ast, extend({}, options, {
prefixIdentifiers
}));
}
1.3 、baseParse(模版语法解析器)
function baseParse(content, options = {}) {
const context = createParserContext(content, options);
const start = getCursor(context);
return createRoot(parseChildren(context, 0 , []), getSelection(context, start));
}
1.4、createParserContext(创建语法解析器的上下文)
const defaultParserOptions = {
delimiters: [`{{`, `}}`],
getNamespace: () => 0 ,
getTextMode: () => 0 ,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
decodeEntities: (rawText) => rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError,
comments: false
};
function createParserContext(content, rawOptions) {
const options = extend({}, defaultParserOptions);
for (const key in rawOptions) {
options[key] = rawOptions[key] || defaultParserOptions[key];
}
return {
options,
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false
};
}
1.5 、getCursor(获取语法解析的位置信息)
function getCursor(context) {
const { column, line, offset } = context;
return { column, line, offset };
}
1.6 、parseChildren(解析子元素)
该方法在第一次root时会调用,在解析标签元素时在解析完本元素时会调用该方法解析自己的子元素
function parseChildren(context, mode, ancestors) {
const parent = last(ancestors);
const ns = parent ? parent.ns : 0 ;
const nodes = [];
while (!isEnd(context, mode, ancestors)) {
const s = context.source;
let node = undefined;
if (mode === 0 || mode === 1 ) {
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
node = parseInterpolation(context, mode);
}
else if (mode === 0 && s[0] === '<') {
if (s.length === 1) {
emitError(context, 5 , 1);
}
else if (s[1] === '!') {
}
else if (s[1] === '/') {
if (s.length === 2) {
emitError(context, 5 , 2);
}
else if (s[2] === '>') {
emitError(context, 14 , 2);
advanceBy(context, 3);
continue;
}
else if (/[a-z]/i.test(s[2])) {
emitError(context, 23 );
parseTag(context, 1 , parent);
continue;
}
else {
emitError(context, 12 , 2);
node = parseBogusComment(context);
}
}
else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors);
}
else if (s[1] === '?') {
emitError(context, 21 , 1);
node = parseBogusComment(context);
}
else {
emitError(context, 12 , 1);
}
}
}
if (!node) {
node = parseText(context, mode);
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i]);
}
}
else {
pushNode(nodes, node);
}
}
let removedWhitespace = false;
if (mode !== 2 && mode !== 1 ) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!context.inPre && node.type === 2 ) {
if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1];
const next = nodes[i + 1];
if (!prev ||
!next ||
prev.type === 3 ||
next.type === 3 ||
(prev.type === 1 &&
next.type === 1 &&
/[\r\n]/.test(node.content))) {
removedWhitespace = true;
nodes[i] = null;
}
else {
node.content = ' ';
}
}
else {
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ');
}
}
}
if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
const first = nodes[0];
if (first && first.type === 2 ) {
first.content = first.content.replace(/^\r?\n/, '');
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes;
}
1.7、 isEnd(查看内容是否结束)
每次循环开始时调用该方法检查模版字符串当前是否已经结束。
function isEnd(context, mode, ancestors) {
const s = context.source;
switch (mode) {
case 0 :
if (startsWith(s, '</')) {
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true;
}
}
}
break;
}
return !s;
}
1.8 、parseInterpolation(解析插值逻辑)
function parseInterpolation(context, mode) {
const [open, close] = context.options.delimiters;
const closeIndex = context.source.indexOf(close, open.length);
if (closeIndex === -1) {
emitError(context, 25 );
return undefined;
}
const start = getCursor(context);
advanceBy(context, open.length);
const innerStart = getCursor(context);
const innerEnd = getCursor(context);
const rawContentLength = closeIndex - open.length;
const rawContent = context.source.slice(0, rawContentLength);
const preTrimContent = parseTextData(context, rawContentLength, mode);
const content = preTrimContent.trim();
const startOffset = preTrimContent.indexOf(content);
if (startOffset > 0) {
advancePositionWithMutation(innerStart, rawContent, startOffset);
}
const endOffset = rawContentLength - (preTrimContent.length - content.length - startOffset);
advancePositionWithMutation(innerEnd, rawContent, endOffset);
advanceBy(context, close.length);
return {
type: 5 ,
content: {
type: 4 ,
isStatic: false,
constType: 0 ,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
};
}
1.9、 advanceBy(对模版进行推进)
整个解析过程中经常调用的方法, 负责对模版进行推进,不断改变当前解析模版的值,直到最后模版为空,解析完毕,通过截取方法改变字符串的值。
function advanceBy(context, numberOfCharacters) {
const { source } = context;
advancePositionWithMutation(context, source, numberOfCharacters);
context.source = source.slice(numberOfCharacters);
}
1.10、 advancePositionWithMutation(推进位置计算)
计算每次推进的位置信息。
function advancePositionWithMutation(pos, source, numberOfCharacters = source.length) {
let linesCount = 0;
let lastNewLinePos = -1;
for (let i = 0; i < numberOfCharacters; i++) {
if (source.charCodeAt(i) === 10 ) {
linesCount++;
lastNewLinePos = i;
}
}
pos.offset += numberOfCharacters;
pos.line += linesCount;
pos.column =
lastNewLinePos === -1
? pos.column + numberOfCharacters
: numberOfCharacters - lastNewLinePos;
return pos;
}
1.11 、getSelection (获取在源模版中的位置信息)
function getSelection(context, start, end) {
end = end || getCursor(context);
return {
start,
end,
source: context.originalSource.slice(start.offset, end.offset)
};
}
1.12 、parseElement(解析元素)
function parseElement(context, ancestors) {
const wasInPre = context.inPre;
const wasInVPre = context.inVPre;
const parent = last(ancestors);
const element = parseTag(context, 0 , parent);
const isPreBoundary = context.inPre && !wasInPre;
const isVPreBoundary = context.inVPre && !wasInVPre;
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, 1 , parent);
}
else {
emitError(context, 24 , 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, 8 );
}
}
}
element.loc = getSelection(context, element.loc.start);
if (isPreBoundary) {
context.inPre = false;
}
if (isVPreBoundary) {
context.inVPre = false;
}
return element;
}
1.13、parseTag (解析标签)
function parseTag(context, type, parent) {
const start = getCursor(context);
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source);
const tag = match[1];
const ns = context.options.getNamespace(tag, parent);
advanceBy(context, match[0].length);
advanceSpaces(context);
const cursor = getCursor(context);
const currentSource = context.source;
let props = parseAttributes(context, type);
if (context.options.isPreTag(tag)) {
context.inPre = true;
}
if (!context.inVPre &&
props.some(p => p.type === 7 && p.name === 'pre')) {
context.inVPre = true;
extend(context, cursor);
context.source = currentSource;
props = parseAttributes(context, type).filter(p => p.name !== 'v-pre');
}
let isSelfClosing = false;
if (context.source.length === 0) {
emitError(context, 9 );
}
else {
isSelfClosing = startsWith(context.source, '/>');
if (type === 1 && isSelfClosing) {
emitError(context, 4 );
}
advanceBy(context, isSelfClosing ? 2 : 1);
}
let tagType = 0 ;
const options = context.options;
if (!context.inVPre && !options.isCustomElement(tag)) {
const hasVIs = props.some(p => p.type === 7 && p.name === 'is');
if (options.isNativeTag && !hasVIs) {
if (!options.isNativeTag(tag))
tagType = 1 ;
}
else if (hasVIs ||
isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag) ||
tag === 'component') {
tagType = 1 ;
}
if (tag === 'slot') {
tagType = 2 ;
}
else if (tag === 'template' &&
props.some(p => {
return (p.type === 7 && isSpecialTemplateDirective(p.name));
})) {
tagType = 3 ;
}
}
return {
type: 1 ,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined
};
}
1.14、parseAttributes(解析获取标签属性列表)
function parseAttributes(context, type) {
const props = [];
const attributeNames = new Set();
while (context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/>')) {
if (startsWith(context.source, '/')) {
}
if (type === 1 ) {
}
const attr = parseAttribute(context, attributeNames);
if (type === 0 ) {
props.push(attr);
}
if (/^[^\t\r\n\f />]/.test(context.source)) {
emitError(context, 15 );
}
advanceSpaces(context);
}
return props;
}
1.15、parseAttribute(解析属性)
function parseAttribute(context, nameSet) {
const start = getCursor(context);
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source);
const name = match[0];
if (nameSet.has(name)) {
emitError(context, 2 );
}
nameSet.add(name);
advanceBy(context, name.length);
let value = undefined;
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
value = parseAttributeValue(context);
if (!value) {
emitError(context, 13 );
}
}
const loc = getSelection(context, start);
if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(name);
const dirName = match[1] ||
(startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot');
let arg;
if (match[2]) {
const isSlot = dirName === 'slot';
const startOffset = name.lastIndexOf(match[2]);
const loc = getSelection(context, getNewPosition(context, start, startOffset), getNewPosition(context, start, startOffset + match[2].length + ((isSlot && match[3]) || '').length));
let content = match[2];
let isStatic = true;
if (content.startsWith('[')) {
isStatic = false;
if (!content.endsWith(']')) {
emitError(context, 26 );
}
content = content.substr(1, content.length - 2);
}
else if (isSlot) {
content += match[3] || '';
}
arg = {
type: 4 ,
content,
isStatic,
constType: isStatic
? 3
: 0 ,
loc
};
}
if (value && value.isQuoted) {
const valueLoc = value.loc;
valueLoc.start.offset++;
valueLoc.start.column++;
valueLoc.end = advancePositionWithClone(valueLoc.start, value.content);
valueLoc.source = valueLoc.source.slice(1, -1);
}
return {
type: 7 ,
name: dirName,
exp: value && {
type: 4 ,
content: value.content,
isStatic: false,
constType: 0 ,
loc: value.loc
},
arg,
modifiers: match[3] ? match[3].substr(1).split('.') : [],
loc
};
}
return {
type: 6 ,
name,
value: value && {
type: 2 ,
content: value.content,
loc: value.loc
},
loc
};
}
1.16 、parseText(解析文本)
function parseText(context, mode) {
const endTokens = ['<', context.options.delimiters[0]];
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1);
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
const start = getCursor(context);
const content = parseTextData(context, endIndex, mode);
return {
type: 2 ,
content,
loc: getSelection(context, start)
};
}
1.17、 parseTextData(解析文本数据)
function parseTextData(context, length, mode) {
const rawText = context.source.slice(0, length);
advanceBy(context, length);
if (mode === 2 ||
mode === 3 ||
rawText.indexOf('&') === -1) {
return rawText;
}
else {
return context.options.decodeEntities(rawText, mode === 4 );
}
}
2 具体示例
通过阅读源码我们大体了解了整个解析模版生成AST的过程,我们通过一个完整的示例来更清晰的了解一下其中模版如何生成AST的过程 template
<div id="counter">
<section id="name" @click="sayHello">
morning!{{ name }}
</section>
</div>
当前模版转换后的字符串,里面有很多换行符空格等!
\n <section id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n
- parseChildren 解析子节点
- 调用isEnd方法判断当前解析节点是否已经结束了,上面介绍过这个方法.
- 通过判断字符串开头得知是文字解析,进入parseTextData逻辑解析文字,根据<和{{这两个标签找到结束位置为4
- 调用advancBy方法对字符串进行推进并且调用advancePositionWithMutation进行偏移量计算,最后将文字部分进行截取,达到字符串推进的目的
- 推进后的字符串:
<section id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n - pushNode将解析的结果放入到集合中
- 继续读取字符串,根据判断得到需要解析标签,进入parseElement逻辑解析标签
- 调用parseTag解析标签,解析出section标签后调用advancBy方法和advancePositionWithMutation进行推进和偏移量计算,这次推进8个字符(<section)
- 推进后的字符串:
id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n - 新字符串开头可能含有空格,所以下一步继续先推进,去除掉空格,推进后的字符串:
id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n - 继续解析标签内部的属性信息parseAttributes内部调用parseAttribute解析每一个属性
- 获取属性节点id,调用advanceBy推进两个字符,生成新的字符串
="name" @click="sayHello">\n morning!{{ name }}\n </section>\n - 在去除=号和空格,推进后的结果为
"name" @click="sayHello">\n morning!{{ name }}\n </section>\n - 在去除掉引号,推进后的结果为
name" @click="sayHello">\n morning!{{ name }}\n </section>\n - 解析其中的文字信息parseTextData,将字符串推进4个字符,推进后的结果为:
" @click="sayHello">\n morning!{{ name }}\n </section>\n - 去除引号和空格继续推进,推进后的结果为:
@click="sayHello">\n morning!{{ name }}\n </section>\n - 继续解析下一个属性为@click,继续推进6个字符,推进后的结果为:
="sayHello">\n morning!{{ name }}\n </section>\n - 去除引号解析内部的value值,
sayHello">\n morning!{{ name }}\n </section>\n - 继续调用parseTextData解析纯文本数据,解析后继续推进
">\n morning!{{ name }}\n </section>\n - 去除引号,继续推进
>\n morning!{{ name }}\n </section>\n - 标签内的属性遍历结束,检查标签是否是自闭和,自闭和推进2个字符,非自闭和推进1个字符,推进后的结果:
\n morning!{{ name }}\n </section>\n - 解析完标签后,在祖辈数组ancestors添加该标签,继续调用parseChildren解析标签下的子元素
- 获取到开头是字符串,进入parseText解析纯文本的逻辑,找到纯文本结束的标志,这次找到{{的结束位置,进行推进,推进结果为:
{{ name }}\n </section>\n - 推进2个字符,去掉{{,结果为:
name }}\n </section>\n - 调用parseTextData解析纯文本数据,继续推进
}}\n </section>\n - 推进2个字符去掉}},
\n </section>\n - 再次进入纯文本解析parseText,找到纯文本结束标签<,继续推进
</section>\n - 发现了结束标签,结束子元素的解析,**ancestors.pop()**弹出解析完子元素的标签,继续推进结果为:
>\n - 推进结束字符后结果为:
\n - 最后一次解析parseText,推进最后一个字符,当前字符串已经为空,解析完毕,返回。
我们看一下最后生成的AST树
{
"type": 0,
"children": [
{
"type": 1,
"ns": 0,
"tag": "section",
"tagType": 0,
"props": [
{
"type": 6,
"name": "id",
"value": {
"type": 2,
"content": "name",
"loc": {
"start": {
"column": 17,
"line": 2,
"offset": 17
},
"end": {
"column": 23,
"line": 2,
"offset": 23
},
"source": "\"name\""
}
},
"loc": {
"start": {
"column": 14,
"line": 2,
"offset": 14
},
"end": {
"column": 23,
"line": 2,
"offset": 23
},
"source": "id=\"name\""
}
},
{
"type": 7,
"name": "on",
"exp": {
"type": 4,
"content": "sayHello",
"isStatic": false,
"constType": 0,
"loc": {
"start": {
"column": 32,
"line": 2,
"offset": 32
},
"end": {
"column": 40,
"line": 2,
"offset": 40
},
"source": "sayHello"
}
},
"arg": {
"type": 4,
"content": "click",
"isStatic": true,
"constType": 3,
"loc": {
"start": {
"column": 25,
"line": 2,
"offset": 25
},
"end": {
"column": 30,
"line": 2,
"offset": 30
},
"source": "click"
}
},
"modifiers": [],
"loc": {
"start": {
"column": 24,
"line": 2,
"offset": 24
},
"end": {
"column": 41,
"line": 2,
"offset": 41
},
"source": "@click=\"sayHello\""
}
}
],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": " morning!",
"loc": {
"start": {
"column": 42,
"line": 2,
"offset": 42
},
"end": {
"column": 17,
"line": 3,
"offset": 59
},
"source": "\n morning!"
}
},
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"constType": 0,
"content": "name",
"loc": {
"start": {
"column": 20,
"line": 3,
"offset": 62
},
"end": {
"column": 24,
"line": 3,
"offset": 66
},
"source": "name"
}
},
"loc": {
"start": {
"column": 17,
"line": 3,
"offset": 59
},
"end": {
"column": 27,
"line": 3,
"offset": 69
},
"source": "{{ name }}"
}
}
],
"loc": {
"start": {
"column": 5,
"line": 2,
"offset": 5
},
"end": {
"column": 15,
"line": 4,
"offset": 84
},
"source": "<section id=\"name\" @click=\"sayHello\">\n morning!{{ name }}\n </section>"
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 1,
"line": 5,
"offset": 85
},
"source": "\n <section id=\"name\" @click=\"sayHello\">\n morning!{{ name }}\n </section>\n"
}
}
总结
这篇文章简单的介绍了AST的一个生成过程,很多细节并没有细说,大家可以自行阅读源码了解更多,文章如果有错误和不足的地方也希望大家多多斧正~,最后求个一键3连,给孩子点个关注吧!
|