Compilation module 6: codegen implementation

Source location:
vue-next/packages/compile-core/src/codegen.ts

The transformation is implemented above, and now we will enter the final code generation codegen of the whole compile. Personally, compared with the previous transformation, codegen is really very simple, and the source code is located here

Say at the beginning

My implementation is quite different from the source implementation, because various helper s, cache s, hoist and so on are considered in the source code, which I have not implemented. After transform ation, the type attribute mounted on the codegenNode node on the AST is the code structure corresponding to this node, and the source code is generated according to this. For details, see the source code. This part is relatively clear

Next, it is necessary to make complaints about the beginning. The transform module in front is basically written in accordance with the structure of the source code. The codegenNode structure is basically the same as the source code. But because of this, the codegen link has to be handled very, very, very hard. I hope you will excuse me and understand what it means.

Analyze it

Since there is no need to consider various complex structures, I will simply divide them into elements, attributes, text and combined expressions for code generation
The function that generates nodes naturally thinks of the h function exposed in the runtime module. createVNode is used in the source code, but there is little difference between the two. VNode can be created. The following is the parameter received by the h function

function h(type, props, children) {
  // TODO
}

Write it down

createCodegenContext

In fact, I don't need too much context here, but I'd like to write it a little, which is very simple, as follows

function createCodegenContext() {
  const context = {
    // state
    code: '', // Object code
    indentLevel: 0, // Indent level

    // method
    push(code) {
      context.code += code;
    },
    indent() {
      newline(++context.indentLevel);
    },
    deindent(witoutNewLine = false) {
      if (witoutNewLine) {
        --context.indentLevel;
      } else {
        newline(--context.indentLevel);
      }
    },
    newline() {
      newline(context.indentLevel);
    },
  };
  function newline(n) {
    context.push('\n' + '  '.repeat(n));
  }
  return context;
}

generate

The generate function is the main entry of codegen. In this function, we need to obtain the context, and then generate the preliminary structure of the code. The content is recursively generated by genNode. Of course, we also have to return the generated code

function generate(ast) {
  const context = createCodegenContext();
  const { push, indent, deindent } = context;

  indent();
  push('with (ctx) {');
  indent();

  push('return ');
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context);
  } else {
    push('null');
  }

  deindent();
  push('}');

  return {
    ast,
    code: context.code,
  };
}

genNode

In genNode, simply use switch case to control a process and call different methods

function genNode(node, context) {
  // If it is a string, push it directly
  if (typeof node === 'string') {
    context.push(node);
    return;
  }

  switch (node.type) {
    case NodeTypes.ELEMENT:
      genElement(node, context);
      break;
    case NodeTypes.TEXT:
    case NodeTypes.INTERPOLATION:
      genTextData(node, context);
      break;
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context);
      break;
  }
}

genElement

At the beginning, the h function is used to create VNode, that is, we need to parse tag, props and children as parameters. Here, the logic of generating attributes and child nodes is separated. The genElement is as follows

function genElement(node, context) {
  const { push, deindent } = context;
  const { tag, children, props } = node;

  // tag
  push(`h(${tag}, `);

  // props
  if (props) {
    genProps(props.arguments[0].properties, context);
  } else {
    push('null, ');
  }

  // children
  if (children) {
    genChildren(children, context);
  } else {
    push('null');
  }

  deindent();
  push(')');
}

genProps

GenProps is to get the attribute data in the node and splice it into an object's appearance. push enters the target code. Here's a look at what props.arguments[0].properties is called by genProps in the genElement above.

// <p class="a" @click="fn">hello {{ World }}</p>
[
    {
        "type": "JS_PROPERTY",
        "key": {
            "type": "SIMPLE_EXPRESSION",
            "content": "class",
            "isStatic": true
        },
        "value": {
            "type": "SIMPLE_EXPRESSION",
            "content": {
                "type": "TEXT",
                "content": "a"
            },
            "isStatic": true
        }
    },
    {
        "type": "JS_PROPERTY",
        "key": {
            "type": "SIMPLE_EXPRESSION",
            "content": "onClick",
            "isStatic": true,
            "isHandlerKey": true
        },
        "value": {
            "type": "SIMPLE_EXPRESSION",
            "content": "fn",
            "isStatic": false
        }
    }
]

Then we only need to operate according to this structure, as follows

function genProps(props, context) {
  const { push } = context;

  if (!props.length) {
    push('{}');
    return;
  }

  push('{ ');
  for (let i = 0; i < props.length; i++) {
    // Traverse each prop object to get the key node and value node
    const prop = props[i];
    const key = prop ? prop.key : '';
    const value = prop ? prop.value : prop;

    if (key) {
      // key
      genPropKey(key, context);
      // value
      genPropValue(value, context);
    } else {
      // If the key does not exist, it means it is a v-bind
      const { content, isStatic } = value;
      const contentStr = JSON.stringify(content);
      push(`${contentStr}: ${isStatic ? contentStr : content}`);
    }

    if (i < props.length - 1) {
      push(', ');
    }
  }
  push(' }, ');
}

// Generate key
function genPropKey(node, context) {
  const { push } = context;
  const { isStatic, content } = node;
  push(isStatic ? JSON.stringify(content) : content);
  push(': ');
}

// Generated value
function genPropValue(node, context) {
  const { push } = context;
  const { isStatic, content } = node;
  push(isStatic ? JSON.stringify(content.content) : content);
}

Here is the time to make complaints about yourself. This code is really ugly.

genChildren

The child node is an array. You only need to refer to the structure of genProps above. However, because my transformText is lazy and does not generate codegenNode, it has to be processed separately. In addition, the combined expression complex_ Expression is also processed separately, and other normal recursive gennodes can be used

function genChildren(children, context) {
  const { push, indent } = context;

  push('[');
  indent();

  // Separate processing of COMPOUND_EXPRESSION
  if (children.type === NodeTypes.COMPOUND_EXPRESSION) {
    genCompoundExpression(children, context);
  } 
  
  // Process TEXT separately
  else if (isObject(children) && children.type === NodeTypes.TEXT) {
    genNode(children, context);
  } 
  
  // Other nodes are recursive directly
  else {
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      genNode(child.codegenNode || child.children, context);
      push(', ');
    }
  }

  push(']');
}

genTextData

Interpolation expressions and text nodes are handled by this function, because the only difference between them in the result of code generation is whether the child node is a string

function genTextData(node, context) {
  const { push } = context;
  const { type, content } = node;

  // If it is a text node, take out the content directly
  // If it is an interpolation expression, you need to take out content.content
  const textContent =
    type === NodeTypes.TEXT
      ? JSON.stringify(content)
      : NodeTypes.INTERPOLATION
      ? content.content
      : '';

  // As another example, the default text node has no attributes
  push('h(Text, ');
  push('null, ');
  push(`${textContent})`);
}

genCompoundExpression

In fact, a combined expression is essentially a node. Several child nodes may be text nodes or interpolation expression nodes, which can be recursive directly

function genCompoundExpression(node, context) {
  const { push } = context;
  for (let i = 0; i < node.children.length; i++) {
    const child = node.children[i];
    if (typeof child === 'string') {
      push(child);
    } else {
      genNode(child, context);
    }

    if (i !== node.children.length - 1) {
      push(', ');
    }
  }
}

Q&A

Q: What is the relationship between the H function and createVNode?
A: In fact, the underlying call of the H function is createVNode, which belongs to the parent-child relationship, and some fault-tolerant processing is carried out in the H function. For example, you can use the H function to directly pass in children without passing props, and this call createVNode will report an error, but h has carried out fault-tolerant processing, so there is no problem

Q: What is the main difference between your implementation here and the implementation of the source code?
A: There are differences everywhere. The implementation in the source code is completely guided by the type attribute of codegenNode to generate the corresponding structure, and the content of the node is not the main focus. In other words, my implementation here is based on the function, while the source code is based on the structure, which makes an obvious difference. There is no genChildren genProps, genPropKey, genObjectExpression, genArrayExpression, gennodelissarray and so on are used in the source code. In this way, taking the structure as the starting point and extracting the function, the function can be reused to a great extent, and the operation is more flexible. This is really stupid code I wrote

Q: What's the use of this you wrote?
A: Passed the test I wrote myself

summary

To be honest, I'm sorry to summarize, because the implementation here is really bad, but I still need to defend myself. In order to fit the transform ation implementation of the source code as much as possible, I generated codegenNode. If I don't use it, I think it's a little meaningless. If I use it, I can only use such a rigid implementation. Let's look at Figure 1 and have fun, so as to warn you not to be lazy

Tags: Javascript Front-end Vue.js

Posted on Tue, 23 Nov 2021 03:11:12 -0500 by DKY