元素转换+代码生成

364 阅读4分钟

元素转换+代码生成

元素转换补充内容

// 创建节点的标识
const CREATE_VNODE = Symbol('CREATE_VNODE');

// 创建一个虚拟节点给codegenNode用
function createVnodeCall(
    ctx,
    vnodeTag,
    vnodeProps,
    vnodeChildren,
    vnodePatchFlg,
){
    // 标识一下创建vnode的节点
    ctx.setHelp(CREATE_VNODE); // 用什么动作去创建
    
    return {
        type: NodeTypes.VNODE_CALL, // 节点本身的类型
        tag: vnodeTag,
        props: vnodeProps,
        children: vnodeChildren,
        patchFlg: vnodePatchFlg,
    }
    
}


function transformElement(node, ctx){
    // 转换器1 处理元素的
    // 需要筛选一下这个是处理元素
    // 想在整颗树处理完之后再去处理这个。
    if(node.type != NodeTypes.ELEMENT){
        // 代表元素节点 此节点是元素 1
        // 如果不是1就不处理了
        return;

        
    }
    
    
    // coding
    return () => { // 这种函数叫做退出函数,洋葱模型 最里面的限制性 逻辑是 parent 返回fn child 返回fn sunzi 返回fn 执行的时候从内向外。执行 【element】【test】 执行 【test】【element】
        console.log('处理元素的');
        // createVnode('h1', {}, 'hello'); // ctx 向helper里面放入这个
        
        // tag 标签名字
        // children 孩子
        const {tag, children} = node;
        
        const vnodeTag = `'${tag}'`;
        const vnodeProps = undefined;
        let vnodeChildren = children; // 这个需要处理
        let patchFlg = 0; // 用于标记这个标签是动态的 <div>{{name}}</div> 因为里面有动态内容所以标签也是动态的需要patchFlg的优化
        let vnodePatchFlg = 0;
        
        // 儿子的处理
        if(vnodeChildren.length > 0) {
            if(vnodeChildren.length === 1){
                const child = vnodeChildren[0]; // 取到了唯一的孩子。
                // 判断是不是动态节点如果是动态节点需要给当前的父标签加一个动态类型。
                const type = child.type; // 看一下它是不是动态的节点
                const hasDynNode = false;
                if(type == NodeTypes.COMPOUND_EXPERSION || type == NodeTypes.INTERPOLATION) {
                    // 含有动态节点。
                    hasDynNode = true;
                }
                
                if(hasDynNode) {
                    patchFlg |= PatchFlgs.TEXT; 
                }
                vnodeChildren = child // 就一个孩子直接赋值过来行了
                
            } else{
                // 多个孩子的情况
                vnodeChildren = children; // 多个孩子不用处理。
            }
        }
        
        if(patchFlg !== 0) {
            // 说明有动态节点。
            vnodePatchFlg = `'${patchFlg}'`
        }
        
         // 给当前节点加一个codegen 如何动态生成代码。
        node.codegenNode = createVnodeCall(
            ctx,
            vnodeTag,
            vnodeProps,
            vnodeChildren,
            vnodePatchFlg,
        )
        
   }


}


// 修改 traversNode 方法的switch

function traversNode(node, ctx){ 
    // 遍历 node 这个玩意需要调用 ctx这里面的CL进行转换
    const {CL} = ctx;

    // 定位一下当前node
    ctx.currentNode = node; // 代码进入的时候指向

    // 把所有的nodes里面的节点都传入到CL这个集合的方法中
    const existsAy = []; // 执行的时候从后往前能
    CL.forEach(cb => { // CL 的顺序一定要放对了影响回调顺序
        // 洋葱模型这里可以拿到返回的函数。收集起来从后向前执行
        const onExit = cb(node, ctx); // 遍历的所有节点都过一遍转换器
        onExit && existsAy.push(onExit);
    });

    const TO_DISPLAY_STRING = symbol("TO_DISPLAY_STRING");

    // 如果有儿子就遍历起来
    switch(node.type) {
        case NodeTypes.ROOT:
        case NodeTypes.ELEMENT:
           // 跟节点和元素节点可能有儿子需要拿出来遍历儿子在调用这个方法
           traversChildren(node, ctx);break;
        case NodeTypes.INTERPOLATION: 
            ctx.setHelp(TO_DISPLAY_STRING) // 加了一种处理标识
    }


    // 执行回调从内到外依此执行。
    ctx.currentNode = node; // 上面的递归不停的在改当我执行这个的时候在还原回去 函数的栈空间保存当前的那个node,代码出来的时候还原洋葱。
    for(let i = existsAy.length-1; i >= 0; i--){
        existsAy[i]();
        // 注意currentNode的指向问题
        // 一层层的进来出来的时候current指向的是最层那个内容。
        // 为了保证退出的方法对应的current是正确的
    }
}


const OPEN_BLOCK = symbol('OPEN_BLOCK');
const FRAGMENT = symbol('FRAGMENT')
const CREATE_BLOCK = symbol('CREATE_BLOCK');

// 转换的时候需要根元素处理下

function createRootCodeGen(ast, ctx){
    const {setHelp} = ctx;
    const children = ast.children;
    setHelp(OPEN_BLOCK);
    setHelp(CREATE_BLOCK);
    if(children.length == 1) {
        const child = chiildren[0];

        // 用child作为根
        const codegen = child.codegenNode;
        codegen.isBlock = true; // 用这个作为根节点。 只有一个儿子这就是block tree
        
        ast.codegenNode = codegen; // 一个孩子直接把这个孩子的codegen挂在到最外层当block tree来使用。当根来使用。
    } else if(children.length > 1) {
        // 生成一个fragment来包裹。
        
        ast.codegenNode = createVnodeCall(
            ctx,
            setHelp(FRAGMENT),
            undefined,
            children,
            PatchFLgs.STABLE_FRAGMENT
        );
        ast.codegenNode.isBlock = true;
    }
}

export function transform(ast = {}, {CL}) { // -1
    // 创建AST的时候有个解析上下文 createParserContext
    // 转换的时候可以创建一个转换上下文
    const ctx = createTransformContext(ast, CL); 

    // 有了转换上下文开始遍历。
    // 遍历根据是否有孩子有就继续递归

    // 遍历节点
    // 没有上下文一堆参数需要在这里传递。
    traversNode(ast, ctx);


    // 根节点的处理在最外边包裹一层。
    // 如果有多个跟节点需要用Fragment套一层
    // <div></div>
    // <div></div>
    createRootCodeGen(ast, ctx);

    ast.helpers = [...ctx.helpers] // 把ctx的属性挂到 节点上否则generate拿不到。
}

开始生成代码

// symbol的映射表
const helperNameMap = {
    [FRAGMENT]: 'Fragment' // FRAGMENT 对应的symbol值拿的symbol做的key NodeTypes的那一批
    .....
}

function createGenerateCtx(ast){
    // 换行每次缩进的时候都需要换行。
    const newLine = (n) => {
        ctx.push('\r\n' + '  '.repeat(n)); // 换行+空格2个
    }

    // 要包含字符串节点。
    const ctx = {
        code: ``, // 拼的结果。
        indentLevel: 0, // 代码的缩紧要不太丑了。 0就是没有锁进
        setindentLevel(){
            newLine(++ctx.indentLevel) // 没调用一次缩进就等级+1

        },
        helper(key){
             
            return `${helperNameMap[key]}`;
        },
        push(c){ // 拼接代码 
            ctx.code += c
        },
        deindent(){
            newLine(--ctx.indentLevel)
        },
        newline(){ // 换行不缩进
            newLine(ctx.indentLevel); // 默认多少就多少。
        }
    };

    return ctx;
}


// genVNodeCall 针对VNODE_CALL的类型生成代码
function genVNodeCall(codegen, ctx){
    const {push, newline,setindentLevel,deindent,helper} = ctx;
    const {tag,children,props,patchFlgs,isBlock} = codegen;

    if(isBlock) {
        push(`(${helper(OPEN_BLOCK)}(),`) // 这些内容参照ast vue-next-template-explorer 直接拼接就完事儿了
        // 后面不同的逻辑递归处理行了。
    }
}

// 递归生成代码
function genNode(codegen, ctx){
    switch(node.type){
        case NodeTypes.ELEMENT:;break;
        case NodeTypes.TEXT:;break;
        case NodeTypes.INTERPOLATION:;break;
        case NodeTypes.SIMPLE_EXPERSION:;break;
        case NodeTypes.COMPOUND_EXPERSION:;break;
        
        case NodeTypes.TEXT_CALL:;break;
        case NodeTypes.VNODE_CALL:
            genVNodeCall(codegen, ctx);
        ;break;
        case NodeTypes.JS_CALL_EXPERSION:;break;
        
    }
}


// 生成代码字符串
function generate(ast){
    // genNode除了这个都是些壳子可以直接去ast的网站搞
    // 在生成的过程需要创建一个字符串最后拼接后的结果。
    // 在做所有的操作之前都需要个上下文这个也不例外。
    const ctx = createGenerateCtx(ast);

    const {push, newline,setindentLevel,deindent} = ctx;

    // 开始拼接
    push(`const _Vue = vue`); 
    newline();
    push(`return function render(_ctx){`);
    setindentLevel();
    push(`with(_ctx){`)
    setindentLevel();

    push(
        `const {${ast.helpers.map(s=> `${helperNameMap[s]}`).join(',')}} = _Vue`
    )
    newline();
    push(`稍后继续`); // 这是核心。需要根据转换后的结果生成字符串
    genNode(ast.codegenNode,ctx); // 因为需要递归的生成。[核心]

    deindent();
    push(`}`)
    deindent();
    push(`}`)
    return ctx.code;
}


// 最后这一步就是根据codegenNode拼接字符串而已。
export function buildCompiler(template) { // -1
    // 1. 生成AST树
    const ast = baseParse(template);

    // 转化策略会很多
    const CL = getBaseTransformPreset(); // 返回了一系列的节点转化方式。每遍历一个AST节点都需要调用这里面的方法。

    // 2. 转换AST
    transform(ast, {CL});


    // 3. 生成代码
    return generate(ast)
    
}

最终对代码new Function 实现。