【vue3源码笔记4】- 编译

Vue | 2021-03-10 02:05:13 1720次 2次

在 runtime 运行时,发现传入的是模板语法,则会走编译,编译后的字符串通过 new Function 形式传给组件的 render 方法。

vue 的 compiler 部分是一个转译器,和 babel 的原理上区别不大,都是用来做编译的,只不过 babel 处理的是 js,vue 处理的是一套自定义模板。

babel 是一个特殊的编译器(可以理解为转译器),它的大致工作原理为,简易编译器 

token -> parse -> travers -> transform -> generator

      vue 的模板编译链路很短:

parse -> transform -> generator

Parse

vue 中处理 case 是很复杂的,尽管逻辑上很清晰,但是体量大了也很痛苦,所以在写 svu 过程中尽可能的简化过程,只保证一些流程调通,支持数据、事件、条件判断这三种情况,其他的暂时没有精力去写,所以编译部分的解析按照简化版的思路去分析。

这个过程中需要处理传入的模板字符串,解析为一颗 ast 语法树,这颗树描述了 dom 结构和对应的位置信息,比如如下一段模板:

    template: `
            <div>
                <h1 v-if="res.value" name = '11'>
                    <p @click = "tes">11{{res.value}}</p>
                </h1>
                <h2 v-else>
                    <p>
                        <span>222</span>
                    </p>
                </h2 >
            </div>
        `,


经过处理后的 ast 信息为:


未命名1615392761.png


就是将模板字符串转为一个 dom 结构,多了一个 loc 来记录字符串的位置信息,创造 ast 的时候就是做这么单纯的事情,还是比较容易理解的,接下来就看下这个处理的过程。


核心逻辑在 parseChildren,这个过程会递归处理,分为三个 case:


 * 1、parseText 纯文本、字符串中的换行、空格等解析

 * 2、parseElement 节点解析,这里会递归 parseChildren

 * 3、parseInterpolation 动态绑定数据处理


 parseElement 的过程是一个前序遍历时取出标签前半段,后序遍历修正,做到字符串的分割就是依据正则,大致过程如下:

   function parseElement(context: ParserContext, ancestors: Element[]): any {
   
        // 拿出标签的前半部分 比如 <p @click = "tes">
        const element = parseTag(context, TagType.Start);
        
        // children
        ancestors.push(element);
        // 递归执行
        const children = parseChildren(context, ancestors);
        ancestors.pop();
    
        element.children = children;
        
        // 消除后半段标签
        if(startsWithEndTagOpen(context.source, element.tag)){
            parseTag(context, TagType.End);
        }
        
        // 消除后半个标签 需要重新计算source,因为 offset 标志会后移
        // 此时需要根据 context 最新的end位移来修正补全 source
        element.loc = getSelection(context, element.loc.start);
        return element;
    }


重点的计算在 parseTag,它主要根据一个正则来进行标签分段:

    const tagRe = /^<\/?([a-z][^\t\r\n\f />]*)/i;
    function parseTag(context: ParserContext, type: TagType): any {
        const start = getCursor(context);
        const [tagStart, tag] = tagRe.exec(context.source)!; // <div 、 </div
        
        // 位置计算
        advanceBy(context, tagStart.length);
        advanceSpace(context); // tag 空格 <div v-if=...>
        
        // 属性的解析
        let props = parseAttributes(context, type);
    
        if(context.source.length){
            // </div>: “</div”已被消除,这里消除“>”
            advanceBy(context, 1);
        }
    
        return{
            type: NodeTypes.ELEMENT,
            tag,
            props,
            loc: getSelection(context, start)
        }
    }

所有的字符信息都是通过 context 中来操作,比如原始的一段字符串,经过前序编译过程中会取出标签前半段,后续遍历补充上后半段的闭合标签以及中间的内容部分(内容转换由 parseText 完成),通过一些图看下这个转换过程:


前序遍历

微信截图_20210311115701.png

这个过程是从 div -> h1 -> p 前半部分【蓝框】先解析完成,存入对应节点上,在递归过程中发现【绿色】文本,则存入 p 的子节点,此时的 context 字符串信息只剩下【红框】部分,等待 h1 节点内所有子节点全部完成后(经过后续遍历),再进入 h2 解析。此时节点上的信息只有前半段,所以再经过后序遍历补全信息。


后序遍历

后序遍历时首先要处理的肯定是把上次遗留的标签处理干净:

.... </p>
..</ h>

消除完这些残留标签后,字符位置发生了改变,此时再重新根据最新的 context 偏移量和源字符串求出最终的字符串信息:


微信截图_20210311122957.png


等到 h1 节点完成后,就轮到它的兄弟节点,h2 重复上述流程,整个节点的处理过程为:

div -- 进入

    h1 -- 进入

        p -- 进入

        p -- 完成 

    h1 -- 完成

    h2 -- 进入

        p -- 进入

            span -- 进入

            span -- 完成

        p -- 完成

    h2 -- 完成

div -- 完成


流程上比较清晰了,明白了要解析一个 html 模板需要怎么做,那么既然这是一段字符串,如何进行分段?上面也提到了就是通过正则,只需要校验出 【<div】 或 【</div】 即可,然后标签上的属性再通过属性解析完成放入 props 中,最后手动剔除【>】即可,核心点就是 context 字符串的不断消减,这个过程中涉及到大量的位置计算调用,以及位置修正,这里不再展开描述。


Transform

拿到 ast 之后,ast 描述了这个模板节点的层级关系和其他信息,接下来要做一些转换操作,可以立即为语法制导分析,构造语法分析树,这样为了更方便的生成目标代码。

首先是从 traverseNode 开始进行的递归,svu 中只处理了如下几种语义:


1、模板 if:vIf

2、事件绑定:vOn

3、文本:transformText

4、节点:transformElement



至于静态节点处理是在转换完成后再进行处理,因为它需要知道某个节点有没有动态属性,没有的话就提升为静态节点,仅此而已,通过 patchFlag 是否有值进行判断是否为静态节点,这个值会在节点的处理过程中给出。


对于条件判断和事件绑定则是对 ast 树的前序遍历中进行信息处理,后序节点遍历中仅仅是生成固定的语法,保证数据的层层往上挂靠。对于文本、节点的处理都是在后序遍历过程中进行。


在做转换的过程中,会涉及到一些 ast 节点的操作,可以将一些操作方法进行类似 jq 式封装,方便节点的操作。


processIf

export function processIf(...) {
    // 属性解析时取到的
    if (dir.name === 'if') {
        const branch = createIfBranch(node, dir)
        const ifNode = {
            type: NodeTypes.IF,
            loc: node.loc,
            branches: [branch]
        }
        // 旧的 ast 转为 v-if 信息
        context.replaceNode(ifNode)
        if (processCodegen) {
            // 这里返回这个方法,等到v-if的节点完成时再执行
            return processCodegen(ifNode, branch, true)
        }
    } else {
        // v-else
        // else 过程中肯定都是父节点的所有子节点 [if, else, else ...]
        const siblings = context.parent!.children
        let i = siblings.indexOf(node)
        while (i-- >= -1) {
            const sibling = siblings[i]

            if (sibling && sibling.type === NodeTypes.IF) {
                // 删除当前 else节点  为了和 if 的 branch放在一起
                context.removeNode()
                const branch = createIfBranch(node, dir)
                // 推入 if 的branch
                sibling.branches.push(branch)
                const onExit = processCodegen && processCodegen(sibling, branch, false)
                // else 分支里面还有节点内容 需要再次进行转换
                traverseNode(branch, context)
                // 这里和 if 不同,直接执行了,一样的道理 后序
                onExit && onExit()
                context.currentNode = null
            }
            break
        }
    }
}

再通过 processCodegen 回调,最终生成 if 分支(consequent)和 else 分支(alternate)的代码,结构如下图:


微信截图_20210311173714.png


transformElement

vue 源码中,这部分写的内容很多,考虑了很多的情况,这里只考虑解析 props,比如事件的绑定解析,最终赋值到 vnode 的 dynamicProps,这样在runtime可以直接使用这个方法;再一个就是 patchFlag 的判断,判断是否为动态属性、静态属性,方便静态节点的提升。


Generator

得到转义后的信息,最后一步就是递归打印代码,最终通过 new Function 的形式执行。完结!

具体代码请查看:https://github.com/wineSu/svu

2人赞

分享到: