Vue3源码 第五篇-Vue3 模版compile AST生成篇

系列文章目录

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)) {
    	// 是HTML DOM,nodeType是DOM的属性
        if (template.nodeType) {
        	// 将dom内的html作为模版
            template = template.innerHTML;
        }
        else {
            warn(`invalid template option: `, template);
            return NOOP;
        }
    }
    // 获取缓存,查看缓存中是否存在template为key的,有则直接返回
    // compileCache = Object.create(null); 一个空对象
    const key = template;
    const cached = compileCache[key];
    if (cached) {
        return cached;
    }
    // 如果传入的是id
    if (template[0] === '#') {
    	// 获取DOM节点
        const el = document.querySelector(template);
        if (!el) {
            warn(`Template element not found or is empty: ${template}`);
        }
        template = el ? el.innerHTML : ``;
    }
    // 执行compile$1函数,我们在下面详细读一下内部结构
    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));
    // 将我们通过compile$1返回的字符串创建成函数
    const render = (new Function(code)()
    );
    // runtime-compile 设置成true,运行时编译,不同的情况对应着不同的render包装
    render._rc = true;
    // 将渲染函数放入到缓存中
    return (compileCache[key] = render);
}

1.1、compile$1

具体执行的编译模版的函数

function compile$1(template, options = {}) {
	// 直接调用函数,这里面对参数进行了处理
	// nodeTransforms
    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 || []) // user transforms
        ],
        directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // user transforms
        )
    }));
    // 通过语法树生成
    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 /* DATA */, []), getSelection(context, start));
}

1.4、createParserContext(创建语法解析器的上下文)

const defaultParserOptions = {
    delimiters: [`{{`, `}}`],// 插值符,就是我们在模版中默认是{{name}}为模版变量
    getNamespace: () => 0 /* HTML */,
    getTextMode: () => 0 /* DATA */,
    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) {
        // @ts-ignore
        options[key] = rawOptions[key] || defaultParserOptions[key];
    }
    // 上下文中存入了配置项options,字符串模版的行号,列号,偏移量,原始模版,当前模版,都是为了后面做语法解析时需要的!
    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 /* HTML */;
    // 存放解析过的节点
    const nodes = [];
    // 循环判断标签是否已经结束
    while (!isEnd(context, mode, ancestors)) {
    	// 获取当前解析的内容
        const s = context.source;
        let node = undefined;
        if (mode === 0 /* DATA */ || mode === 1 /* RCDATA */) {
        	// 如果是 {{ 开头的话,进入插值逻辑
            if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
                // 解释插值
                node = parseInterpolation(context, mode);
            }
            // 如果是<开始认为这是标签或者是注解
            else if (mode === 0 /* DATA */ && s[0] === '<') {
                if (s.length === 1) {
                    emitError(context, 5 /* EOF_BEFORE_TAG_NAME */, 1);
                }
                // 如果是声明或者是注解
                else if (s[1] === '!') { 
                   	//  ...解析注释
                }
                // 如果是结束标签
                else if (s[1] === '/') {
                    // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
                    if (s.length === 2) {
                        emitError(context, 5 /* EOF_BEFORE_TAG_NAME */, 2);
                    }
                    else if (s[2] === '>') {// </>
                        emitError(context, 14 /* MISSING_END_TAG_NAME */, 2);
                        advanceBy(context, 3);
                        continue;
                    }
                    else if (/[a-z]/i.test(s[2])) {
                        emitError(context, 23 /* X_INVALID_END_TAG */);
                        parseTag(context, 1 /* End */, parent);
                        continue;
                    }
                    else {
                        emitError(context, 12 /* INVALID_FIRST_CHARACTER_OF_TAG_NAME */, 2);
                        node = parseBogusComment(context);
                    }
                }
                // 否则就是开始标签类似于<p> 匹配到p
                else if (/[a-z]/i.test(s[1])) {
                	// 解析标签
                    node = parseElement(context, ancestors);
                }
                else if (s[1] === '?') {
                    emitError(context, 21 /* UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME */, 1);
                    node = parseBogusComment(context);
                }
                else {
                    emitError(context, 12 /* INVALID_FIRST_CHARACTER_OF_TAG_NAME */, 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);
        }
    }
    // Whitespace management for more efficient output
    // (same as v2 whitespace: 'condense')
    let removedWhitespace = false;
    if (mode !== 2 /* RAWTEXT */ && mode !== 1 /* RCDATA */) {
        for (let i = 0; i < nodes.length; i++) {
            const node = nodes[i];
            if (!context.inPre && node.type === 2 /* TEXT */) {
                if (!/[^\t\r\n\f ]/.test(node.content)) {
                    const prev = nodes[i - 1];
                    const next = nodes[i + 1];
                    // If:
                    // - the whitespace is the first or last node, or:
                    // - the whitespace is adjacent to a comment, or:
                    // - the whitespace is between two elements AND contains newline
                    // Then the whitespace is ignored.
                    if (!prev ||
                        !next ||
                        prev.type === 3 /* COMMENT */ ||
                        next.type === 3 /* COMMENT */ ||
                        (prev.type === 1 /* ELEMENT */ &&
                            next.type === 1 /* ELEMENT */ &&
                            /[\r\n]/.test(node.content))) {
                        removedWhitespace = true;
                        nodes[i] = null;
                    }
                    else {
                        // Otherwise, condensed consecutive whitespace inside the text
                        // down to a single space
                        node.content = ' ';
                    }
                }
                else {
                    node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ');
                }
            }
        }
        if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
            // remove leading newline per html spec
            // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
            const first = nodes[0];
            if (first && first.type === 2 /* TEXT */) {
                first.content = first.content.replace(/^\r?\n/, '');
            }
        }
    }
    return removedWhitespace ? nodes.filter(Boolean) : nodes;
}

1.7、 isEnd(查看内容是否结束)

每次循环开始时调用该方法检查模版字符串当前是否已经结束。

function isEnd(context, mode, ancestors) {
	// 获取当前要解析的模版内容,source会发生变化,解析时会不断截断原template
    const s = context.source;
   	// 这里我们只关注0的情况
    switch (mode) {
        case 0 /* DATA */:
        	// 如果标签开头为结束标签,需要去寻找是否存在对应的开始标签来确认这段结束了
            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) {
	// 获得插值的开始和结束,open = {{ close = }}
    const [open, close] = context.options.delimiters;
    // 找到{{的位置,从{{之后开始 为了之后推进时截取掉{{后计算插值变量中的内容长度
    const closeIndex = context.source.indexOf(close, open.length);
    if (closeIndex === -1) {
        emitError(context, 25 /* X_MISSING_INTERPOLATION_END */);
        return undefined;
    }
    // 获取光标位置的行、列、偏移信息
    const start = getCursor(context);
    // 根据给定长度对模版进行推进,对source模版进行截取 
    advanceBy(context, open.length);
    // 获取开始位置
    const innerStart = getCursor(context);
    // 获取结束位置
    const innerEnd = getCursor(context);
    // 获取插值字符串变量的长度 {{name}} 4
    const rawContentLength = closeIndex - open.length;
    // 截取到内容,之前source已经在推进(advanceBy)过程中截取掉了{{
    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 /* INTERPOLATION */,
        content: {
            type: 4 /* SIMPLE_EXPRESSION */,
            isStatic: false,
            // Set `isConstant` to false by default and will decide in transformExpression
            constType: 0 /* NOT_CONSTANT */,
            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 /* newline char code */) {
            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) {
    // Start tag.
    const wasInPre = context.inPre;
    const wasInVPre = context.inVPre;
    // 获取最近的祖先元素
    const parent = last(ancestors);
    // 解析标签
    const element = parseTag(context, 0 /* Start */, 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 /* End */, parent);
    }
    else {
        emitError(context, 24 /* 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, 8 /* EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT */);
            }
        }
    }
    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);
    // save current state in case we need to re-parse attributes with v-pre
    const cursor = getCursor(context);
    const currentSource = context.source;
    // 解析属性
    let props = parseAttributes(context, type);
    // 检查是否是<pre>标签
    if (context.options.isPreTag(tag)) {
        context.inPre = true;
    }
    // check v-pre
    if (!context.inVPre &&
        props.some(p => p.type === 7 /* DIRECTIVE */ && p.name === 'pre')) {
        context.inVPre = true;
        // reset context
        extend(context, cursor);
        context.source = currentSource;
        // re-parse attrs and filter out v-pre itself
        props = parseAttributes(context, type).filter(p => p.name !== 'v-pre');
    }
    // Tag close.
    let isSelfClosing = false;
    if (context.source.length === 0) {
        emitError(context, 9 /* EOF_IN_TAG */);
    }
    else {
        isSelfClosing = startsWith(context.source, '/>');
        if (type === 1 /* End */ && isSelfClosing) {
            emitError(context, 4 /* END_TAG_WITH_TRAILING_SOLIDUS */);
        }
        advanceBy(context, isSelfClosing ? 2 : 1);
    }
    let tagType = 0 /* ELEMENT */;
    const options = context.options;
    if (!context.inVPre && !options.isCustomElement(tag)) {
        const hasVIs = props.some(p => p.type === 7 /* DIRECTIVE */ && p.name === 'is');
        if (options.isNativeTag && !hasVIs) {
            if (!options.isNativeTag(tag))
                tagType = 1 /* COMPONENT */;
        }
        else if (hasVIs ||
            isCoreComponent(tag) ||
            (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
            /^[A-Z]/.test(tag) ||
            tag === 'component') {
            tagType = 1 /* COMPONENT */;
        }
        if (tag === 'slot') {
            tagType = 2 /* SLOT */;
        }
        else if (tag === 'template' &&
            props.some(p => {
                return (p.type === 7 /* DIRECTIVE */ && isSpecialTemplateDirective(p.name));
            })) {
            tagType = 3 /* TEMPLATE */;
        }
    }
    return {
        type: 1 /* ELEMENT */,
        ns,
        tag,
        tagType,
        props,
        isSelfClosing,
        children: [],
        loc: getSelection(context, start),
        codegenNode: undefined // to be created during transform phase
    };
}

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 /* End */) {
                // ...错误验证
            }
            const attr = parseAttribute(context, attributeNames);
            if (type === 0 /* Start */) {
                props.push(attr);
            }
            if (/^[^\t\r\n\f />]/.test(context.source)) {
                emitError(context, 15 /* MISSING_WHITESPACE_BETWEEN_ATTRIBUTES */);
            }
            // 继续推进,截取掉空格字符
            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 /* DUPLICATE_ATTRIBUTE */);
    }
    nameSet.add(name);
    // ...验证属性逻辑
    // 推进属性名字的长度
    advanceBy(context, name.length);
    // Value
    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 /* MISSING_ATTRIBUTE_VALUE */);
        }
    }
    // 获取在源模版的位置
    const loc = getSelection(context, start);
    // 匹配vue属性 v-if @click :data 
    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 /* X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END */);
                }
                content = content.substr(1, content.length - 2);
            }
            else if (isSlot) {
                // #1241 special case for v-slot: vuetify relies extensively on slot
                // names containing dots. v-slot doesn't have any modifiers and Vue 2.x
                // supports such usage so we are keeping it consistent with 2.x.
                content += match[3] || '';
            }
            arg = {
                type: 4 /* SIMPLE_EXPRESSION */,
                content,
                isStatic,
                constType: isStatic
                    ? 3 /* CAN_STRINGIFY */
                    : 0 /* NOT_CONSTANT */,
                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 /* DIRECTIVE */,
            name: dirName,
            exp: value && {
                type: 4 /* SIMPLE_EXPRESSION */,
                content: value.content,
                isStatic: false,
                // Treat as non-constant by default. This can be potentially set to
                // other values by `transformExpression` to make it eligible for hoisting.
                constType: 0 /* NOT_CONSTANT */,
                loc: value.loc
            },
            arg,
            modifiers: match[3] ? match[3].substr(1).split('.') : [],
            loc
        };
    }
    return {
        type: 6 /* ATTRIBUTE */,
        name,
        value: value && {
            type: 2 /* TEXT */,
            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 /* TEXT */,
        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 /* RAWTEXT */ ||
        mode === 3 /* CDATA */ ||
        rawText.indexOf('&') === -1) {
        return rawText;
    }
    else {
        // DATA or RCDATA containing "&"". Entity decoding required.
        return context.options.decodeEntities(rawText, mode === 4 /* ATTRIBUTE_VALUE */);
    }
}

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
  1. parseChildren 解析子节点
  2. 调用isEnd方法判断当前解析节点是否已经结束了,上面介绍过这个方法.
  3. 通过判断字符串开头得知是文字解析,进入parseTextData逻辑解析文字,根据<和{{这两个标签找到结束位置为4
  4. 调用advancBy方法对字符串进行推进并且调用advancePositionWithMutation进行偏移量计算,最后将文字部分进行截取,达到字符串推进的目的
  5. 推进后的字符串:<section id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n
  6. pushNode将解析的结果放入到集合中
  7. 继续读取字符串,根据判断得到需要解析标签,进入parseElement逻辑解析标签
  8. 调用parseTag解析标签,解析出section标签后调用advancBy方法和advancePositionWithMutation进行推进和偏移量计算,这次推进8个字符(<section
  9. 推进后的字符串: id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n
  10. 新字符串开头可能含有空格,所以下一步继续先推进,去除掉空格,推进后的字符串:id="name" @click="sayHello">\n morning!{{ name }}\n </section>\n
  11. 继续解析标签内部的属性信息parseAttributes内部调用parseAttribute解析每一个属性
  12. 获取属性节点id,调用advanceBy推进两个字符,生成新的字符串="name" @click="sayHello">\n morning!{{ name }}\n </section>\n
  13. 在去除=号和空格,推进后的结果为"name" @click="sayHello">\n morning!{{ name }}\n </section>\n
  14. 在去除掉引号,推进后的结果为name" @click="sayHello">\n morning!{{ name }}\n </section>\n
  15. 解析其中的文字信息parseTextData,将字符串推进4个字符,推进后的结果为:" @click="sayHello">\n morning!{{ name }}\n </section>\n
  16. 去除引号和空格继续推进,推进后的结果为:@click="sayHello">\n morning!{{ name }}\n </section>\n
  17. 继续解析下一个属性为@click,继续推进6个字符,推进后的结果为:="sayHello">\n morning!{{ name }}\n </section>\n
  18. 去除引号解析内部的value值,sayHello">\n morning!{{ name }}\n </section>\n
  19. 继续调用parseTextData解析纯文本数据,解析后继续推进">\n morning!{{ name }}\n </section>\n
  20. 去除引号,继续推进>\n morning!{{ name }}\n </section>\n
  21. 标签内的属性遍历结束,检查标签是否是自闭和,自闭和推进2个字符,非自闭和推进1个字符,推进后的结果:\n morning!{{ name }}\n </section>\n
  22. 解析完标签后,在祖辈数组ancestors添加该标签,继续调用parseChildren解析标签下的子元素
  23. 获取到开头是字符串,进入parseText解析纯文本的逻辑,找到纯文本结束的标志,这次找到{{的结束位置,进行推进,推进结果为:{{ name }}\n </section>\n
  24. 推进2个字符,去掉{{,结果为: name }}\n </section>\n
  25. 调用parseTextData解析纯文本数据,继续推进}}\n </section>\n
  26. 推进2个字符去掉}},\n </section>\n
  27. 再次进入纯文本解析parseText,找到纯文本结束标签<,继续推进</section>\n
  28. 发现了结束标签,结束子元素的解析,**ancestors.pop()**弹出解析完子元素的标签,继续推进结果为:>\n
  29. 推进结束字符后结果为:\n
  30. 最后一次解析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连,给孩子点个关注吧!
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小疯疯0413

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值