vue3模板核心编译(简易)思路

380 阅读6分钟

前言

  • vue3核心compiler源码地址
  • Vue 3 Template Explorer
  • 本文只是描述一下思路, 源码做的太多, 因为vue3引入Block的概念(Block Tree)
  • block的作用就是为了收集动态节点 将树的递归 拍平成了有一个数组dynamicChildren
  • createVnode的时候 会判断这个节点是动态的 就让外层的block收集起来
  • 所以 transform这一步的 PatchFlags 对不同的动态节点进行描述
  • 为了diff的时候只diff动态的节点, 减少比对, 同时不像vue2遇到孩子 就要递归操作, 虽然vue2做了静态标记
  • 只是比对是静态 就跳过, vue3 在这一块做了很大的性能提升
  • vue3 可以直接写render函数 h('div') 但是并不好 如果dom元素太多 不好维护
  • 也可以jsx语法 会转成h('div') 没有优化
  • 用vue3的template 转化为的render函数 具备优化特点的 patchFlag blockTree
  • template -> render函数
      1. html解析成抽象语法树ast(描述语法本身)
      1. 转化ast语法树 加标记和进行优化 transform
      1. 生成render函数(字符串拼接)

示例

<script src="../node_modules/@vue/compiler-dom/dist/compiler-dom.global.js"></script>
<script>
  const { baseCompile } = VueCompilerDOM

  const template = `
    <div  >tom {{  age  }}</div>
  
    <span>123456</span>
  `
  let res = baseCompile(template)
  console.log("🚀 ~ res", res)
</script>

最终要的字符串

// 这是要拼成字符串的样式, 最后通过 new Function 包裹起来
const _Vue = Vue

return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock(_Fragment, null, [
      _createVNode("div", null, "tom " + _toDisplayString(age), 1 /* TEXT */),
      _createVNode("span", null, "123456")
    ], 64 /* STABLE_FRAGMENT */))
  }
}

compile.ts


import { generate } from "./codegen"
import { baseParse } from "./parse"
import { getBaseTransformPreset, transform } from "./transform"


/**
 * @description 模板编译的过程
 * @description 生成的最后 用new Function
 */
export function baseCompile(template) {
    // template - ast语法树
    const ast = baseParse(template)
    // ast语法树 进行优化 静态提升 
    const nodeTransforms = getBaseTransformPreset()
    transform(ast, nodeTransforms)

    // 字符串拼接
    return generate(ast)
}

// 动态节点类型
export const enum PatchFlags {
    /**
     * Indicates an element with dynamic textContent (children fast path)
     */
    TEXT = 1,
  
    /**
     * Indicates an element with dynamic class binding.
     */
    CLASS = 1 << 1,
  
    /**
     * Indicates an element with dynamic style
     * The compiler pre-compiles static string styles into static objects
     * + detects and hoists inline static objects
     * e.g. style="color: red" and :style="{ color: 'red' }" both get hoisted as
     *   const style = { color: 'red' }
     *   render() { return e('div', { style }) }
     */
    STYLE = 1 << 2,
  
    /**
     * Indicates an element that has non-class/style dynamic props.
     * Can also be on a component that has any dynamic props (includes
     * class/style). when this flag is present, the vnode also has a dynamicProps
     * array that contains the keys of the props that may change so the runtime
     * can diff them faster (without having to worry about removed props)
     */
    PROPS = 1 << 3,
  
    /**
     * Indicates an element with props with dynamic keys. When keys change, a full
     * diff is always needed to remove the old key. This flag is mutually
     * exclusive with CLASS, STYLE and PROPS.
     */
    FULL_PROPS = 1 << 4,
  
    /**
     * Indicates an element with event listeners (which need to be attached
     * during hydration)
     */
    HYDRATE_EVENTS = 1 << 5,
  
    /**
     * Indicates a fragment whose children order doesn't change.
     */
    STABLE_FRAGMENT = 1 << 6,
  
    /**
     * Indicates a fragment with keyed or partially keyed children
     */
    KEYED_FRAGMENT = 1 << 7,
  
    /**
     * Indicates a fragment with unkeyed children.
     */
    UNKEYED_FRAGMENT = 1 << 8,
  
    /**
     * Indicates an element that only needs non-props patching, e.g. ref or
     * directives (onVnodeXXX hooks). since every patched vnode checks for refs
     * and onVnodeXXX hooks, it simply marks the vnode so that a parent block
     * will track it.
     */
    NEED_PATCH = 1 << 9,
  
    /**
     * Indicates a component with dynamic slots (e.g. slot that references a v-for
     * iterated value, or dynamic slot names).
     * Components with this flag are always force updated.
     */
    DYNAMIC_SLOTS = 1 << 10,
  
    /**
     * Indicates a fragment that was created only because the user has placed
     * comments at the root level of a template. This is a dev-only flag since
     * comments are stripped in production.
     */
    DEV_ROOT_FRAGMENT = 1 << 11,
  
    /**
     * SPECIAL FLAGS -------------------------------------------------------------
     * Special flags are negative integers. They are never matched against using
     * bitwise operators (bitwise matching should only happen in branches where
     * patchFlag > 0), and are mutually exclusive. When checking for a special
     * flag, simply check patchFlag === FLAG.
     */
  
    /**
     * Indicates a hoisted static vnode. This is a hint for hydration to skip
     * the entire sub tree since static content never needs to be updated.
     */
    HOISTED = -1,
    /**
     * A special flag that indicates that the diffing algorithm should bail out
     * of optimized mode. For example, on block fragments created by renderSlot()
     * when encountering non-compiler generated slots (i.e. manually written
     * render functions, which should always be fully diffed)
     * OR manually cloneVNodes
     */
    BAIL = -2
}

1.html 转化 ast(parse.ts)

  • AST Explorer网址
  • 可以根据上面的网站, 将语言选择vue, 左面输入html, 右面显示解析的ast语法树
  • vue3源码的编译成ast语法树 处理的情况很多(vue3比vue2编译部分做了许多优化部分) 这里只是抽出了 核心部分
  • 从以下三方面 做了词法解析(以下面的示例为主)
    • 文本
    • 表达式{{内容}}
    • 元素
  • 最后通过递归的形式 将所有标签都组装起来
  • 核心方法是parseChildren, 通过循环方式, 解析一点词法 就将html字符串, 截掉一点(方法advanceBy)
  • 直到解析的html字符串为空, 跳出循环
  • vue3支持外层不用包裹一个根元素, 它会把你创建一个, 返回最终的结果
* 文本ast语法树
* 假如html: `hello`

{
  type: 2,
  content: 'hello',
  loc: {
    start:  { column: 1, line: 1, offset: 0 },
    end:    { column: 6, line: 1, offset: 5 },
    source: 'hello'
  }
}

================================================

* 表达式ast语法树
* 假如html: `{{aaa}}`

{
    type: 5,
    content: {
        type: 4,
        isStatic: false,
        content: 'aaa',
        loc: {
            start:  { column: 3, line: 1, offset: 2 },
            end:    { column: 6, line: 1, offset: 5 },
            source: 'aaa'
        }
    },
    loc: {
        start:  { column: 1, line: 1, offset: 0 },
        end:    { column: 8, line: 1, offset: 7 },
        source: '{{aaa}}'
    }
}

================================================

* 标签ast语法树
* 假如html: `<div></div>`

{
  type: 1,
  isSelfClosing: false,
  children: [],
  loc: {
    start:  { column: 1, line: 1, offset: 0 },
    end:    { column: 12, line: 1, offset: 11 },
    source: '<div></div>'
  }
}

================================================

* 根 自己创建的, 将所有标签都包裹起来

{
    type: 0,
    children: [ ... ],
    loc: {
        start: { ... },
        end: { ... },
        source: '...'
    }
}

图片替换文本
export const enum NodeTypes {
    ROOT,                           // fragment         0
    ElEMENT,                        // div,p...         1
    TEXT,                           // 'hello'          2
    SIMPLE_EXPRESSION = 4,          // aaa              4
    INTERPOLATION = 5,              // {{aaa}}          5
    COMPOUND_EXPRESSION = 8,        // {{aaa}}hello     8 
    TEXT_CALL = 12,                 // createTextVnode  12
    VNODE_CALL = 13,                // 
    JS_CALL_EXPRESSION = 17         //
}



/**
 * @description     创建根(vue3 支持多元素并列写, 不用外层包裹一个根元素)
 * @param children  孩子ast语法数组
 * @param loc       获取根元素模板信息
 */
function createRoot(children,loc){
    return {
        type: NodeTypes.ROOT,
        children,
        loc
    }
}

/**
 * @description   ast语法树
 * @param content 模板
 */
export function baseParse(content) {
    // 创建解析上下文 在整个解析过程中会修改对应信息
    const context = createParserContext(content)

    // 记录开始位置(行, 列, 偏移)
    const start = getCursor(context) 
    
    // 开始解析模板
    let children =  parseChildren(context)

    // 根的模板信息(开始, 结束, 内容)
    let rootLoc = getSelection(context, start)

    return createRoot(children, rootLoc)
}

/****************** 核心方法 *****************/
/**
 * @description   模板解析
 * @param context 模板
 */
function parseChildren(context) {
    const nodes = []

    while (!isEnd(context)) {
        // 当前上下文内容
        const s = context.source
        let node

        if (s[0] == '<') {
            // 标签
            node = parseElement(context)
        } else if (s.startsWith('{{')) {
            // 表达式
            node = parseInterpolation(context)
        } else {
            // 文本
            node = parseText(context)
        }

        nodes.push(node)    
    }

    // 如果是文本
    // 只要没有内容 就删除掉
    // 有内容 将多个空格变成一个空格
    nodes.forEach((node, index) => {
        if (node.type === NodeTypes.TEXT) {
            if (!/[^ \t\r\n]/.test(node.content)) {
                nodes[index] = null
            } else {
                node.content = node.content.replace(/[ \t\r\n]+/g,' ')
            }
        }
    })

    return nodes.filter(Boolean) // 过滤null值
}

/**
 * @description   标签解析
 * @param context 不断减少的模板信息
 */
function parseElement(context) {
    let ele:any = parseTag(context)

    // 处理儿子
    const children = parseChildren(context)

    // 解析关闭标签时 同时会移除关闭信息并且更新偏移量
    if(context.source.startsWith('</')){
        parseTag(context)
    }

    ele.children = children

    // 更新元素的位置信息
    ele.loc = getSelection(context, ele.loc.start)

    return ele
}

/**
 * @description   表达式解析
 * @param context 不断减少的模板信息
 */
function parseInterpolation(context) {
    // 开始位置信息
    const start = getCursor(context)

    // 关闭的索引 {{   aaa   }} -> 8
    const closeIndex = context.source.indexOf('}}', '{{')

    // 去掉'{{'
    advanceBy(context, 2)
    
    const innerStart = getCursor(context)
    // 解析结束的 位置信息 这个先定义 下面会改
    const innerEnd = getCursor(context)

    //  aaa   }} -> 6  包含空格的
    const rawContentLength = closeIndex - 2

    const preTrimContent = parseTextData(context, rawContentLength)

    // 去掉前后空格  "  aaa  " ->  aaa
    const content = preTrimContent.trim()
    // "  aaa  " ->  2
    const startOffset = preTrimContent.indexOf(content)

    // 有前面空格 更新模板信息
    if (startOffset > 0) {
        advancePositionWithMutation(innerStart, preTrimContent, startOffset)
    }
    
    // 更新模板信息
    const endOffset = content.length + startOffset
    advancePositionWithMutation(innerEnd,preTrimContent,endOffset)

    // '}}' 去掉 并且更新模板信息
    advanceBy(context, 2)

    return {
        type: NodeTypes.INTERPOLATION,
        content: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            isStatic: false,
            loc: getSelection(context, innerStart, innerEnd),
            content
        },
        loc: getSelection(context, start)
    }
}

/**
 * @description   文本解析
 * @param context 当前模板信息
 */
function parseText(context) {
    const endTokens = ['<', '{{']

    // 文本的整个长度
    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
        }
    }

    // 开始模板位置信息
    let start = getCursor(context)

    // 这一步
    // 1.context.source - 解析完的文本
    // 2.更新模板信息
    // 3.返回 截取解析的文本 如 hello<div... -> hello
    const content = parseTextData(context, endIndex)

    return {
        type: NodeTypes.TEXT,
        content,
        loc: getSelection(context, start)
    }

}

/****************** 辅助方法 *****************/
/**
 * @description   创建上下文(模板信息)
 * @param content 模板
 */
function createParserContext(content) {
    return {
        column: 1,                // 列数
        line: 1,                  // 行数
        offset: 0,                // 偏移字符数
        source: content,          // 解析的文本 会不断的减少
        originalSource: content,  // 原文本 不会变
    }
}

/**
 * @description   记录位置信息(行, 列, 偏移)
 * @param content 模板
 */
 function getCursor(context) {
    let { line, column, offset } = context
    return { line, column, offset }
}

/**
 * @description     获取模板信息对应 开始, 结束, 内容
 * @param context   模板信息
 * @param start     解析之前的模板信息 行, 列, 偏移
 * @param end       解析之后的模板信息 行, 列, 偏移
 */
function getSelection(context, start, end?) {
    end = end || getCursor(context)
    return {
       start,
       end,
       source: context.originalSource.slice(start.offset, end.offset)
    }
}

/**
 * @description   是不是解析完毕
 * @param context 当前不断减少的模板内容 context.source = ''
 */
function isEnd(context) {
    const source = context.source

    if(source.startsWith('</')){ return true }

    return !source
}


/**
 * @description     更新模板内容 并计算新的位置信息
 * @param context   当前不断减少的模板内容
 * @param endIndex  将模板内容要减去掉的位置
 */
function advanceBy(context, endIndex) {
    // 原模板内容
    let s = context.source

    // 计算出一个新的结束位置 根据内容和结束索引来 更新上下文的信息
    advancePositionWithMutation(context, s, endIndex)

    // 截取内容
    context.source = s.slice(endIndex)
}

/**
 * @description     更新模板信息
 * @param context   模板信息
 * @param s         原模板内容
 * @param endIndex  结束索引
 */
function advancePositionWithMutation(context, s, endIndex) {
    let linesCount = 0   // 行的数量
    let linePos = -1     // 换行后对应的索引

    for (let i = 0; i < endIndex; i++) {
        // 遇到换行(\n) 就++ , \n == 10
        if (s.charCodeAt(i) == 10) {
            linesCount++;
            linePos = i
        }
    }

    // 模板信息 偏移量
    context.offset += endIndex
    // 模板信息 结束时对应的行数
    context.line += linesCount
    // 模板信息 = -1(当前就是一行) ? 列数 = 已有列数 + 结束索引 : 列数 = 结束的索引 - 最后一个换行对应的位置
    context.column = linePos == -1 ? context.column + endIndex : endIndex - linePos
}

/**
 * @description     截取解析完文本内容 context.source去掉解析完的文本
 * @param context   当前模板信息
 * @param endIndex  结束位置
 */
function parseTextData(context, endIndex) {
    // 截取解析完文本内容
    const rawText = context.source.slice(0, endIndex)

    // 在context.source中把文本内容删除掉
    advanceBy(context, endIndex)

    return rawText
}

/**
 * @description     截取解析标签
 * @param context   当前模板信息
 */
function parseTag(context){
    // 假如:context.source = <div  ></div>
    // 标签开始的位置信息
    const start = getCursor(context)
    // 最基本的元字符 排除空格 换行... <div> | </div>
    const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source)

    // div
    const tag = match[1]

    // <div 去掉 并且更新模板信息
    advanceBy(context, match[0].length)
    // 去掉空格 换行 并且更新模板信息
    advanceSpaces(context)

    // 是不是自闭合标签
    const isSelfClosing = context.source.startsWith('/>')

    // 去掉 /> 或者 > ,并且更新模板信息
    advanceBy(context, isSelfClosing ? 2 : 1)

    return {
        type: NodeTypes.ElEMENT,
        tag,
        isSelfClosing,
        loc: getSelection(context, start)
    }
}

/**
 * @description   去除空格 换行 更新模板信息
 * @param context 模板信息
 */
function advanceSpaces(context){
    const match = /^[ \t\r\n]+/.exec(context.source)
    if (match) {
       advanceBy(context, match[0].length)
    }
}

2.ast优化transfrom(transform.ts)

  • 处理动态标记, 处理文本, 元素, 属性, 指令...
import { PatchFlags } from "@vue/shared/src"
import { NodeTypes } from "./parse"

export const CREATE_VNODE = Symbol('createVnode')
export const TO_DISPALY_STRING = Symbol('toDisplayString')
export const OPEN_BLOCK = Symbol('openBlock')
export const CREATE_BLOCK = Symbol('createBlock')
export const FRAGMENT = Symbol('Fragment')
export const CREATE_TEXT = Symbol('createTextVNode')

/**
 * @description 树的每一个节点进行转化
 */
export function getBaseTransformPreset() {
    return [
        // 元素转换方法 文本转换方法
        transformElement,
        transformText
    ]
}

/**
 * @description           转换
 * @param root            ast
 * @param nodeTransforms  转换方法
 */
export function transform(root, nodeTransforms) {
    const context = createTransformContext(root, nodeTransforms)
    traverseNode(root, context)
    createRootCodegen(root, context)

    root.helpers = [...context.helpers]

}


function createVnodeCall(context, tag, props, children, patchFlag) {
    context.helper(CREATE_VNODE);
    return {
        type: NodeTypes.VNODE_CALL,
        tag,
        props,
        children,
        patchFlag
    }
}

/**
 * @description 处理元素的
 */
function transformElement(node, context) {
    // 此节点是元素
    if (node.type != NodeTypes.ElEMENT) {  return }
    
    // 洋葱模型
    return () => { 
        // createVnode('h1',{},'helloworld',1)
        const { tag, children } = node
        let vnodeTag = `'${tag}'`
        let vnodeProps;     // props
        let vnodeChildren;  // 处理好的儿子
        let vnodePatchFlag;
        let patchFlag = 0;  // 用于标记这个标签是不是动态的
        if (children.length > 0) {
            if (children.length == 1) {
                const child = children[0]
                const type = child.type     // 看一下他是不是动态
                const hasDymanicTextChild = type === NodeTypes.INTERPOLATION || type === NodeTypes.COMPOUND_EXPRESSION;
                if (hasDymanicTextChild) {
                    patchFlag |= PatchFlags.TEXT
                }
                vnodeChildren = child           // 直接把一个儿子拿出来即可
            } else {    
                vnodeChildren = children        // 多个儿子 不用处理
            }
        }
        if (patchFlag !== 0) {
            vnodePatchFlag = patchFlag + ''
        }
        node.codegenNode = createVnodeCall(context, vnodeTag, vnodeProps, vnodeChildren, vnodePatchFlag);
    }
}

/**
 * @description 是不是文本
 */
function isText(node) {
    return node.type === NodeTypes.INTERPOLATION || node.type == NodeTypes.TEXT
}

function createCallExpression(callee, args) {
    return {
        type: NodeTypes.JS_CALL_EXPRESSION,
        callee,
        arguments: args
    }
}

/**
 * @description 处理文本
 */
function transformText(node, context) {
    // {{name}} hello  => createTextNode(name + ‘hello’) 
    if (node.type == NodeTypes.ROOT || node.type == NodeTypes.ElEMENT) {

        return () => {
            let hasText = false;
            let children = node.children;
            let container = null;
            for (let i = 0; i < children.length; i++) {
                const child = children[i];
                if (isText(child)) {    // "hello hello" + name (div) +world+ 'hello'
                    hasText = true;     // 当前元素确实有文本,我需要合并
                    for (let j = i + 1; j < children.length; j++) {
                        const next = children[j];
                        if (isText(next)) {
                            if (!container) {
                                container = children[i] = {
                                    type: NodeTypes.COMPOUND_EXPRESSION,
                                    loc: child.loc,
                                    children: [child]
                                }
                                container.children.push(`+`, next);
                                children.splice(j, 1);
                                j--;    // 防止塌陷
                            }
                        } else {
                            container = null;
                            break; // 跳过
                        }
                    }
                }
            }

            if (!hasText || children.length == 1) { // 只有一个孩子 可以直接innerHTML
                return;
            }
            for (let i = 0; i < children.length; i++) {
                const child = children[i];
                if (isText(child) || child.type == NodeTypes.COMPOUND_EXPRESSION) {
                    const callArgs = []; // 用于存放参数的
                    callArgs.push(child); // 文本内容
                    if (child.type !== NodeTypes.TEXT) {
                        callArgs.push(PatchFlags.TEXT + '');
                    }
                    children[i] = { // createTextNode('')
                        type: NodeTypes.TEXT_CALL,
                        content: child,
                        loc: child.loc,
                        codegenNode: createCallExpression( // 用于最后生成代码的
                            context.helper(CREATE_TEXT),
                            callArgs
                        )
                    }
                }
            }
        }
    }
}

export function createTransformContext(root, nodeTransforms) {
    const context = {
        root,
        currentNode: root,      // 当前节点 会随着树的遍历而更新
        nodeTransforms,         //  上下文的目的时为了传参方法
        helpers: new Set(),
        helper(name) {          // 代码中用到了具体方法 需要调用此方法 将对应的名字加入到helpers
            context.helpers.add(name);
            return name;
        }
    };
    return context
}
function traverseChildren(node, context) { // 深度优先
    for (let i = 0; i < node.children.length; i++) {
        const child = node.children[i];
        traverseNode(child, context);
    }
}
function traverseNode(node, context) {
    const { nodeTransforms } = context;
    context.currentNode = node;
    const exits = [];
    for (let i = 0; i < nodeTransforms.length; i++) {
        const onExit = nodeTransforms[i](node, context);
        if (onExit) exits.push(onExit)
    }
    switch (node.type) {
        case NodeTypes.ROOT:
        case NodeTypes.ElEMENT:
            traverseChildren(node, context);
        case NodeTypes.INTERPOLATION:  // name => {....}.toString
            context.helper(TO_DISPALY_STRING)
    }
    let i = exits.length;

    // 为了保证退出方法对应的context.currentNode是正确的
    context.currentNode = node;
    while (i--) {
        exits[i]();
    }
}

/**
 * @description 创建codegen
 */
function createRootCodegen(root, context) {
    const { helper } = context;
    const children = root.children;
    helper(OPEN_BLOCK)
    helper(CREATE_BLOCK)
    if (children.length == 1) {
        const child = children[0];          // 直接以当前这个孩子作为根节点
        const codegen = child.codegenNode;  // 获取刚才元素转化后的codegen
        codegen.isBlock = true;             // 只有一个儿子 那么他就是blocktree的根节点
       
        root.codegenNode = codegen          // 一个儿子直接把儿子的codegen挂载到最外层上
    } else if (children.length > 1) {
        root.codegenNode = createVnodeCall(
            context, helper(FRAGMENT),
            undefined,
            children,
            PatchFlags.STABLE_FRAGMENT)
        root.codegenNode.isBlock=  true;
    }
   
}


3.拼接codegen(codegen.ts)

  • 就是上面最终要的字符串
  • 这里处理不完整 只是个思路
import { NodeTypes } from "./parse"
import { CREATE_BLOCK, CREATE_TEXT, CREATE_VNODE, FRAGMENT, OPEN_BLOCK, TO_DISPALY_STRING } from "./transform"

/**
 * @description 生产render函数的要引入的方法
 */
export const helperNameMap: any = {
    [FRAGMENT]: `Fragment`,
    [OPEN_BLOCK]: `openBlock`,
    [CREATE_BLOCK]: `createBlock`,
    [CREATE_VNODE]: `createVNode`,
    [TO_DISPALY_STRING]: "toDisplayString",
    [CREATE_TEXT]: "createTextVNode"
}

/**
 * @description 拼接字符串方法
 */
function createCodegenContext(ast) {
    const newLine = (n) => {
        context.push('\n' + '  '.repeat(n))
    }
    const context = {
        code: ``, // 拼的结果
        push(c) { // 拼接代码
            context.code += c;
        },
        helper(key){
            return  `${helperNameMap[key]}`;
        },
        indentLevel: 0,                         // 缩进几次
        newLine() {
            newLine(context.indentLevel);       // 换行
        },
        indent() {
            newLine(++context.indentLevel);     // 缩进
        },
        deindent() {
            newLine(--context.indentLevel);
        }
    }
    return context
}

/**
 * @description 嵌套vnode
 */
function genVNodeCall(node,context){
    const {push,helper} = context
    const {tag,children,props,patchFlag,isBlock} = node
    if (isBlock) {
        push(`(${helper(OPEN_BLOCK)}(),`) 
        // 后面递归处理即可
    }
}

/**
 * @description 根据不同类型 拼接方法不一样
 */
function genNode(node, context) {
    switch (node.type) {    
        case NodeTypes.VNODE_CALL:
            genVNodeCall(node, context)         
            break;
        case NodeTypes.ElEMENT:
            break;
        case NodeTypes.TEXT:
            break;
        case NodeTypes.INTERPOLATION:
            break
        case NodeTypes.SIMPLE_EXPRESSION:
            break;
        case NodeTypes.COMPOUND_EXPRESSION:
            break;
        case NodeTypes.TEXT_CALL:
            break;
        case NodeTypes.JS_CALL_EXPRESSION:
            break;
    }
}

/**
 * @description 拼接
 */
export function generate(ast) {
    const context = createCodegenContext(ast);
    const { push, newLine, indent, deindent } = context;
    push(`const _Vue = Vue`);
    newLine();
    push(`return function render(_ctx){`);
    indent();
    push(`with (_ctx) {`)
    indent()
    push(`const {${ast.helpers.map(s => `${helperNameMap[s]}`).join(',')}} = _Vue`);
    newLine()
    push(`return `)
    genNode(ast.codegenNode, context);
    deindent();
    push(`}`)
    deindent();
    push(`}`);
    return context.code
}