测试用例
test('union 3 type', () => {
const template = '<div>hi,{{message}}</div>'
const ast = baseParse(template)
transform(ast)
const code = codegen(ast)
expect(code).toMatchSnapshot()
})
最终生成
// 生成为
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue
export function render(_ctx, _cache) {
return _createElementVNode(
'div', null, 'hi,' + _toDisplayString(_ctx.message)) }
实现
目前问题
上节实现的 element类型转换,只是简单的判断如果是 element
类型就push对应的 helper
和 tag
,并没有考虑标签
内包裹的内容,即转换成 ast
里的 children
处理children
function genElement(node, context) {
const { push, helper } = context
const { tag } = node
push(`${helper(CREATE_ELEMENT_VNODE)}('${tag}'`)
// 加入对 children 的处理
const { children } = node
if (children.length) {
push(', null, ')
for (let i = 0; i < children.length; i++) {
genNode(children[i], context)
}
}
push(')')
}
改完代码,我们查看一下快照
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue
export function render(_ctx, _cache) { return _createElementVNode('div', null, 'hi,'_toDisplayString(_ctx.message)) }
快照跟我们预期的任有所差别,字符串部分与插值部分没有用 +
拼接
如果是 text
与 interpolation
相邻的,我们做拼接处理,这一块单独处理的判断,可以加多一个 NodeType 来判断这种类型
新增联合类型
主要是处理
text
与interpolation
相邻的情况
添加 COMPOUND_EXPRESSION
类型
export const enum NodeType {
INTERPOLATION,
SIMPLE_EXPRESSION,
ELEMENT,
TEXT,
ROOT,
// 加入复合类型
COMPOUND_EXPRESSION,
}
添加 plugin
新增 transformText plugin
,主要处理 COMPOUND_EXPRESSION
类型数据
/*
* @Author: Lin ZeFan
* @Date: 2022-04-10 10:45:42
* @LastEditTime: 2022-04-23 11:45:43
* @LastEditors: Lin ZeFan
* @Description:
* @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformText.ts
*
*/
import { NodeType } from "../ast";
import { isText } from "../utils";
export function transformText(node) {
if (node.type === NodeType.ELEMENT) {
// hi,{{msg}}
// 上面的模块会生成2个节点,一个是 text 一个是 interpolation 的话
// 生成的 render 函数应该为 "hi," + _toDisplayString(_ctx.msg)
// 这里面就会涉及到添加一个 “+” 操作符
// 那这里的逻辑就是处理它
// 检测下一个节点是不是 text 类型,如果是的话, 那么会创建一个 COMPOUND 类型
// COMPOUND 类型把 2个 text || interpolation 包裹(相当于是父级容器)
const { children = [] } = node;
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
/**
* 1. 判断当前节点是否 text 类型,是的话创建一个 COMPOUND 类型
* 2. 判断相邻元素是否为 text 类型,是的话通过 + 拼接
*/
if (isText(child)) {
// 看看下一个节点是不是 text 类
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
if (!currentContainer) {
// 相邻元素为text类型,把它们放到同一个children里
currentContainer = children[i] = {
type: NodeType.COMPOUND_EXPRESSION,
children: [child],
};
}
// 前面的元素为当前元素,这里再追加一个 + 拼接后一个元素
currentContainer.children.push(` + `, next);
// 把当前的节点放到容器内, 然后删除当前节点
children.splice(j, 1);
// 因为把 j 删除了,所以这里就少了一个元素,那么 j 需要 --
j--;
}
}
} else {
currentContainer = undefined;
}
}
};
}
function isText(node) {
return node.type === NodeType.TEXT || node.type === NodeType.INTERPOLATION
}
在测试用例上,加上当前 plugin
test('union 3 type', () => {
const template = '<div>hi,{{message}}</div>'
const ast = baseParse(template)
transform(ast, {
// 加入 transformText plugin
nodeTransforms: [transformElement, transformExpression, transformText],
})
const code = codegen(ast)
expect(code).toMatchSnapshot()
})
处理 COMPOUD 类型
function genNode(node, context) {
switch (node.type) {
case NodeType.TEXT:
genText(node, context)
break
case NodeType.INTERPOLATION:
genInterpolation(node, context)
break
case NodeType.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeType.ELEMENT:
genElement(node, context)
break
// 加入对 compound 类型的处理
case NodeType.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
}
}
function genCompoundExpression(node, context) {
const { children } = node
const { push } = context
// 对 children 进行遍历
for (let i = 0; i < children.length; i++) {
const child = children[i]
// 如果是 string,也就是我们手动添加的 +
if (typeof child === "string") {
// 直接 push
push(child)
} else {
// 否则还是走 genNode
genNode(child, context)
}
}
}
优化
优化 genElement
前面实现 genElement用了比较偷懒的形式,在扩展性上并不是特别好,针对element
类型我们通过拓展transformElement plugin
优化
拓展 transformElement plugin
/*
* @Author: Lin ZeFan
* @Date: 2022-04-10 10:45:42
* @LastEditTime: 2022-04-23 12:30:13
* @LastEditors: Lin ZeFan
* @Description:
* @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformElement.ts
*
*/
import { NodeType } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";
export function transformElement(node, context) {
if (node.type === NodeType.ELEMENT) {
// 添加相关的helper
context.helper(CREATE_ELEMENT_VNODE);
// 中间处理层,处理 props 和 tag
const { tag, props, children } = node;
const vnodeTag = `'${tag}'`;
const vnodeProps = props;
const vnodeChildren = children;
const vnodeElement = {
type: NodeType.ELEMENT,
tag: vnodeTag,
props: vnodeProps,
children: vnodeChildren,
};
node.codegenNode = vnodeElement;
}
}
新增 createRootCodegen 内部判断
// transform.ts
function createdRootCodegen(root: any) {
const { children } = root;
for (let index = 0; index < children.length; index++) {
const child = children[index];
// 在这里进行判断,如果说 child 的类型是 ELEMENT,那么直接取child.codegenNode
if (child.type === NodeType.ELEMENT) {
root.codegenNode = child.codegenNode;
} else {
root.codegenNode = child;
}
}
}
修改 genElement
function genElement(node, context) {
const { push, helper } = context;
const { tag, children, props } = node;
push(`${helper(CREATE_ELEMENT_VNODE)}(`);
// 批量处理 tag,props 和 children,优化空值情况
genNodeList(genNullable([tag, props, children]), context);
// 每创建一个 createVNode,就要加上一个结束符号
push(`)`);
}
function genNodeList(nodes, context) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
/** 处理node list
* 1. 如果是text,直接拼接
* 2. 如果是数组,遍历数组,把每一项再通过 genNode 检测类型
* 3. 如果是对象,给 genNode 检测类型
*/
if (isString(node)) {
push(`${node}`);
} else if (isArray(node)) {
for (let j = 0; j < node.length; j++) {
const n = node[j];
genNode(n, context);
}
} else {
genNode(node, context);
}
// 遍历完,加上分隔符
i < nodes.length - 1 && push(", ");
}
}
function genNullable(args) {
// 把undefined、null,转为 “null”
return args.map((arg) => arg || "null");
}
优化插件执行顺序
实现完 genElement
优化,产生了另外一个问题,就是 interpolation 的 ctx
消失了?
出现这个问题的原因,在于我们通过 transformText
这个 plugin 去处理 相邻元素的一些操作,改变了vnode的结构,把插值类型
包裹到复合
类型里面去了。
解决这个问题关键是,让 transformExpression
这个 plugin 优先执行,也就是优化插件执行顺序
方案:
- 在遍历我们的nodeTransforms里的
plugin
时,收集所有plugin
- 遍历完所有
plugin
后,再去执行收集的plugin
,若是返回一个
- 插件执行返回的是一个函数,表示该插件会是一个退出执行函数
- 始执行退出函数
// transform.ts
function traverseNode(node, context) {
const { nodeTransforms } = context;
const exitFns: any = [];
for (let index = 0; index < nodeTransforms.length; index++) {
const transform = nodeTransforms[index];
const exitFn = transform(node, context);
// 收集退出函数,即返回一个函数的plugin
// 这里只有 transformElement、transformText 两个plugin做了退出函数,因为要优先执行 transformExpression
if (exitFn) exitFns.push(exitFn);
}
// 在这里遍历整棵树的时候,将根据不同的 node 的类型存入不同的 helper
switch (node.type) {
case NodeType.INTERPOLATION:
context.helper(TO_DISPLAY_STRING);
break;
case NodeType.ROOT:
case NodeType.ELEMENT:
// 只有在 ROOT 和 ELEMENT 才会存在 children
traverseChildren(node, context);
break;
default:
break;
}
let i = exitFns.length;
// 执行所有的退出函数
while (i--) {
typeof exitFns[i] === "function" && exitFns[i]();
}
}
修改 transformText plugin
/*
* @Author: Lin ZeFan
* @Date: 2022-04-10 10:45:42
* @LastEditTime: 2022-04-23 11:45:43
* @LastEditors: Lin ZeFan
* @Description:
* @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformText.ts
*
*/
import { NodeType } from "../ast";
import { isText } from "../utils";
export function transformText(node) {
if (node.type === NodeType.ELEMENT) {
// 在 exit 的时期执行
// 下面的逻辑会改变 ast 树
// 有些逻辑是需要在改变之前做处理的
return () => {
// hi,{{msg}}
// 上面的模块会生成2个节点,一个是 text 一个是 interpolation 的话
// 生成的 render 函数应该为 "hi," + _toDisplayString(_ctx.msg)
// 这里面就会涉及到添加一个 “+” 操作符
// 那这里的逻辑就是处理它
// 检测下一个节点是不是 text 类型,如果是的话, 那么会创建一个 COMPOUND 类型
// COMPOUND 类型把 2个 text || interpolation 包裹(相当于是父级容器)
const { children = [] } = node;
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
/**
* 1. 判断当前节点是否 text 类型,是的话创建一个 COMPOUND 类型
* 2. 判断相邻元素是否为 text 类型,是的话通过 + 拼接
*/
if (isText(child)) {
// 看看下一个节点是不是 text 类
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
if (!currentContainer) {
// 相邻元素为text类型,把它们放到同一个children里
currentContainer = children[i] = {
type: NodeType.COMPOUND_EXPRESSION,
children: [child],
};
}
// 前面的元素为当前元素,这里再追加一个 + 拼接后一个元素
currentContainer.children.push(` + `, next);
// 把当前的节点放到容器内, 然后删除当前节点
children.splice(j, 1);
// 因为把 j 删除了,所以这里就少了一个元素,那么 j 需要 --
j--;
}
}
} else {
currentContainer = undefined;
}
}
};
}
}
修改transformElement plugin
/*
* @Author: Lin ZeFan
* @Date: 2022-04-10 10:45:42
* @LastEditTime: 2022-04-23 14:06:34
* @LastEditors: Lin ZeFan
* @Description:
* @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformElement.ts
*
*/
import { NodeType } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";
export function transformElement(node, context) {
if (node.type === NodeType.ELEMENT) {
// 返回一个函数
return () => {
// 添加相关的helper
context.helper(CREATE_ELEMENT_VNODE);
// 中间处理层,处理 props 和 tag
const { tag, props, children } = node;
const vnodeTag = `'${tag}'`;
const vnodeProps = props;
const vnodeChildren = children;
const vnodeElement = {
type: NodeType.ELEMENT,
tag: vnodeTag,
props: vnodeProps,
children: vnodeChildren,
};
node.codegenNode = vnodeElement;
};
}
}
最后去修改我们的测试用例
test('union 3 type', () => {
const template = '<div>hi,{{message}}</div>'
const ast = baseParse(template)
transform(ast, {
// 加入 transformText plugin
nodeTransforms: [transformExpression, transformElement, transformText],
})
const code = generate(ast)
expect(code).toMatchSnapshot()
})
图示
我们初始化过程应该是从 transformExpression - transformText - transformElement
所以例子上,plugin的顺序才这么放