五、AST语法树处理
1. baseCompile
在上一节主要介绍了vue是如何处理模板template,将html结构的模板转换为对象形式的AST语法树。本节主要介绍如何将AST语法树转为render函数。
最终我们生成的AST语法树为
const ast = {
type: 1,
children: {
type: 4,
tag: 'p',
tagType: 0,
children:{
type: 2,
content: {
type: 3,
content:'msg'
}
}
},
helpers: [],
}
function baseCompile(template, options) {
const ast = baseParse(template);
transform(ast, Object.assign(options, {
nodeTransforms: [transformElement, transformText, transformExpression],
}));
return generate(ast);
}
}
2. transform
接下来通过transform以及传入的三个tranform函数来处理AST语法树
function transform(root, options = {}) {
const context = createTransformContext(root, options);
traverseNode(root, context);
createRootCodegen(root);
root.helpers.push(...context.helpers.keys());
}
2.1 createTransformContext
给root(语法树)添加一个nodeTransform转换属性,值为前面传入的三个处理函数。
同时增加了一个名为helpers的map结构,通过helper方法更改helpers对应的计数值
function createTransformContext(root, options) {
const context = {
root,
nodeTransforms: options.nodeTransforms || [],
helpers: new Map(),
helper(name) {
const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
},
};
return context;
}
2.2 traverseNode
遍历和处理AST语法树的节点
遍历处理函数,通过三个处理函数分别处理AST语法树,同时判断节点类型,如果为标签节点(type == 4)则遍历子节点进行转换。
function traverseNode(node, context) {
const type = node.type; // 获取节点类型
const nodeTransforms = context.nodeTransforms; // 获取处理函数
const exitFns = []; // 退出函数数组
for (let i = 0; i < nodeTransforms.length; i++) { //遍历处理函数
const transform = nodeTransforms[i];
const onExit = transform(node, context);
if (onExit) { //判断有无退出函数
exitFns.push(onExit);
}
}
switch (type) {
case 2:
context.helper(TO_DISPLAY_STRING);
break;
case 1:
case 4:
traverseChildren(node, context); //如果为标签节点则遍历内部子节点
break;
}
let i = exitFns.length;
while (i--) {
exitFns[i](); //连续执行退出函数,具体如下
}
}
function traverseChildren(parent, context) {
parent.children.forEach((node) => {
traverseNode(node, context);
});
}
处理结果
const node = {
"type": 1,
"children": [
{
"type": 4,
"tag": "p",
"tagType": 0,
"children": [
{
"type": 2,
"content": {
"type": 3,
"content": "_ctx.msg"
}
}
],
"codegenNode": {
"type": 4,
"tag": "'p'",
"props": null,
"children": {
"type": 2,
"content": {
"type": 3,
"content": "_ctx.msg"
}
}
}
}
],
"helpers": [
Symbol(toDisplayString),
Symbol(createElementVNode)
],
}
2.2.1 transformElement
转换元素节点
主要作用:
- 转换元素节点
- 创建虚拟节点调用
- 为代码生成做准备
function transformElement(node, context) {
if (node.type === 4) {
return () => { //返回一个退出函数,在traverseNode中重新调用
const vnodeTag = `'${node.tag}'`; //元素标签名
const vnodeProps = null; //元素属性
let vnodeChildren = null; // 元素子节点
if (node.children.length > 0) {
if (node.children.length === 1) { //如果元素子节点只有一个则直接设置为vnodeChildren
const child = node.children[0];
vnodeChildren = child;
}
}
node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren);
};
}
}
2.2.2 createVNodeCall
调用一次ast上下文的CREATE_ELEMENT_VNODE,同时返回一个虚拟节点。
function createVNodeCall(context, tag, props, children) {
if (context) {
context.helper(CREATE_ELEMENT_VNODE);
}
return {
type: 4,
tag,
props,
children,
};
}
flowchart LR
id1["{ type: 4, tag: 'p', tagType: 0, children}"]
id2["{ type: 4, tag: 'p', props: null, children}"]
id1-- transformElement --> id2
2.2.3 transformText
转换文本节点
作用:
- 合并连续的文本节点
- 减少虚拟节点的数量
- 提高渲染效率
判断是否为文本节点
function isText(node) {
return node.type === 2 || node.type === 0;
}
function transformText(node, context) {
if (node.type === 4) {
return () => { // 返回退出函数
const children = node.children; // 获取标签节点子元素
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (isText(child)) { //判断子元素是否为文本节点
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
if (!currentContainer) {
currentContainer = children[i] = {
type: 5,
loc: child.loc,
children: [child],
};
}
currentContainer.children.push(` + `, next);
children.splice(j, 1);
j--;
}
else {
currentContainer = undefined;
break;
}
}
}
}
};
}
}
以<p>111{{msg}}</p>为例来讲解vue内部如何合并连续的文本节点,
在上一节讲到,在通过parseTag识别出开始标签后,标签内部的111{{msg}}会重新进入parsechilren进行循环
通过parsechildren,前面的111会进入praseText流程返回{type:0,content:'111'},
而{{msg}}会进入parseInterpolation流程返回{type:2,content:'msg'},此时返回的AST语法树的children为两个元素。
node = {
type: 4,
children: [
{ type: 0, content: '111' }, // 文本节点
{ type: 2, content: 'msg' }, // 文本节点
]
}
//转换后
node = {
type:4,
children: [
{ type:5,
children:[
{ type: 0, content: '111'},
"+",
{ type: 2, content: 'msg'}
]}
]
}
进入transformText
- child为
{type:0,content:'111'},next为{type:2,content:'msg'} - 且二者均满足
type == 2 || type == 0,isText为true - currentContainer为空被赋值为
{type:5,children:[{type:0,content:'111'} ,'+',{type:2,content:'msg'}]} - j-- 结束循环
2.2.4 transformExpression
转换表达式
当node.type为2,即存在插值语法时,将content转为_ctx.content,实现了在模板中访问组件的数据和方法。
function transformExpression(node) {
if (node.type === 2) {
node.content = processExpression(node.content);
}
}
function processExpression(node) {
node.content = `_ctx.${node.content}`;
return node;
}
2.3 createRootCodegen
为root.codegenNode赋值
const node = {
codegenNode:child[0].codegenNode
}
function createRootCodegen(root, context) {
const { children } = root;
const child = children[0];
if (child.type === 4 && child.codegenNode) {
const codegenNode = child.codegenNode;
root.codegenNode = codegenNode;
}
else {
root.codegenNode = child;
}
}
3. generate
function generate(ast, options = {}) {
const context = createCodegenContext(ast, options);
const { push, mode } = context;
if (mode === "module") {
genModulePreamble(ast, context);
}
else {
genFunctionPreamble(ast, context);
}
const functionName = "render";
const args = ["_ctx"];
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
push("return ");
genNode(ast.codegenNode, context);
push("}");
return {
code: context.code,
};
}
genFunctionPreamble()
//生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return
push(`function ${functionName}(${signature}) {`);
push("return ");
//生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return function render(_ctx) { return
getNode(ast.codegenNode, context)
//最终生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return function render(_ctx) {return _createElementVNode('p', null, _toDisplayString(_ctx.msg))}
3.1 createCodegenContext
创建代码生成上下文
相当于赋值了几个默认值和几个生成方法:
mode:'function',runtimeModuleName:'vue',runtimeGlobalName: 'Vue'
helper,push,newline
const helperNameMap = {
[TO_DISPLAY_STRING]: "toDisplayString",
[CREATE_ELEMENT_VNODE]: "createElementVNode"
};
function createCodegenContext(ast, { runtimeModuleName = "vue", runtimeGlobalName = "Vue", mode = "function" }) {
const context = {
code: "",
mode,
runtimeModuleName,
runtimeGlobalName,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() {
context.code += "\n";
},
};
return context;
}
3.2 genFunctionPreamble
mode 默认值为function
这个函数是用来生成函数前言的代码的。函数前言是指函数体之前的代码,通常包括函数的参数声明、局部变量声明等
function genFunctionPreamble(ast, context) {
const { runtimeGlobalName, push, newline } = context;
const VueBinging = runtimeGlobalName; //'Vue'
const aliasHelper = (s) => {
return `${helperNameMap[s]} : _${helperNameMap[s]}`
}
if (ast.helpers.length > 0) {
push(`
const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging}
`);
}
newline();
push(`return `);
}
//生成
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return
3.3 getNode
循环根据节点类型生成代码
function genNode(node, context) {
switch (node.type) {
case 2:
genInterpolation(node, context);
break;
case 3:
genExpression(node, context);
break;
case 4:
genElement(node, context);
break;
case 5:
genCompoundExpression(node, context);
break;
case 0:
genText(node, context);
break;
}
}
3.3.1 genInterpolation
function genInterpolation(node, context) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(")");
}
3.3.2 genExpression
function genExpression(node, context) {
context.push(node.content, node);
}
3.3.3 genElement
function genElement(node, context) {
const { push, helper } = context;
const { tag, props, children } = node;
push(`${helper(CREATE_ELEMENT_VNODE)}(`);
genNodeList(genNullableArgs([tag, props, children]), context);
push(`)`);
}
3.3.4 genCompoundExpression
function genCompoundExpression(node, context) {
const { push } = context;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (isString(child)) {
push(child);
}
else {
genNode(child, context);
}
}
}
3.3.5 genText
function genText(node, context) {
const { push } = context;
push(`'${node.content}'`);
}
4. compile
function compileToFunction(template, options = {}) {
const { code } = baseCompile(template, options);
console.log("🚀 ~ compileToFunction ~ code:", code)
const render = new Function("Vue", code)(runtimeDom);
console.log("🚀 ~ compileToFunction ~ render:", render,runtimeDom)
return render;
}
此时经过AST转换后的code
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return function render(_ctx) {return _createElementVNode('p', null, _toDisplayString(_ctx.msg))}
通过new Function生成render函数,同时传入运行时环境runtimeDom
var runtimeDom = /*#__PURE__*/Object.freeze({
__proto__: null,
createApp: createApp,
getCurrentInstance: getCurrentInstance,
registerRuntimeCompiler: registerRuntimeCompiler,
inject: inject,
provide: provide,
renderSlot: renderSlot,
createTextVNode: createTextVNode,
createElementVNode: createVNode,
createRenderer: createRenderer,
toDisplayString: toDisplayString,
watchEffect: watchEffect,
reactive: reactive,
ref: ref,
readonly: readonly,
unRef: unRef,
proxyRefs: proxyRefs,
isReadonly: isReadonly,
isReactive: isReactive,
isProxy: isProxy,
isRef: isRef,
shallowReadonly: shallowReadonly,
effect: effect,
stop: stop,
computed: computed,
h: h,
createAppAPI: createAppAPI
});
new Function("Vue", code) 的意思是:
- 创建一个新的函数对象,它接受一个名为
Vue的参数。 - 使用
code变量的值作为函数体的代码。
实际生成的render函数
function(Vue) {
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return function render(_ctx) {return _createElementVNode('p', null, _toDisplayString(_ctx.msg))}
}