在上一章中咱们了解到了Vue是怎么将组件注册到全局的,咱们运用的时候就直接运用一个标签的方法就能够运用这个组件;

那么这个组件是怎么从这个模板中正确的解析出来的呢?这便是咱们今天要学习的内容,还是持续从上一章的示例开端;

历史章节能够拉到文末查看,全系列文章链接都会放到文末。

1. Vue 的模板字符串

在上一章最终咱们知道一个模板字符串最终会被创立成一个子树,首要咱们来看看这个模板字符串是怎么被创立的;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id='app'>
    <my-component></my-component>
</div>
</body>
<script src="./vue.global.js"></script>
<script>
    const { createApp, h } = Vue;
    const app = createApp({});
    const MyComponent = {
        render() {
            return h('div', 'MyComponent');
        }
    };
    app.component('MyComponent', MyComponent);
    debugger;
    app.mount('#app');
</script>
</html>

这是我上一章的示例代码,咱们能够mount的上面打上断点进行调试,查看模板生成的进程:

【源码&库】Vue3的模板转换为AST的进程

跟着调试成果能够看到,咱们的模板有三种方法能够创立:

  1. 直接运用一个函数,这个函数的返回值便是咱们的模板;
  2. 一个目标中有一个render函数,这个函数的返回值便是咱们的模板;
  3. 一个目标中有一个template特点,这个特点的值便是咱们的模板;

假如上面三种方法都没有,那么就会运用容器的innerHTML作为咱们的模板,这个innerHTML最终会被赋值到template特点上;

app._component便是咱们在createApp的时候传入的目标,这个目标便是咱们的根组件;

后续便是挂载的进程,这一块能够查看我之前的章节:【源码&库】 Vue3 的组件是怎么挂载的?

2. 模板字符串转换为AST

在之前的章节中仅仅解说了一个组件的挂载流程,但是仅仅单组件的挂载,回忆上面提到的文章,能够看到有一个finishComponentSetup的调用,其中有一段代码:

function finishComponentSetup(instance, isSSR, skipOptions) {
    // 删去影响阅览的代码
    // 根据之前的学习咱们知道 type 其实便是组件目标
    const Component = instance.type;
    // 组件目标的 template 便是模板字符串
    const template = Component.template || resolveMergedOptions(instance).template;
    // 经过 compile 函数将模板字符串编译成 render 函数
    Component.render = compile$1(template, finalCompilerOptions);
    instance.render = Component.render || NOOP;
}

这儿的compile$1函数指向的是compileToFunction,咱们来看看这个函数的完成:

function compileToFunction(template, options) {
    // 删去一些不会进入的分支
    // 将整个模板字符串作为缓存的 key
    const key = template;
    const cached = compileCache[key];
    if (cached) {
        return cached;
    }
    const opts = extend(
        {
            hoistStatic: true,
            onError: onError,
            onWarn: (e) => onError(e, true)
        },
        options
    );
    if (!opts.isCustomElement && typeof customElements !== "undefined") {
        opts.isCustomElement = (tag) => !!customElements.get(tag);
    }
    // 中心:经过 compile 函数生成一段代码字符串,然后经过 new Function(code) 生成 render 函数
    const {code} = compile(template, opts);
    function onError(err, asWarning = false) {
        // ...
    }
    const render = new Function(code)();
    render._rc = true;
    return compileCache[key] = render;
}

内部中心便是compile函数,他经过这个函数会发生一个代码字符串,然后经过new Function(code)的方法生成一个render函数;

咱们现在来看看compile函数的完成:

function compile(template, options = {}) {
    // 内部便是调用 baseCompile,只不过传入了一些默许的参数
    return baseCompile(
        template,
        extend({}, parserOptions, options, {
            nodeTransforms: [
                // ignore <script> and <tag>
                // this is not put inside DOMNodeTransforms because that list is used
                // by compiler-ssr to generate vnode fallback branches
                ignoreSideEffectTags,
                ...DOMNodeTransforms,
                ...options.nodeTransforms || []
            ],
            directiveTransforms: extend(
                {},
                DOMDirectiveTransforms,
                options.directiveTransforms || {}
            ),
            transformHoist: null
        })
    );
}
function baseCompile(template, options = {}) {
    // 删去一些目前调试状况不会进入的代码
    // 经过 baseParse 解析模板字符串,得到 AST
    const ast = baseParse(template, options);
    // 获取编译器选项
    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
    // 转换 AST
    transform(
        ast,
        extend({}, options, {
            prefixIdentifiers,
            nodeTransforms: [
                ...nodeTransforms,
                ...options.nodeTransforms || []
                // user transforms
            ],
            directiveTransforms: extend(
                {},
                directiveTransforms,
                options.directiveTransforms || {}
                // user transforms
            )
        })
    );
    // 生成代码
    return generate(
        ast,
        extend({}, options, {
            prefixIdentifiers
        })
    );
}

这儿的baseParse函数便是咱们的模板字符串转换为AST的中心代码,看到这儿终所以看到了ast;

3. 模板字符串转换为AST的中心代码

咱们来看看baseParse函数的完成:

function baseParse(content, options = {}) {
    // content 便是咱们要解析的内容
    // 经过 createParserContext 创立一个解析上下文
    const context = createParserContext(content, options);
    // 获取解析上下文中的游标信息
    const start = getCursor(context);
    // 这儿是三步操作
    // 1. parseChildren 解析子节点
    // 2. getSelection 获取从解析开端方位到当时方位的规模
    // 3. createRoot 返回整个模板结构的根节点
    return createRoot(
        parseChildren(context, 0, []),
        getSelection(context, start)
    );
}

baseParse虽然内容很少,但是里边包含的信息量是非常大的,咱们接着剖析createParserContext函数:

3.1 createParserContext

function createParserContext(content, rawOptions) {
    const options = extend({}, defaultParserOptions);
    // 生成解析器选项,假如用户没有传入,则运用默许的
    // 这儿的解析器比较多,感兴趣的同学能够深挖一下,后面这些解析器会用到
    let key;
    for (key in rawOptions) {
        options[key] = rawOptions[key] === void 0 ? defaultParserOptions[key] : rawOptions[key];
    }
    // 生成解析器上下文
    return {
        options, // 解析器选项
        column: 1, // 当时解析方位的列号,初始值为1
        line: 1, // 当时解析方位的行号,初始值为1
        offset: 0, // 当时解析方位相对于整个模板字符串的偏移量,初始值为0
        originalSource: content, // 整个模板字符串的原始内容
        source: content, // 当时解析方位之后的模板内容
        inPre: false, // 表明是否当时坐落 <pre> 标签内,初始值为 false
        inVPre: false, // 表明是否当时坐落带有 v-pre 指令的元素内,初始值为 false
        onWarn: options.onWarn, // 正告处理函数,从解析选项中获取,用于在解析进程中宣布正告
    };
}

整体来说createParserContext函数便是生成一个解析上下文,这个上下文中包含了解析器的选项,当时解析方位的信息,以及一些解析进程中需求用到的函数;

生成的解析器上下文会用于后续的解析进程中,传递给其他解析函数进行运用;

3.2 getCursor

function getCursor(context) {
    const { column, line, offset } = context;
    return { column, line, offset };
}

这个函数便是获取当时解析方位的信息,非常简单,直接返回解析上下文中的信息;

3.3 parseChildren

温馨提示:这儿的代码量比较多,而且逻辑会比较复杂,建议大家经过调试的方法,然后对照着我这儿的代码解释来学习;

function parseChildren(context, mode, ancestors) {
    // 获取父节点,ancestors 是一个数组,里边寄存的是当时节点的父节点
    // last 函数是一个辅助函数,用于获取数组的最终一个元素
    const parent = last(ancestors);
    // 获取当时节点的命名空间(ns),假如没有父节点,则命名空间为0
    const ns = parent ? parent.ns : 0;
    // 用于存储解析得到的子节点
    const nodes = [];
    // 经过 isEnd 函数判别当时节点是否是结束节点,假如不是则持续解析
    while (!isEnd(context, mode, ancestors)) {
        // 获取当时解析方位的源代码片段
        const s = context.source;
        // 存储解析得到的节点
        let node = void 0;
        // 这儿的 mode 默许传入了 0,表明解析的是元素节点
        if (mode === 0 || mode === 1) {
            // 查看是否在非 v-pre 环境下而且当时源代码以插值表达式的开端符号为开端
            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] === "!") {
                    // 1. 注释节点
                    if (startsWith(s, "<!--")) {
                        node = parseComment(context);
                    }
                    // 2. 伪注释节点
                    else if (startsWith(s, "<!DOCTYPE")) {
                        node = parseBogusComment(context);
                    }
                    // 3. 大块文本节点
                    else if (startsWith(s, "<![CDATA[")) {
                        // 大块文本节点不能是根节点
                        if (ns !== 0) {
                            node = parseCDATA(context, ancestors);
                        } else {
                            emitError(context, 1);
                            node = parseBogusComment(context);
                        }
                    }
                    // 其他状况表明不是一个合法的 xml 节点信息,都以一般文原本进行处理
                    else {
                        emitError(context, 11);
                        node = parseBogusComment(context);
                    }
                }
                // 结束标签符号
                else if (s[1] === "/") {
                    // 只有一个 "</",则报错
                    if (s.length === 2) {
                        emitError(context, 5, 2);
                    }
                    // "</>" 不合法,报错
                    else if (s[2] === ">") {
                        emitError(context, 14, 2);
                        advanceBy(context, 3);
                        continue;
                    }
                    // "</a" 这种状况,表明是一个结束标签,进一步进行解析
                    else if (/[a-z]/i.test(s[2])) {
                        emitError(context, 23);
                        parseTag(context, 1 /* End */, parent);
                        continue;
                    }
                    // 其他状况不合法,作为一般文本处理
                    else {
                        emitError(context, 12, 2);
                        node = parseBogusComment(context);
                    }
                }
                // 正常的开端标签 "<a" 这种状况
                else if (/[a-z]/i.test(s[1])) {
                    node = parseElement(context, ancestors);
                }
                // 开端标签是 “<?" 这种可能是 xml ,作为一般文本处理
                else if (s[1] === "?") {
                    emitError(context, 21, 1);
                    node = parseBogusComment(context);
                }
                // 其他状况都是不合法的状况
                else {
                    emitError(context, 12, 1);
                }
            }
        }
        // 假如 node 没有值就阐明上面的解析都没有成功,那么就作为一般文原本处理
        if (!node) {
            node = parseText(context, mode);
        }
        // 假如 node 是一个数组,则阐明解析得到的是多个节点,需求将其展开
        if (isArray(node)) {
            for (let i = 0; i < node.length; i++) {
                pushNode(nodes, node[i]);
            }
        }
        // 否则就直接加入到 nodes 数组中
        else {
            pushNode(nodes, node);
        }
    }
    // 符号是否移除了空白节点
    let removedWhitespace = false;
    // mode 是 0,会进入这个分支
    if (mode !== 2 && mode !== 1) {
        // 根据解析选项中的 whitespace 装备,判别是否需求进行空白字符压缩
        const shouldCondense = context.options.whitespace !== "preserve";
        // 遍历解析得到的节点
        for (let i = 0; i < nodes.length; i++) {
            // 获取当时子节点
            const node = nodes[i];
            // 假如当时节点是一个一般文本节点
            if (node.type === 2) {
                // 假如不在 <pre> 标签内
                if (!context.inPre) {
                    // 全空白字符的文本检测
                    if (!/[^trnf ]/.test(node.content)) {
                        // 获取上一个节点和下一个节点
                        const prev = nodes[i - 1];
                        const next = nodes[i + 1];
                        // 这儿的状况比较复杂,拆解解释为如下:
                        // 1. 假如前后节点存在其中一个为空,或许进行了空白字符压缩
                        // 2. 而且前后节点都是文本节点或前后节点都是元素节点或前后节点一个是文本节点一个是元素节点
                        // 3. 而且文本节点内容包含换行符或回车符
                        if (!prev || !next || shouldCondense && (prev.type === 3 && next.type === 3 || prev.type === 3 && next.type === 1 || prev.type === 1 && next.type === 3 || prev.type === 1 && next.type === 1 && /[rn]/.test(node.content))) {
                            // 这儿主要处理的是换行符和回车符的状况,换行是没有意义的,会删去这个节点
                            removedWhitespace = true;
                            nodes[i] = null;
                        }
                        // 不满足上述的条件就替换成一个空格
                        else {
                            node.content = " ";
                        }
                    }
                    // 压缩空白字符为一个空格
                    else if (shouldCondense) {
                        node.content = node.content.replace(/[trnf ]+/g, " ");
                    }
                }
                // 压缩空白字符为一个空格
                else {
                    node.content = node.content.replace(/rn/g, "n");
                }
            }
            // 假如是注释节点,而且禁用了注释节点,那么就删去这个节点
            else if (node.type === 3 && !context.options.comments) {
                removedWhitespace = true;
                nodes[i] = null;
            }
        }
        // 假如当时处于 <pre> 标签内,而且存在父节点以及父节点的标签是一个 <pre> 标签:
        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/, "");
            }
        }
    }
    // 返回解析得到的节点,假如 removedWhitespace 为 true,则阐明移除了空白节点,需求过滤掉
    return removedWhitespace ? nodes.filter(Boolean) : nodes;
}

这个函数的效果便是解析子节点,这儿的子节点包含元素节点、文本节点、注释节点等等;

在这儿并没有跟进到每个不同类型的节点解析中,因为自身这儿的代码量就比较大,更加细节的代码量就更大了,所以这儿仅仅大约的了解一下整个解析进程,后续会详细剖析每个节点的解析进程;

3.4 getSelection

function getSelection(context, start, end) {
    end = end || getCursor(context);
    return {
        start,
        end,
        source: context.originalSource.slice(start.offset, end.offset)
    };
}

这个函数的效果便是获取从解析开端方位到当时方位的规模,这个规模包含开端方位、结束方位、以及源代码片段;

3.5 createRoot

function createRoot(children, loc = locStub) {
    return {
        type: 0,
        children,
        helpers: /* @__PURE__ */ new Set(),
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: void 0,
        loc
    };
}

到这儿就能够看到整个模板字符串转换为AST的进程了,而且也看到了AST的结构;

这儿主要的内容还是整个children字段,它寄存的是整个模板字符串解析得到的节点树,这个便是咱们的AST

4. 总结

这一章主要解说了Vue是怎么将模板字符串转换为AST的,这儿的AST是一个树状结构,里边包含了咱们模板字符串的一切信息;

到这儿咱们就能够知道,咱们的模板字符串是怎么被解析的,这儿的AST后续还会被解析为render函数,然后经过new Function(code)的方法生成一个render函数;

因为篇幅约束加上这一章的内容比较难以理解,所以这儿仅仅大约的解说了一下整个进程,后续持续解说后续的流程;

历史章节