Vue源码速读 | 第五章:AST 语法树处理揭秘

78 阅读3分钟

五、AST语法树处理

1. baseCompile

在上一节主要介绍了vue是如何处理模板template,将html结构的模板转换为对象形式的AST语法树。本节主要介绍如何将AST语法树转为render函数。

最终我们生成的AST语法树为

const ast = {
    type: 1,
    children: {
        type: 4,
        tag: 'p',
        tagType: 0,
        children:{
            type: 2,
            content: {
                type: 3,
                content:'msg'
            }
        }
    },
    helpers: [],
}
function baseCompile(template, options) {
    const ast = baseParse(template);
    transform(ast, Object.assign(options, {
        nodeTransforms: [transformElement, transformText, transformExpression],
    }));
    return generate(ast);
}
}

2. transform

接下来通过transform以及传入的三个tranform函数来处理AST语法树

function transform(root, options = {}) {
    const context = createTransformContext(root, options);
    traverseNode(root, context);
    createRootCodegen(root);
    root.helpers.push(...context.helpers.keys());
}

2.1 createTransformContext

给root(语法树)添加一个nodeTransform转换属性,值为前面传入的三个处理函数。

同时增加了一个名为helpers的map结构,通过helper方法更改helpers对应的计数值

function createTransformContext(root, options) {
    const context = {
        root,
        nodeTransforms: options.nodeTransforms || [],
        helpers: new Map(),
        helper(name) {
            const count = context.helpers.get(name) || 0;
            context.helpers.set(name, count + 1);
        },
    };
    return context;
}

2.2 traverseNode

遍历和处理AST语法树的节点

遍历处理函数,通过三个处理函数分别处理AST语法树,同时判断节点类型,如果为标签节点(type == 4)则遍历子节点进行转换。

function traverseNode(node, context) {
    const type = node.type; // 获取节点类型
    const nodeTransforms = context.nodeTransforms; // 获取处理函数
    const exitFns = []; // 退出函数数组
    for (let i = 0; i < nodeTransforms.length; i++) { //遍历处理函数
        const transform = nodeTransforms[i];
        const onExit = transform(node, context); 
        if (onExit) { //判断有无退出函数
            exitFns.push(onExit);
        }
    }
    switch (type) {
        case 2:
            context.helper(TO_DISPLAY_STRING);
            break;
        case 1:
        case 4:
            traverseChildren(node, context); //如果为标签节点则遍历内部子节点
            break;
    }
    let i = exitFns.length;
    while (i--) {
        exitFns[i](); //连续执行退出函数,具体如下
    }
}
function traverseChildren(parent, context) {
    parent.children.forEach((node) => {
        traverseNode(node, context);
    });
}

处理结果

const node = {
    "type": 1,
    "children": [
        {
            "type": 4,
            "tag": "p",
            "tagType": 0,
            "children": [
                {
                    "type": 2,
                    "content": {
                        "type": 3,
                        "content": "_ctx.msg"
                    }
                }
            ],
            "codegenNode": {
                "type": 4,
                "tag": "'p'",
                "props": null,
                "children": {
                    "type": 2,
                    "content": {
                        "type": 3,
                        "content": "_ctx.msg"
                    }
                }
            }
        }
    ],
    "helpers": [
        Symbol(toDisplayString),
        Symbol(createElementVNode)
    ],
}
2.2.1 transformElement

转换元素节点

主要作用:

  • 转换元素节点
  • 创建虚拟节点调用
  • 为代码生成做准备
function transformElement(node, context) {
    if (node.type === 4) {
        return () => { //返回一个退出函数,在traverseNode中重新调用
            const vnodeTag = `'${node.tag}'`; //元素标签名
            const vnodeProps = null; //元素属性
            let vnodeChildren = null; // 元素子节点
            if (node.children.length > 0) { 
                if (node.children.length === 1) { //如果元素子节点只有一个则直接设置为vnodeChildren
                    const child = node.children[0]; 
                    vnodeChildren = child;
                }
            }
            node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren);
        };
    }
}
2.2.2 createVNodeCall

调用一次ast上下文的CREATE_ELEMENT_VNODE,同时返回一个虚拟节点。

function createVNodeCall(context, tag, props, children) {
    if (context) {
        context.helper(CREATE_ELEMENT_VNODE);
    }
    return {
        type: 4,
        tag,
        props,
        children,
    };
}
flowchart LR
    id1["{ type: 4, tag: 'p', tagType: 0,  children}"]
    id2["{ type: 4, tag: 'p', props: null, children}"]
    id1-- transformElement --> id2
2.2.3 transformText

转换文本节点

作用:

  • 合并连续的文本节点
  • 减少虚拟节点的数量
  • 提高渲染效率

判断是否为文本节点

function isText(node) {
    return node.type === 2 || node.type === 0;
}
function transformText(node, context) {
    if (node.type === 4) {
        return () => { // 返回退出函数
            const children = node.children; // 获取标签节点子元素
            let currentContainer;
            for (let i = 0; i < children.length; i++) {
                const child = children[i];
                if (isText(child)) {    //判断子元素是否为文本节点
                    for (let j = i + 1; j < children.length; j++) {
                        const next = children[j];
                        if (isText(next)) {
                            if (!currentContainer) {
                                currentContainer = children[i] = {
                                    type: 5,
                                    loc: child.loc,
                                    children: [child],
                                };
                            }
                            currentContainer.children.push(` + `, next);
                            children.splice(j, 1);
                            j--;
                        }
                        else {
                            currentContainer = undefined;
                            break;
                        }
                    }
                }
            }
        };
    }
}

<p>111{{msg}}</p>为例来讲解vue内部如何合并连续的文本节点,

在上一节讲到,在通过parseTag识别出开始标签后,标签内部的111{{msg}}会重新进入parsechilren进行循环

通过parsechildren,前面的111会进入praseText流程返回{type:0,content:'111'},

{{msg}}会进入parseInterpolation流程返回{type:2,content:'msg'},此时返回的AST语法树的children为两个元素。

node = {
  type: 4,
  children: [
    { type: 0, content: '111' }, // 文本节点
    { type: 2, content: 'msg' }, // 文本节点
  ]
}


//转换后
node = {
    type:4,
    children: [
        { type:5,
          children:[
          { type: 0, content: '111'},
          "+",
          { type: 2, content: 'msg'}     
        ]}
    ]
}

进入transformText

  • child为{type:0,content:'111'},next为{type:2,content:'msg'}
  • 且二者均满足type == 2 || type == 0,isText为true
  • currentContainer为空被赋值为{type:5,children:[{type:0,content:'111'} ,'+',{type:2,content:'msg'}]}
  • j-- 结束循环
2.2.4 transformExpression

转换表达式

当node.type为2,即存在插值语法时,将content转为_ctx.content,实现了在模板中访问组件的数据和方法。

function transformExpression(node) {
    if (node.type === 2) {
        node.content = processExpression(node.content);
    }
}
function processExpression(node) {
    node.content = `_ctx.${node.content}`;
    return node;
}

2.3 createRootCodegen

为root.codegenNode赋值

const node = {
    codegenNode:child[0].codegenNode
}
function createRootCodegen(root, context) {
    const { children } = root;
    const child = children[0];
    if (child.type === 4 && child.codegenNode) {
        const codegenNode = child.codegenNode;
        root.codegenNode = codegenNode;
    }
    else {
        root.codegenNode = child;
    }
}

3. generate

function generate(ast, options = {}) {
    const context = createCodegenContext(ast, options);
    const { push, mode } = context;
    if (mode === "module") {
        genModulePreamble(ast, context);
    }
    else {
        genFunctionPreamble(ast, context);
    }
    const functionName = "render";
    const args = ["_ctx"];
    const signature = args.join(", ");
    push(`function ${functionName}(${signature}) {`);
    push("return ");
    genNode(ast.codegenNode, context);
    push("}");
    return {
        code: context.code,
    };
}
genFunctionPreamble()
//生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue 
return 

push(`function ${functionName}(${signature}) {`);
push("return ");
//生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue 
return function render(_ctx) { return  

getNode(ast.codegenNode, context)
//最终生成

const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return function render(_ctx) {return _createElementVNode('p', null, _toDisplayString(_ctx.msg))}

3.1 createCodegenContext

创建代码生成上下文

相当于赋值了几个默认值和几个生成方法:

mode:'function',runtimeModuleName:'vue',runtimeGlobalName: 'Vue'

helper,push,newline

const helperNameMap = {
    [TO_DISPLAY_STRING]: "toDisplayString",
    [CREATE_ELEMENT_VNODE]: "createElementVNode"
};
function createCodegenContext(ast, { runtimeModuleName = "vue", runtimeGlobalName = "Vue", mode = "function" }) {
    const context = {
        code: "",
        mode,
        runtimeModuleName,
        runtimeGlobalName,
        helper(key) {
            return `_${helperNameMap[key]}`;
        },
        push(code) {
            context.code += code;
        },
        newline() {
            context.code += "\n";
        },
    };
    return context;
}

3.2 genFunctionPreamble

mode 默认值为function

这个函数是用来生成函数前言的代码的。函数前言是指函数体之前的代码,通常包括函数的参数声明、局部变量声明等

function genFunctionPreamble(ast, context) {
    const { runtimeGlobalName, push, newline } = context;
    const VueBinging = runtimeGlobalName; //'Vue'
    const aliasHelper = (s) => {
        return `${helperNameMap[s]} : _${helperNameMap[s]}`
    }
    if (ast.helpers.length > 0) {
        push(` 
        const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging} 

      `);
    }
    newline();
    push(`return `);
}
//生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue 
return 

3.3 getNode

循环根据节点类型生成代码

function genNode(node, context) {
    switch (node.type) {
        case 2:
            genInterpolation(node, context);
            break;
        case 3:
            genExpression(node, context);
            break;
        case 4:
            genElement(node, context);
            break;
        case 5:
            genCompoundExpression(node, context);
            break;
        case 0:
            genText(node, context);
            break;
    }
}  
3.3.1 genInterpolation
function genInterpolation(node, context) {
    const { push, helper } = context;
    push(`${helper(TO_DISPLAY_STRING)}(`);
    genNode(node.content, context);
    push(")");
}  
3.3.2 genExpression
function genExpression(node, context) {
    context.push(node.content, node);
}
3.3.3 genElement
function genElement(node, context) {
    const { push, helper } = context;
    const { tag, props, children } = node;
    push(`${helper(CREATE_ELEMENT_VNODE)}(`);
    genNodeList(genNullableArgs([tag, props, children]), context);
    push(`)`);
}  
3.3.4 genCompoundExpression
function genCompoundExpression(node, context) {
    const { push } = context;
    for (let i = 0; i < node.children.length; i++) {
        const child = node.children[i];
        if (isString(child)) {
            push(child);
        }
        else {
            genNode(child, context);
        }
    } 
}  
3.3.5 genText
function genText(node, context) {
    const { push } = context;
    push(`'${node.content}'`);
}

4. compile

function compileToFunction(template, options = {}) {
    const { code } = baseCompile(template, options);
    console.log("🚀 ~ compileToFunction ~ code:", code)
    const render = new Function("Vue", code)(runtimeDom);
    console.log("🚀 ~ compileToFunction ~ render:", render,runtimeDom)
    return render;
}  

此时经过AST转换后的code

const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue 

return function render(_ctx) {return _createElementVNode('p', null, _toDisplayString(_ctx.msg))}

通过new Function生成render函数,同时传入运行时环境runtimeDom

var runtimeDom = /*#__PURE__*/Object.freeze({
    __proto__: null,
    createApp: createApp,
    getCurrentInstance: getCurrentInstance,
    registerRuntimeCompiler: registerRuntimeCompiler,
    inject: inject,
    provide: provide,
    renderSlot: renderSlot,
    createTextVNode: createTextVNode,
    createElementVNode: createVNode,
    createRenderer: createRenderer,
    toDisplayString: toDisplayString,
    watchEffect: watchEffect,
    reactive: reactive,
    ref: ref,
    readonly: readonly,
    unRef: unRef,
    proxyRefs: proxyRefs,
    isReadonly: isReadonly,
    isReactive: isReactive,
    isProxy: isProxy,
    isRef: isRef,
    shallowReadonly: shallowReadonly,
    effect: effect,
    stop: stop,
    computed: computed,
    h: h,
    createAppAPI: createAppAPI
});

new Function("Vue", code) 的意思是:

  • 创建一个新的函数对象,它接受一个名为 Vue 的参数。
  • 使用 code 变量的值作为函数体的代码。

实际生成的render函数

function(Vue) {
  const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue 
  return function render(_ctx) {return _createElementVNode('p', null, _toDisplayString(_ctx.msg))}
}