Vue-next /packages/compile-core/ SRC /transform.ts

Parse already has an AST that can be directly used to generate object code. However, it is not necessary to generate an AST for the sake of generating an AST. In fact, we can easily modify the AST to meet other requirements. That’s what the transform here does

Speaking at the beginning

The whole process of transform is very complicated, and the most important thing is that the whole transform module has a high degree of cohesion and variables are closely related to each other, so it is difficult to completely simplify. Therefore, my implementation here is actually very rough, but it retains the most critical core logic. If you can understand my code, you should be able to barely understand the source code. Then, as the Transform module is relatively complex, I wrote a large number of annotations for step-by-step analysis, hoping that I could read them patiently

explain

If transform tries to generalize, it can be summarized as follows

  • Part of the structure can be optimized
  • A processing instruction
  • generatePatchFlag
  • generatecodegenNodenode

The third and fourth points are very, very important and will be explained in detail

patchFlag

In fact, patchFlag is similar to shapeFlag in the previous runtime in that it is an enumeration, but it is marked in a diff mode, that is, according to the type of diff required by the node. In the source code, patchFlag looks like this

// vue-next/packages/shared/src/patchFlags.ts r19
export const enum PatchFlags {
  TEXT = 1,
  CLASS = 1 << 1,
  STYLE = 1 << 2,
  PROPS = 1 << 3,
  FULL_PROPS = 1 << 4,
  HYDRATE_EVENTS = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  DEV_ROOT_FRAGMENT = 1 << 11,

  HOISTED = -1,
  BAIL = -2
}
Copy the code

When the subsequent component is updated, Diff will reduce the amount of calculation according to this patchFlag to achieve targeted update, which will be explained in detail later in this section

codegenNode

CodegenNode is used to generate codegen code. Yes, it is used to generate codeGen code. CodegenNode records the information after the Transform. Including structure conversion, type conversion, patchFlag, etc., and then CodeGen will generate the final object code according to codegenNode, and the specific conversion method will be discussed later. In short, codegenNode represents the structure and content of the node. To simplify generate code generation

Look at the

Directly according to the source code to a rough analysis, in fact, in the source code a lot of use of space for time cache optimization strategy, will need the data in advance of the cache, to use the time to directly use, although occupy extra space, but multiple references do not need to repeat calculation

transform

In line 125 of transform.ts, you see a createTransformContext function that creates the context for the entire Transform module, storing the required data in it. Parse, Transform, codeGen, and createxxxContext are all available to parse, transform, and CodeGen. So let’s take a quick look at some of the parameters that need to be explained

// vue-next/packages/compile-core/src/transform.ts r125
export function createTransformContext(
  root,
  {
    hoistStatic = false,
    cacheHandlers = false,
    nodeTransforms = [],
    directiveTransforms = {},
  }
) {
  const context = {
    // options
    hoistStatic, // Enable static variable promotion
    cacheHandlers, // Whether to enable event caching
    nodeTransforms, // Node conversion function sequence
    directiveTransforms, // Sequence of instruction conversion functions

    // state
    root, / / the root node
    helpers: new Map(), // Helper functions
    components: new Set(), // Component sequence
    directives: new Set(), // Sequence of instructions
    hoists: [].// Upgrade sequence
    parent: null./ / the parent node
    currentNode: root, // The current operating node

    // methods
    helper(name) {
      // TODO
    },
    helperString(name) {
      // TODO
    },
    hoist(exp) {
      // TODO
    },
    cache(exp, isVNode = false) {
      // TODO}}return context
}
Copy the code

helper

As the name suggests, helper is pretty much everywhere in the Transform process, so check out the official tools to see what helper does

In the image below, the tool displays the compiled results on the right, based on the template structure entered on the left, where a very simple template is entered

The compilation results are as follows

The _createElementVNode function indicated by the arrow in the figure is a helper and is introduced in advance to ensure that it works properly, as are openBlock and _createElementBlock above. These helper functions are actually defined and exposed elsewhere and can be used normally, but introduced by helpers, so helpers are actually functions that need to be introduced in advance to make sure the generated code works. Here are some of the helper functions listed below

// vue-next/packages/compile-core/src/runtimeHelpers.ts
export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ` `)
export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ` `)
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ` `)
export const CREATE_ELEMENT_BLOCK = Symbol(__DEV__ ? `createElementBlock` : ` `)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ` `)
export const CREATE_ELEMENT_VNODE = Symbol(__DEV__ ? `createElementVNode` : ` `)
/ /...

export const helperNameMap: any = {
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_ELEMENT_VNODE]: `createElementVNode`./ /...
}

// To inject helper functions into the current production environment
export function registerRuntimeHelpers(helpers: any) {
  Object.getOwnPropertySymbols(helpers).forEach(s= > {
    helperNameMap[s] = helpers[s]
  })
}
Copy the code

hoistStatic

This variable determines whether to enable static promotion. Static promotion, as the name suggests, is to promote some static data outside the function body in advance declaration, equivalent to caching, so that multiple references do not need to calculate each time, can be directly used by you, the specific effect is shown below

It’s still a simple template structure, just a paragraph of text, and the whole P node is actually a static node, because there’s no chance that the content will change, so everything can be improved

The upgrade is as follows

As you can see, _hoisted_1 creates a text node, and _HOisted_2 encapsulates it in an array to represent the child node, while _HOisted_2 is used directly. This is static promotion. If you need to use the P node later, you don’t need to recreate it. Just use _hoisted_1

/*#__PURE__*/ indicates that this function is a pure function with no side effects. It is used to boot tree-shaking, -1 /* HOISTED */ is the patchFlag of this node

nodeTransforms

This nodeTransforms is a sequence of nodeTransforms that stores the transformation functions to be executed. Instead of explicitly transforming the node, the transforms are stored in nodeTransforms and executed once and for all when the transforms are ready. So what do I mean by ripe

As mentioned earlier, one of the things transform does is generate a codegenNode, which of course also needs to store the basic information of the node, including the child nodes, which in turn have their own CodegenNodes, nested to form a complex tree structure. Then the most appropriate sequence to generate codeGenNodes is from leaf nodes to root nodes in reverse order, This ensures that children in a codegenNode have their own codegenNode, which is executed after the codegenNode of the child node is generated

// vue-next/packages/compile-core/src/transform.ts r408
export function traverseNode(node: RootNode | TemplateChildNode, context: TransformContext) {
  context.currentNode = node
  // apply transform plugins
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      exitFns.push(onExit)
    }
  }

  switch (node.type) {
    / /...
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      // traverseChildren recursively calls the traverseNode
      traverseChildren(node, context) 
      break
  }

  // exit transforms
  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}
Copy the code

Transforms the nodeTransforms for a loop that loops through the nodeTransforms and forces the result returned into the exitFns After the switch-case recursion ends, the exitFns top of the stack begins, hence the exit function. To put it another way, CodeGenNodes are actually generated in the “Transform Plugins” transformation plug-in

directiveTransforms

Using the nodeTransforms analysis above, directiveTransforms is also a class of transformation plug-ins that process instructions, except that they are called in a special fashion and are converted within element transforms

exitFns

Just to mention a little bit about what the exit function is, here is the code structure of the element transformation plug-in transformElement

export const transformElement: NodeTransform = (node, context) = > {
  // perform the work on exit, after all child expressions have been
  // processed and merged.
  return function postTransformElement() {
    // TODO
  }
Copy the code

It can be seen that transformElement is a higher-order function that returns a postTransformElement function. CodegenNode generation and patchFlag analysis are actually carried out in this exit function returned. As an additional note, the context required by postTransformElement is cached so that it can be executed directly exitFns[I]()

cacheHandlers

CacheHandlers have a name for them, but they’re basically a cache for the eventHandler eventHandler, as follows

Registers a click event for an element

By default, this is compiled to

Event caching is enabled to compile like this

Put them together and compare them as follows

{ onClick: _ctx.fn }
{
  onClick: 
    _cache[0] ||
    (_cache[0] = (. args) = >(_ctx.fn && _ctx.fn(... args))) }Copy the code

In other words, the eventHandler handler will be cached in _cache when enabled and will not need to be redefined later

Write about the

The above analysis of a lot, and then in accordance with the source ideas to simplify the implementation

createTransformContext

As mentioned at the beginning, the source code is complex, so laziness is a must, as follows

function createTransformContext(root, { nodeTransforms = [], directiveTransforms = {} }) {
  const context = {
    // plugin
    nodeTransforms,
    directiveTransforms,

    // state
    root,
    parent: null.currentNode: root,
  };
  return context;
}
Copy the code

My default implementation does not enable any optimization strategies and only implements the core logic, so all I need is this data

transform

The transform function needs to do the following

  • Get context
  • Traverse each node
  • Generated for the root nodecodegenNode
function transform(root, options) {
  const context = createTransformContext(root, options);
  traverseNode(root, context);
  createRootCodegen(root);
}
Copy the code

traverseNode

TraverseNode requires traversal of each node, and the “traversal” in this case refers to the transform plugins used to process the node, so it is clear what needs to be done

  • Gets the sequence of transformation plug-ins
  • The current nodes are processed in turn by plug-ins
  • Get the exit function and cache it
  • The child nodes are traversed recursively based on the node type
  • Execute exit function

The concrete implementation is as follows

traverseNode(node, context) {
  // Save the current node
  context.currentNode = node;
  // Get the sequence of conversion plug-ins
  const { nodeTransforms } = context;
  // Get the exit function
  const exitFns = [];
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context);
    if (onExit) {
      if(isArray(onExit)) { exitFns.push(... onExit); }else{ exitFns.push(onExit); }}if(! context.currentNode) {return;
    } else{ node = context.currentNode; }}// Process child nodes recursively
  switch (node.type) {
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context);
      break;

    case NodeTypes.INTERPOLATION:
    case NodeTypes.TEXT:
      // The two brothers are not dealt with here
      break;
  }

  context.currentNode = node;

  // Execute the exit function
  // Execute from leaf to root
  let i = exitFns.length;
  while(i--) { exitFns[i](); }}Copy the code

The nodeTransforms plug-in will be implemented later

traverseChildren

It’s easy to iterate through the children node, but if the child node is a string, you can just skip it and recurse

function traverseChildren(parent, context) {
  for (let i = 0; i < parent.children.length; i++) {
    const child = parent.children[i];
    // Skip the string
    if (typeof child === 'string') continue; context.parent = parent; traverseNode(child, context); }}Copy the code

This allows you to recursively traverse each node in the AST, and after being processed by the conversion plug-in, the nodes that need codeGenNodes generate their own CodeGenNodes. The ROOT node NodeTypes.ROOT also needs codegenNode as an entry to generate, as shown below

createRootCodegen

function createRootCodegen(root) {
  const { children } = root;
  if (children.length === 1) {
    const child = children[0];
    if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
      const codegenNode = child.codegenNode;

      root.codegenNode = codegenNode;
    } else{ root.codegenNode = child; }}// implement multiple root node support in the source code
  // else if (children.length > 1) {}
}
Copy the code

Because of the lazy support for multi-node components, the implementation here is very simple, just take the codegenNode of the child node and mount it on the root

Vue3 supports multi-node components, which are implemented by wrapping all root nodes in a fragment. This fragment was implemented from the runtime module, so it will not be implemented here

transformElement

The codegenNode is actually generated in a transformElement. This is just one of the transformelements, but it’s a very important one, so we’ll implement it first

Some of the functions that we’re going to use

To do this, we need to do some preparation. Define the following utility functions in ast.js, and of course add the corresponding NodeTypes attribute, which will be used later

  • createSimpleExpression

    function createSimpleExpression(content, isStatic = false) {
      return {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content,
        isStatic,
      };
    }
    Copy the code
  • createObjectExpression

    function createObjectExpression(properties) {
      return {
        type: NodeTypes.JS_OBJECT_EXPRESSION,
        properties,
      };
    }
    Copy the code
  • createObjectProperty

    function createObjectProperty(key, value) {
      return {
        type: NodeTypes.JS_PROPERTY,
        key: isString(key) ? createSimpleExpression(key, true) : key,
        value,
      };
    }
    Copy the code
  • createCallExpression

    function createCallExpression(args = []) {
      return {
        type: NodeTypes.JS_CALL_EXPRESSION,
        arguments: args,
      };
    }
    Copy the code
  • createVNodeCall

    // This function is used to generate codegenNode
    function createVNodeCall(type, tag, props, children, patchFlag, dynamicProps, directives, isComponent) {
      // The source code will also handle helper, but I am lazy not to need
      return {
        // The type is VNODE_CALL
        // The node type is the same as the node type
        type,
        tag,
        props,
        children,
        patchFlag,
        dynamicProps,
        directives,
        isComponent,
      };
    }
    Copy the code

Look at the

As described above, there is only one thing that a transformElement needs to do, and that is to generate a codegenNode

// vue-next/packages/compile-core/ast.ts r286
export interface VNodeCall extends Node {
  type: NodeTypes.VNODE_CALL
  tag: string | symbol | CallExpression
  props: PropsExpression | undefined
  children:
    | TemplateChildNode[] // multiple children
    | TemplateTextChildNode // single text child
    | SlotsExpression // component slots
    | ForRenderListExpression // v-for fragment call
    | SimpleExpressionNode // hoisted
    | undefined
  patchFlag: string | undefined
  dynamicProps: string | SimpleExpressionNode | undefined
  directives: DirectiveArguments | undefined
  isBlock: boolean
  disableTracking: boolean
  isComponent: boolean
}
Copy the code

As you can see, its properties are actually similar to those of the AST node generated earlier, with a few differences to cover

  • PatchFlag: Boot DIff type
  • DynamicProps: Saves the dynamic properties
  • Directives: Saves the compilation of the runtime directives (passesdirectiveTransforms )

What we do in transformElement is all around these codegenNode properties, and the steps are as follows

  • Deal with props
    • Analyze attributes/directives
    • Consolidation parameters
    • Analysis patchFlag
    • Standardization of props
  • Process the children node
  • Processing patchFlag
  • returncodegenNode

structure

TransfromElement logic is too complex, and there is a lot of fine processing, so I only choose the key implementation here, transformElement needs to return an exit function postTransformElement, all the logic is executed in this exit function. So we have the general structure

const transformElement = (node, context) = > {
  return function postTransformElement() {
    node = context.currentNode;

    // Only element nodes are processed
    if(node.type ! == NodeTypes.ELEMENT) {return;
    }

    // Initialize the following variables
    const { tag, props } = node;
    const isComponent = node.tagType === ElementTypes.COMPONENT;

    let vnodeTag = `"${tag}"`;
    let vnodeProps;
    let vnodeChildren;
    let vnodePatchFlag;
    let patchFlag = 0;
    let vnodeDynamicProps;
    let dynamicPropNames;
    let vnodeDirectives;

    // TODO processing props

    // TODO handles children

    // TODO handles patchFlag
  }
Copy the code

Deal with props

A buildProps function is separated from the source code to deal with the props. BuildProps is divided into static properties and dynamic properties, and there are instructions, so it needs to be analyzed separately. Define another analyzePatchFlag to do some special processing, as follows

function buildProps(node, context, props = node.props) {
  // Initialize some variables
  const isComponent = node.tagType === ElementTypes.COMPONENT;
  let properties = [];
  const mergeArgs = [];
  const runtimeDirectives = [];

  // Initialize some more variables
  let patchFlag = 0;
  let hasClassBinding = false;
  let hasStyleBinding = false;
  let hasHydrationEventBinding = false;
  let hasDynamicKeys = false;
  const dynamicPropNames = [];

  const analyzePatchFlag = ({ key }) = > {
    // TODO handles some bound attributes
  }

  for (let i = 0; i < props.length; i++) {
    const prop = props[i];
    if (prop.type === NodeTypes.ATTRIBUTE) {
      // TODO handles static attributes
    } else {
      // TODO processing instruction}}// TODO merges parameters

  // TODO analyzes patchFlag

  // TODO normalized props
}
Copy the code

So that’s the rough skeleton, and I’m going to start talking about it a little bit

Attribute processing

I really don’t know how to name this step, specifically in analyzePatchFlag, which will analyze some attributes on the current node, and assign parameters according to the analysis results, such as hasClassBinding, hasStyleBinding, etc. The subsequent analysis of patchFlag will refer to these analysis results, as follows

const analyzePatchFlag = ({ key }) = > {
  // isStatic determines whether the incoming node is a static simple expression node (SIMPLE_EXPRESSION)
  if (isStaticExp(key)) {
    const name = key.content;
    IsOn will determine if the incoming attribute is onXxxx event registered
    const isEventHandler = isOn(name);

    if(! isComponent && isEventHandler && name.toLowerCase() ! = ='onclick' &&
      // The source code also ignores the V-Model bidirectional binding
      // Ignore onVnodeXXX hooks
    ) {
      hasHydrationEventBinding = true;
    }

    // The source code ignores cacheHandler and properties with static values here

    // Analyze by attribute name
    if (name === 'class') {
      hasClassBinding = true;
    } else if (name === 'style') {
      hasStyleBinding = true;
    } else if(name ! = ='key' && !dynamicPropNames.includes(name)) {
      dynamicPropNames.push(name);
    }

    // Treat the class name and style bound to the component as dynamic properties
    if (
      isComponent &&
      (name === 'class' || name === 'style') &&! dynamicPropNames.includes(name) ) { dynamicPropNames.push(name); }}else {
    // Attribute name is not SIMPLE_EXPRESSION
    // is considered to have a dynamic key name
    hasDynamicKeys = true; }};Copy the code

To sum up, analyzePatchFlag traverses all attributes and analyzes them to provide reference standards for the subsequent analysis process of patchFlag

Static property handling

All I need to do in this step is filter out the static attributes and encapsulate them into the corresponding nodes, but because I was lazy, THERE are very few dynamic attributes, such as ref, is, etc., so the implementation of this step will look like this

for (let i = 0; i < props.length; i++) {
  // static attribute
  const prop = props[i];
  if (prop.type === NodeTypes.ATTRIBUTE) {
    const { name, value } = prop;
    let valueNode = createSimpleExpression(value || ' '.true);

    properties.push(
      createObjectProperty(createSimpleExpression(name, true), valueNode)
    );
  } else {
    // TODO processing instruction}}Copy the code

Static properties are wrapped into different types of nodes and stored in properties

Order processing

This step’s instruction processing is split into two parts, one for no-argument V-bind and V-ONS, and the second for runtime instructions processed by directiveTransforms directive parsing plug-ins that put the results into a mergeArgs array


for (let i = 0; i < props.length; i++) {
  // static attribute
  const prop = props[i];
  if (prop.type === NodeTypes.ATTRIBUTE) {
    / / omit...
  } else {
    // directives
    const { name, arg, exp } = prop;
    const isVBind = name === 'bind';
    const isVOn = name === 'on';

    // Source code here will skip the following instructions
    // v-slot
    // v-once/v-memo
    // v-is/:is
    // V-ON in SSR environment

    // Handle v-bind and V-ON without arguments
    if(! arg && (isVBind || isVOn)) {// Have dynamic keys
      hasDynamicKeys = true;

      // If there is a value, it will be processed
      if (exp) {
        if (properties.length) {
          mergeArgs.push(
            createObjectExpression(properties)
          );
          properties = [];
        }

        / / is v - bind
        if (isVBind) {
          mergeArgs.push(exp);
        } 
        
        / / v - on
        else {
          mergeArgs.push({
            type: NodeTypes.JS_CALL_EXPRESSION,
            arguments: [exp], }); }}continue;
    }

    // Runtime instruction processing
    const directiveTransform = context.directiveTransforms[name];
    // Built-in instruction
    if (directiveTransform) {
      const { props, needRuntime } = directiveTransform(prop, node, context);
      // Execute analyzePatchFlag for each attributeprops.forEach(analyzePatchFlag); properties.push(... props);if(needRuntime) { runtimeDirectives.push(prop); }}// Custom instruction
    else{ runtimeDirectives.push(prop); }}}Copy the code

Consolidation parameters

In this step, further encapsulation is performed depending on the parameters. MergeArgs only handles v-bind and V-ON without parameters, so this step combines v-bind and V-ON

if (mergeArgs.length) {
  if (properties.length) {
    mergeArgs.push(createObjectExpression(properties));
  }
  if (mergeArgs.length > 1) {
    propsExpression = createCallExpression(mergeArgs);
  } else {
    // There is only one V-bind
    propsExpression = mergeArgs[0]; }}else if (properties.length) {
  propsExpression = createObjectExpression(properties);
}
Copy the code

Analysis patchFlag

In this step, patchFlag will be analyzed, which is to assign values to patchFlag according to the previous parameters, as follows

if (hasDynamicKeys) {
    patchFlag |= PatchFlags.FULL_PROPS
} else {
  if(hasClassBinding && ! isComponent) { patchFlag |= PatchFlags.CLASS }if(hasStyleBinding && ! isComponent) { patchFlag |= PatchFlags.STYLE }if (dynamicPropNames.length) {
    patchFlag |= PatchFlags.PROPS
  }
  if (hasHydrationEventBinding) {
    patchFlag |= PatchFlags.HYDRATE_EVENTS
  }
}

// Ref and vnodeHook are also considered in the source code
if (
  (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
  runtimeDirectives.length > 0
) {
  patchFlag |= PatchFlags.NEED_PATCH;
}
Copy the code

As mentioned above, patchFlag analysis has been completed, and different patchflags are defined according to different attributes contained in the patchFlag. However, relevant knowledge of bit operators used here has been described in the previous VNode, so it is unnecessary to elaborate on it

Standardization of props

This step is simply a process for propsExpression, which wraps different JS_CALL_EXPRESSION objects according to different situations

if (propsExpression) {
  switch (propsExpression.type) {
    // There is no V-bind in props, only dynamic property binding is needed
    case NodeTypes.JS_OBJECT_EXPRESSION:
      let classKeyIndex = -1;
      let styleKeyIndex = -1;
      let hasDynamicKey = false;

      // Pass through all props to get the class name and the style index
      // Check if there are dynamic keys
      for (let i = 0; i < propsExpression.properties.length; i++) {
        const key = propsExpression.properties[i].key;
        // is a static key name
        if (isStaticExp(key)) {
          if (key.content === 'class') {
            classKeyIndex = i;
          } else if (key.content === 'style') { styleKeyIndex = i; }}// Is the dynamic key name
        else if(! key.isHandlerKey) { hasDynamicKey =true; }}const classProp = propsExpression.properties[classKeyIndex];
      const styleProp = propsExpression.properties[styleKeyIndex];

      // No dynamic key names
      if(! hasDynamicKey) {// Wrap the value of the class name if the value is dynamic
        if(classProp && ! isStaticExp(classProp.value)) { classProp.value = createCallExpression([classProp.value]); }// Wrap the style value when the style value is dynamic
        if (
          styleProp &&
          !isStaticExp(styleProp.value) &&
          (hasStyleBinding ||
            styleProp.value.type === NodeTypes.JS_ARRAY_EXPRESSION)
        ) {
          styleProp.value = createCallExpression([styleProp.value]);
        }
      } 
      
      // With dynamic key names it wraps the whole propsExpression directly
      else {
        propsExpression = createCallExpression([propsExpression]);
      }
      break;

    // Merge attributes, no processing required
    case NodeTypes.JS_CALL_EXPRESSION:
      break;

    // Only v-bind directly wraps the whole propsExpression
    default:
      propsExpression = createCallExpression([
        createCallExpression([propsExpression]),
      ]);
      break; }}Copy the code

This step is to facilitate the subsequent generate code generation process, which further subdivides the attribute into different expressions. The generated code can directly generate different structures based on the node type

Returns the result

Complete the above steps and you are ready to go back

return {
  props: propsExpression,
  directives: runtimeDirectives,
  patchFlag,
  dynamicPropNames,
};
Copy the code

True · Property handling

Now that we’ve done the buildProps function, we go back to the transformElement to do the properties processing

const transformElement = (node, context) = > {
  return function postTransformElement() {
    / /...

    // props
     if (props.length > 0) {
      // Get the result of attribute parsing
      constpropsBuildResult = buildProps(node, context); vnodeProps = propsBuildResult.props; patchFlag = propsBuildResult.patchFlag; dynamicPropNames = propsBuildResult.dynamicPropNames; vnodeDirectives = propsBuildResult.directives; }}}Copy the code

To deal with children

This step needs to deal with some built-in components, analyze the dynamic content in them, and obtain vnodeChildren. The source code handles Keep Alive and Teleport, but I did not implement these, so it is very simple, as follows

if (node.children.length > 0) {
  // List nodes
  if (node.children.length === 1) {
    const child = node.children[0];
    const type = child.type;

    // Analyze whether there are dynamic text child nodes, interpolation and compound text nodes
    // Composite text nodes are introduced in transformText
    const hasDynamicTextChild =
      type === NodeTypes.INTERPOLATION ||
      type === NodeTypes.COMPOUND_EXPRESSION;

    // Modify patchFlag if there are dynamic text child nodes
    if (hasDynamicTextChild) {
      patchFlag |= PatchFlags.TEXT;
    }

    / / get vnodeChildren
    if (hasDynamicTextChild || type === NodeTypes.TEXT) {
      vnodeChildren = child;
    } else{ vnodeChildren = node.children; }}else{ vnodeChildren = node.children; }}Copy the code

Processing patchFlag

The processing of patchFlag here should be said to be formatting, and the assignment of patchFlag has been completed by this step. It is mainly marked with the identification of the development environment for the convenience of viewing and debugging. Details are as follows

if(patchFlag ! = =0) {
  // If patchFlag is negative, there is no composite condition
  if (patchFlag < 0) {
    vnodePatchFlag = patchFlag + ` / *${PatchFlagNames[patchFlag]}* / `;
  } 
  
  // If patchFlag is positive, it indicates that there may be complex cases
  else {
    const flagNames = 
    // Get all the key names in PatchFlagNames
    Object.keys(PatchFlagNames)
      // Convert all to Number
      .map(Number)
      // Retain only those existing in patchFlag whose value is greater than 0
      .filter(n= > n > 0 && patchFlag & n)
      // Convert the patchFlag value to the corresponding patchFlag name
      .map(n= > PatchFlagNames[n])
      // Connect with a comma
      .join(', ');

    // Annotate the above content after patchFlag as a reference
    vnodePatchFlag = patchFlag + ` / *${flagNames}* / `;
  }

  // TODO handles dynamic attribute names
}
Copy the code

Here again the above series of API, for example patchFlag = 7, the corresponding binary data is 111 (2), so the TEXT is the means | CLASS | STYLE, becomes after the above series of API

// patchFlag = 7 === 1 | 10 | 100 === TEXT | CLASS | STYLE
flagNames = 'TEXT, CLASS, STYLE';
// vnodePatchFlag = '7 /* TEXT, CLASS, STYLE */';
Copy the code

Handle dynamic property names

This step converts the previously obtained dynamic property dynamicPropNames to a string for subsequent code generation. It is as simple as this

if(patchFlag ! = =0) {
  / /...

  if(dynamicPropNames && dynamicPropNames.length) { vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames); }}// Returns a string that iterates over all nodes and converts to an array structure
function stringifyDynamicPropNames(props) {
  let propsNamesString = '[';
  for (let i = 0, l = props.length; i < l; i++) {
    propsNamesString += JSON.stringify(props[i]);
    if (i < l - 1) propsNamesString += ', ';
  }
  return propsNamesString + '] ';
}
Copy the code

Mount the node

After all the above steps are complete, you are ready to assemble the properties required for codegenNode and mount them directly to the current node

node.codegenNode = createVNodeCall(
  node.type,
  vnodeTag,
  vnodeProps,
  vnodeChildren,
  vnodePatchFlag,
  vnodeDynamicProps,
  vnodeDirectives,
  isComponent
);
Copy the code

transformText

NodeTypes.COMPOUND_EXPRESSION, which is converted in transformText, The simple method is to combine several consecutive text nodes or interpolation nodes of the same level into a combined expression. This step is not necessary, but the logic is simple, so I will write it in passing

Look at the

Combine several text nodes or interpolation nodes of the same level into a combined expression. The key word here is the same level and continuous. The most direct idea is to directly exhaust the violence of the two-level for loop. In addition, note that transformText is also a Transform plugins, so it also returns an exit function

Write about the

isText

Before we start, we can write a utility function to determine whether the node is text or interpolation, as follows

function isText(node) {
  return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT;
}
Copy the code

transformText

What we need to do is to traverse to find the text/interpolation node, and then traverse the nodes behind it. If the text/interpolation node is found, merge it as follows

function transformText(node) {
  // Only element nodes and root nodes need to be processed
  if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) {
  return () = > {
    const children = node.children;
    let currentContainer = undefined;
    let hasText = false;

    // Traverse to find text/interpolation nodes
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      // If found, set hasText to true and look for subsequent nodes
      if (isText(child)) {
        hasText = true;
        // Find the next node
        for (let j = i + 1; j < children.length; j++) {
          const next = children[j];
          // Merge if found
          if (isText(next)) {
            if(! currentContainer) { currentContainer = children[i] = {type: NodeTypes.COMPOUND_EXPRESSION,
                children: [child],
              };
            }

            // Merge adjacent text/interpolation nodes into currentContainer
            currentContainer.children.push(next);
            children.splice(j, 1);
            j--
          } else {
            // Exit if not found
            currentContainer = undefined;
            break; }}}}// The source code here will also be pre-converted
    // convert the text node to NodeTypes.JS_CALL_EXPRESSION,
    // Call to createTextVNode(text)
    // The source code is annotated as follows
    // pre-convert text nodes into createTextVNode(text) calls to avoid
    // runtime normalization.}}Copy the code

So that’s it, notice the comments I wrote at the end, and the source code also does a pre-conversion, which is a call to createTextVNode(Text), which might be a little bit confusing, so put a little bit of code here

children[i] = {
  type: NodeTypes.TEXT_CALL,
  content: child,
  loc: child.loc,
  codegenNode: createCallExpression(
    context.helper(CREATE_TEXT),
    callArgs
  )
}
Copy the code

As you can see, the createCallExpression uses a CREATE_TEXT helper, which means it calls createTextVNode(text). This step is used to facilitate the subsequent code generation process, which I have been lazy about implementing

Q&A

Q: What is the convenience of generate? A: In the original AST, there was a functional division of node types, ROOT, ELEMENT, ATTRIBUTE, etc., but it was just good for us to look at it, we knew that this was a ROOT node and that was an ELEMENT node, but it was bad for code generation, because in code generation, We need to generate different structures according to different types. For example, the child node sequence should be an array, and the attributes should be in an object. Therefore, one of the things that the Transform segment does is to further subdivide the types of each node, and build codegenNode from the point of view of code generation. The type attribute doesn’t tell you what node it is, it tells you what structure it should generate, and you have to be careful to tell the difference. Okay

Q: This step is for performance optimization, what optimization? A: In fact, there are A lot of performance optimizations, but THE author is not knowledgeable enough to study them thoroughly. I will simply list A few that I can say. Firstly, the most obvious one is to add different patchflags to different nodes to optimize diFF performance, so as to guide the diFF algorithm. For example, some static content does not need DIFF, while some dynamic content may need full diFF. Then, in order to optimize runtime performance, many optimization strategies, such as EventHandler caching and static constant enhancement, are cited as obvious performance optimization strategies. In fact, it is said that one of the keys for vue3 to achieve targeted update is this block, but admittedly, I have not studied it, and I will write a special article about it when I do

Q: What if your writing is too messy for me to understand? A: I don’t think I can understand the principle of Transform thoroughly after reading my article, but I can at least sort out the whole process of transform and the role of each parameter. Therefore, I can take this article as A pre-preparation, and it will be smoother and clearer to read the source code after reading it

conclusion

The previous code is too cluttered to form a coherent line of thought, so here’s a quick summary.

Apart from performance optimization, the most important thing the whole Transform does is to build an AST from the perspective of the corresponding code structure of the node, which is mainly through codegenNode. As mentioned above, codegenNode represents the node structure rather than the node type. This is more advantageous for generate, such as seeing a JS_ARRAY_EXPRESSION to generate an array directly, or seeing a JS_OBJECT_EXPRESSION to generate an object directly.

In terms of performance optimization, the most important personal feeling is patchFlag. Its main function is to distinguish dynamic and static content and mark the ways that need diFF. For example, HOISTED means that the content is static promoted and does not need diFF, FULL_PROPS means that it has dynamic properties, Full diff is required

Finally, because the whole code is quite long, this article is also quite long, SO I will not put the whole code, if you want to see it, you can go to my Github