[Pattern] Context & Plugin patterns

For example, we have following code doing AST transform from templateAST to Javascript AST

function traverseNode(ast) {
  const currentNode = ast;

  // If type is Element
  if(currentNode.type === 'Element' && currentNode.tag === 'p'){
    // for example, swap 'p' with 'h1' 
    currentNode.tag = 'h1'
  }
  
  // If type is Text
  if(currentNode.type === 'Text'){
    // for example, repeat 3 times
    currentNode.content = currentNode.content.repeat(3)
  }

  const children = currentNode.children;
  if (children) {
    for (let i = 0; i < children.length; i++) {
      traverseNode(children[i]);
    }
  }
}

In the code, we hardcode the transform methods against Element, Text. And this is not a good pattern in long run.

First, we can encapsulate the transform into functions -- Plugin pattern

Second, we can use context to keep tracking the related data and helper functions -- Context pattern

function transformElement(node) {
  if (node.type === "Element" && node.tag === "p") {
    node.tag = "h1";
  }
}

function transformText(node) {
  if (node.type === "Text") {
    node.content = node.content.repeat(3);
  }
}

function traverseNode(ast, context) {
  context.currentNode = ast

  const transforms = context.nodeTransforms
  for (let i = 0; i < transforms.length; i++) {
    transforms[i](context.currentNode, context)
    if (!context.currentNode) {
        return;
    }
  }

  const children = context.currentNode.children
  if (children) {
    for (let i = 0; i < children.length; i++) {
      context.parent = context.currentNode
      context.childIndex = i
      traverseNode(children[i], context)
    }
  }
}

function transform(ast) {
  const context = {
    // current processing node
    currentNode: null,
    // current processing node's parent
    parent: null,
    // current node's index under it's parent node
    childIndex:0,
    // Plugin pattern: all the transform methods
    nodeTransforms: [
      transformElement,
      transformText
    ]
  }

  // Context pattern: pass in the context
  traverseNode(ast,context);
}

 

Beside data (currentNode, parent, childIndex...) we can also write helper methods into the context:

function transform(ast) {
  const context = {
    currentNode: null,
    parent: null,
    childIndex:0,
    replaceNode(node){
      context.parent.children[context.childIndex] = node
      context.currentNode = node
    },
    removeNode(){
      if(context.parent){
        context.parent.children.splice(context.childIndex,1)
        context.currentNode = null
      }
    },
    nodeTransforms: [
      transformElement,
      transformText
    ]
  }

  traverseNode(ast,context);
}

Then the individual transform function can be updated as:

function transformText(node, context) {
  if (node.type === "Text") {
    // context.replaceNode({
    //   type: "Text",
    //   content: node.content.repeat(3)
    // })

     context.replaceNode({
       type: "Element",
       tag: 'span'
     })

    // context.removeNode()
  }
}

 

Summary:
Actually, looking back, we just pass in a simple context object. But by using properties like currentNode and parent, we effectively store the global compile state, making it easy for transform functions to access and modify nodes.

Also, nodeTransforms is an array where each element is a function. Right now our functions are very simple, but we can use them to define all kinds of transformation rules for template AST nodes—essentially creating compiler “node transform” plugins.

In fact, some of Vue’s built-in transform logic (for handling v-if, v-for, component nodes, etc.) also lives in nodeTransforms. This lets developers extend the template compiler by registering their own transform functions—for example, to add custom directives or optimization rules.

Designing it as an array fits a modular approach: different AST node types can be handled by different transform functions. Spreading logic across multiple functions makes the code easier to read and maintain—for instance, transformIf handles conditional-render nodes, while transformFor handles loop nodes.

Finally, traverseNode does a depth-first traversal, calling every function in nodeTransforms for each node in turn. This ensures that parent-node logic runs after all child nodes have been processed.

posted @ 2025-06-26 13:50  Zhentiw  阅读(12)  评论(0)    收藏  举报