[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.

浙公网安备 33010602011771号