实现代码生成三种联合类型
- 实现处理 elemenet 类型 render 函数,与插值语法类型 render 函数类似,我们需要生成
引入的方法,createElementVNode(_createElementBlock) 方法
js
复制代码
// 参照对比 https://template-explorer.vuejs.org/#eyJzcmMiOiI8ZGl2PkhlbGxvIFdvcmxkPC9kaXY+Iiwib3B0aW9ucyI6e319 生成代码
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div"))
}
- 单测
js
复制代码
// codegen.spec.ts
it("element",()=>{
const ast = baseParse('<div></div>')
transform(ast, {
nodeTransforms: [transformExpresssion]
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
- 运行单测后
js
复制代码
// codegen.spec.ts.snap
exports[`codegen element 1`] = `
"
return function render(_ctx,_cache){return }"
`;
- 观察对比,我们需要给 return 后面添加返回值,找到需要添加的位置,上一节中我们关于return是在下面函数进行处理,我们还是先按之前的思路的考虑
js
复制代码
// transform.ts
function traverseNode(node: any, context) {
const nodeTransforms = context.nodeTransforms
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i]
transform(node)
}
switch (node.type) {
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING)
break;
case NodeTypes.ROOT:
traverseChildren(node, context)
break;
case NodeTypes.ELEMENT:
// ❗ 我们按以前的逻辑应该放在这里,
// 我们重新抽离出去 traverseElement
traverseChildren(node, context)
default:
break;
}
}
js
复制代码
// runtimeHelpers.ts
export const TO_DISPLAY_STRING = Symbol("toDisplayString")
export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode") // ✅
export const helperMapName = {
[TO_DISPLAY_STRING]: "toDisplayString",
[CREATE_ELEMENT_VNODE]: "createElementVNode" // ✅
}
// ✅ transforms/transformElement.ts
import { NodeTypes } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";
export function transformElement(node, context) {
if(node.type === NodeTypes.ELEMENT) {
context.helper(CREATE_ELEMENT_VNODE)
}
}
// codegen.spec.ts
import { transformElement } from '../src/transforms/transformElement'
it("element",()=>{
const ast = baseParse('<div></div>')
transform(ast, {
nodeTransforms: [transformElement]// ✅ 这里调用就会传入 transform 方法,在 node children 中递归调用
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
// transform.ts
function traverseNode(node: any, context) {
const nodeTransforms = context.nodeTransforms
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i]
transform(node, context) // ✅ 这里的 transform 就是上面传入的方法 transformElement
}
switch (node.type) {
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING)
break;
case NodeTypes.ROOT:
case NodeTypes.ELEMENT:
traverseChildren(node, context)
default:
break;
}
}
js
复制代码
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode } = Vue // 💡 我们发现已经生成引入,上面处理成功了
return function render(_ctx,_cache){return }"
`;
- 后续我们要在
return function render(_ctx,_cache){return }return 里面调用 createElementVNode,然后传入 div 节点,也是参照之前 return 中的逻辑实现放到下面的函数
js
复制代码
// codegen.ts
function getNode(node: any, context) { // 根据不同类型节点解析为不同的返回值
switch (node.type) {
case NodeTypes.TEXT:
genText(node, context)
break;
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break;
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break;
case NodeTypes.ELEMENT: // ✅
genElement(node, context)
break;
default:
break;
}
}
function genElement(node, context) { // ✅
const { push, helper } = context
const { tag } = node
push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}")`)
}
js
复制代码
// codegen.spec.ts.snap
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode } = Vue
return function render(_ctx,_cache){return _createElementVNode("div")}" // ✅
`;
- 生成标签代码已经完成,下面我们的目标是生成联合类型,element,text 和 插值类型混用
<div>hi,{``{message}}</div>
js
复制代码
// 在线网址生成代码
const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, "hi," + _toDisplayString(_ctx.message), 1 /* TEXT */))
}
// 注意这里的 createElementBlock 与我们生成的 createElementVNode 是一个东西
js
复制代码
// 对比生成代码,我们修改我们的代码先跑通单测提供一个快照,方便后续以这个快照为基准进行测试
// codegen.ts
function genElement(node, context) {
const { push, helper } = context
const { tag } = node
push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, "hi," + _toDisplayString(_ctx.message))`) // ✅
}
// codegen.spec.ts
it("element",()=>{
const ast = baseParse('<div>hi,{{message}}</div>') // ✅
transform(ast, {
nodeTransforms: [transformElement]
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
// codegen.spec.ts.snap
// 快照在 -u 的命令下生成了,具体的 jest 快照相关知识参照 49 节
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode,toDisplayString: _toDisplayString } = Vue
return function render(_ctx,_cache){return _createElementVNode("div", null, "hi," + _toDisplayString(_ctx.message))}"
`;
- 我们下面动态实现 "hi," + _toDisplayString(_ctx.message)
js
复制代码
function genElement(node, context) {
const { push, helper } = context
const { tag, children } = node
push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, `)
for(let i = 0; i < children.length;i++) {
const child = children[i]
getNode(child,context) // 这里返回值继续使用 getNode 来处理
}
push(')')
}
- 上面的代码生成的效果对比官方也就是我们刚刚生成的快照,少了 + 号, 和 _ctx.
- 解决方法:设计一个新的节点类型, 复合类型, compound, 基于复合类型做一些特殊处理
- 特点:复合类型节点里面 text 和 插值相邻节点之间是使用 + 号进行拼接的
- 在哪里处理呢?这块是代码转换,所以应该在 transform.ts 相关位置进行处理
js
复制代码
// ✅ 处理复合类型
// transforms/transformText
import { NodeTypes } from "../ast"
export function transformText(node, context) {
const { children } = node
function isText(node) {
return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION
}
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: NodeTypes.COMPOUND_EXPRESSION,
children: [child]
}
}
currentContainer.children.push(" + ") // 进行拼接
currentContainer.children.push(next)
children.splice(j,1)
j--// 防止删除后下标混乱--
} else {
currentContainer = undefined
break
}
}
}
}
}
js
复制代码
// codegen.spec.ts
it("element",()=>{
const ast:any = baseParse('<div>hi,{{message}}</div>')
transform(ast, {
nodeTransforms: [transformElement, transformText] // ✅️
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
// codegen.ts
function getNode(node: any, context) {
switch (node.type) {
case NodeTypes.TEXT:
genText(node, context)
break;
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break;
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break;
case NodeTypes.ELEMENT:
genElement(node, context)
break;
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context) // ✅ 返回类型增加复合节点
break;
default:
break;
}
}
function genCompoundExpression(node, context) { // ✅
const { push } = context
const { children, tag } = node
for(let i = 0; i < children.length; i++) {
const child = children[i]
if(isString(child)){ // 如果已经通过+号拼接好的字符串,直接放入返回字符串中进行返回
push(child)
} else {
getNode(child, context)
}
}
}
js
复制代码
// 上面代码处理效果
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode } = Vue
return function render(_ctx,_cache){return _createElementVNode("div", null, 'hi,' + _toDisplayString(message))}" // ❗ 注意这里的 message 前面少了 _ctx.
` ;
- 优化
js
复制代码
// 优化前
function genElement(node, context) {
const { push, helper } = context
const { tag, children } = node
push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, `)
for(let i = 0; i < children.length;i++) { // 这里的 for 循环不需要,仅需要执行 getNode 就可以
const child = children[i]
getNode(child,context)
}
push(')')
}
// 优化后
// codegen.ts
function genElement(node, context) {
const { push, helper } = context
const { tag, children } = node
const child = children[0]
push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, `)
// for(let i = 0; i < children.length;i++) {
// const child = children[i]
// getNode(child,context)
// }
getNode(child, context) // 这里的 child ,属于 element 类型时,仅会只有一个 child
push(')')
}
- 我们发现上面的优化属于处理层的逻辑,我们应该放在 transformElement 上面
js
复制代码
// transformElement.ts
import { NodeTypes } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";
export function transformElement(node, context) {
if(node.type === NodeTypes.ELEMENT) {
context.helper(CREATE_ELEMENT_VNODE)
// 中间处理层 ✅
// tag
const vnodeTag = node.tag
// props
let vnodeProps
// children
const children = node.children
const vnodeChildren = children[0]
const vnodeElement = {
type: NodeTypes.ELEMENT,
tag: vnodeTag,
props: vnodeProps,
children: vnodeChildren
}
node.codegenNode = vnodeElement // ✅ 生成虚拟dom 进行挂载
}
}
// transform.ts
function createRootCodegen(root: any) {
const child = root.children[0]
if (child.type === NodeTypes.ELEMENT) { // ✅ 将上面的挂载进行赋值
root.codegenNode = child.codegenNode
} else {
root.codegenNode = root.children[0]
}
}
// codegen.spec.ts
it("element",()=>{
const ast:any = baseParse('<div>hi,{{message}}</div>')
transform(ast, {
nodeTransforms: [transformText, transformElement] // ✅ 这里的两个方法交换了位置
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
- 我们处理剩余问题
- null
- _ctx.
- 我们想要解决 _ctx. 就需要再返回值这里进行处理, 并且需要在测试文件传入 transformExpression.ts
js
复制代码
// codegen.spec.ts
it("element",()=>{
const ast:any = baseParse('<div>hi,{{message}}</div>')
transform(ast, {
nodeTransforms: [transformExpresssion, transformText, transformElement] // ✅ 这里放入 transformExpression 方法
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
- 发现还是不行, 原因是,我们虽然在这里
nodeTransforms: [transformExpresssion, transformText, transformElement],调整了执行顺序,但把普通 text + interpolation 两种节点转为复合节点导致,即使按顺序执行,下面这块的条件判断也进不去了,我们需要重新捋顺这块的执行顺序
js
复制代码
// transformExpression.ts
export function transformExpresssion(node) {
if (node.type === NodeTypes.INTERPOLATION) { // 这块进不去
processExpression(node.content)
}
}
- 通过循环调整执行顺讯
- 之前我们仅仅设计了进入的时候依次调用 三个函数,
- 现在我们加上退出调用三个函数的逻辑
- 类似:进入 1 2 3 退出 3 2 1
- 这样我们能够保证一开始的时候执行1 ,退出的时候执行 2 3
js
复制代码
function traverseNode(node: any, context) {
const nodeTransforms = context.nodeTransforms
const existFns: any = [] // ✅ 收集调用时返回的函数
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i]
const onExit = transform(node, context) // ✅
if(onExit) existFns.push(onExit)// ✅
}
switch (node.type) {
case NodeTypes.INTERPOLATION:
// 这里我们想要把插值添加到根节点上,现在我们 node 是循环遍历的
context.helper(TO_DISPLAY_STRING)
break;
case NodeTypes.ROOT:
case NodeTypes.ELEMENT:
traverseChildren(node, context)
default:
break;
}
// 这里是退出的时候执行, 先调用的后执行,后调用的先执行
let i = existFns.length // ✅
while(i--) { // ✅
existFns[i]() // 根据这里的调用需要把 transformText 和 transformElement ,的逻辑放在 return ()=> {} 这里执行
}
}
// transformText.ts
export function transformText(node, context) {
function isText(node) {
return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION
}
if (node.type === NodeTypes.ELEMENT) {
return () => {
// 省略
}
}
}
// transformElement.ts
export function transformElement(node, context) {
if (node.type === NodeTypes.ELEMENT) {
return () => {
// 省略
}
}
}
js
复制代码
// codegen.spec.ts
it("element",()=>{
const ast:any = baseParse('<div>hi,{{message}}</div>')
transform(ast, {
nodeTransforms: [transformExpresssion, transformElement, transformText]
})
const { code } = generate(ast)
expect(code).toMatchSnapshot()
})
- 最后测试成功
- 还有一个 null 我们之前是写死的,我们需要在 参数是 undefined 时,转换为 null,把所有项目中的 undefined 都转为 null 是很多的,我们来做一个映射
js
复制代码
// codegen.ts
function genElement(node, context) {
const { push, helper } = context
const { tag, children, props } = node
push(`${helper(CREATE_ELEMENT_VNODE)}(`)
genNodeList(genNullable([tag, props, children]), context) // ✅
push(')')
}
function genNullable(args: any) { // ✅
return args.map(( arg ) => arg || 'null')
}
function genNodeList(nodes: any, context) { // ✅
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
} else {
getNode(node, context)
}
if(i < nodes.length - 1) { // 💡
push(",")
}
}
}
// transformElement.ts
export function transformElement(node, context) {
if (node.type === NodeTypes.ELEMENT) {
return () => {
context.helper(CREATE_ELEMENT_VNODE)
// 中间处理层
// tag
const vnodeTag = `"${node.tag}"` // ✅
// props
let vnodeProps
// children
const children = node.children
const vnodeChildren = node.children[0]
const vnodeElement = {
type: NodeTypes.ELEMENT,
tag: vnodeTag,
props: vnodeProps,
children: vnodeChildren
}
node.codegenNode = vnodeElement
}
}
}
- 重构
js
复制代码
// transformElement.ts
export function transformElement(node, context) {
if (node.type === NodeTypes.ELEMENT) {
return () => {
context.helper(CREATE_ELEMENT_VNODE)
const vnodeTag = `"${node.tag}"`
let vnodeProps
const children = node.children
const vnodeChildren = node.children[0]
// ❗ 这里需要抽离到 ast.ts
// const vnodeElement = {
// type: NodeTypes.ELEMENT,
// tag: vnodeTag,
// props: vnodeProps,
// children: vnodeChildren
// }
// node.codegenNode = vnodeElement
node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren)
}
}
}
// ast.ts
import { CREATE_ELEMENT_VNODE } from "./runtimeHelpers"
export function createVNodeCall(context, tag, props, children) {
const { helper } = context
helper(CREATE_ELEMENT_VNODE)
return {
type: NodeTypes.ELEMENT,
tag, props, children
}
}
js
复制代码
exports[`codegen element 1`] = `
"const { toDisplayString: _toDisplayString,createElementVNode: _createElementVNode } = Vue
return function render(_ctx,_cache){return _createElementVNode("div",null,'hi,' + _toDisplayString(_ctx.message))}"
`;