codegen的背景说明
vue的compiler中,将我们编写的vue语法先转换成ast语法树。
假设我们编写了一段如下的代码:
<div id="app" a="111" style="color: red;background: green;">
hello{{arr}}word
</div>
compiler会通过parseHTML解析成下面的ast语法树:
{
"tag": "div",
"type": 1,
"children": [{
"type": 3,
"text": "hello{{arr}}word"
}],
"parent": null,
"attrs": [{
"name": "id",
"value": "app"
},
{
"name": "a",
"value": "111"
},
{
"name": "style",
"value": "color: red;background: green;"
}
]
}
而codegen的作用就是把这样的ast语法树转换成render字符串。也就是以_c
、_s
以及_v
开头的字符串。
如果将上面的ast进行转换,得到的结果会是下面这段字符串:
_c('div', {id:"app",a:"111",style:{"color":" red","background":" green"}}),_v("hello"+_s(arr)+"word")
这里可以看得出:ast只能描述语法,而render字符串是用来描述Dom的,它可以扩展属性。
正餐:codegen代码编写和分析
用一句话概括codegen的实现方式:对ast语法进行处理、循环,不断地拼接字符串,直到得到想要的结果。
在正式分析代码之前,我们需要了解_c
、_s
以及_v
分别代表什么意思:
_c: createElement,创建虚拟Dom。
_v: 是创建字符串
_s: 是对模板语法{{}}中的变量进行处理
当codegen接受到ast的json的时候,代码如下:
function generate(el) {
console.log('---------------', el)
let children = genChildren(el)
// 遍历🌲,将🌲拼接成字符串
let code = `_c('${el.tag}', ${el.attrs.length ? genProps(el.attrs) : 'undefined'
})${children ? `,${children}` : ''}`
return code
}
上面的代码实际上做了一件事情:
对属性进行处理
// "attrs": [{
// "name": "id",
// "value": "app"
// },
// {
// "name": "a",
// "value": "111"
// },
// {
// "name": "style",
// "value": "color: red;background: green;"
// }
// ]
function genProps(attrs) {
let str = ''
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i]
if (attr.name === 'style') {
let styleObj = {}
attr.value.replace(/([^:;]+):([^:;]+)/g, function () {
console.log(arguments[1], arguments[2])
styleObj[arguments[1]] = arguments[2]
})
attr.value = styleObj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`
}
我们能看到,入参就是一个attr的数组,对数组的操作自然很简单,就是进行循环然后进行遍历、拼接。得到如下的字符串。
{id:"app",a:"111",style:"color: red;background: green;"}
这里面较为麻烦的是对style的处理,因为style的后面,跟着的是"color: red;background: green;"
的字符串,所以我们在处理的时候,希望把它变成下面这种形式:
style:{"color":" red","background":" green"}
if (attr.name === 'style') {
let styleObj = {}
attr.value.replace(/([^:;]+):([^:;]+)/g, function () {
console.log(arguments[1], arguments[2])
styleObj[arguments[1]] = arguments[2]
})
attr.value = styleObj
}
加餐:正则小课堂
/([^:;]+):([^:;]+)/g
正则中的()代表分组
([^:;]):([^:;])代表通过 : 进行分组
[]代表一个
+代表任意多个
^代表取反
/g代表全局匹配
所以分析出来就是这个意思
(任意多个[不是:或者;的字符]) : (任意多个[不是:或者;的字符])
"color: red;background: green;"被正则匹配后,会得到下面的结果:
color red
background green
然后通过replace进行替换成一个对象,得到最终的结果
对children进行处理
function genChildren(el) {
let children = el.children
if (children) {
return children.map(c => gen(c)).join(',')
}
}
function gen(el) {
let text = el.text
if (el.type === 1) { // element: 1 text: 3
return generate(el)
} else {
if (!defaultTagRE.test(text)) {
return `_v('${text}')`
} else {
// 拆分
let tokens = []
let match;
let lastIndex = defaultTagRE.lastIndex = 0;
while (match = defaultTagRE.exec(text)) {
// 看有没有匹配到
let index = match.index // 开始索引
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
console.log('tokens', tokens)
return `_v(${tokens.join('+')})`
}
}
}
{
"children": [{
"type": 3,
"text": "hello{{arr}}word"
}]
}
这里的逻辑是:
1、如果children的type是1,则代表这是一个element,就要继续走一遍上述的逻辑
2、如果children的type是3,则只需要对其进行_v处理即可
3、如果text中包含用{{}}
包裹的字符串,需要通过_s处理
defaultTagRE是一段正则,专门用来配置{{
开头和}}
结尾的字符串
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
下面的代码则是对text
的文本进行循环的通过正则进行匹配和替换
let tokens = []
let match;
let lastIndex = defaultTagRE.lastIndex = 0;
while (match = defaultTagRE.exec(text)) {
// 看有没有匹配到
let index = match.index // 开始索引
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
console.log('tokens', tokens)
return `_v(${tokens.join('+')})`
完整代码
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{aaaaa}}
function genProps(attrs) {
let str = ''
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i]
if (attr.name === 'style') {
let styleObj = {}
attr.value.replace(/([^:;]+):([^:;]+)/g, function () {
console.log(arguments[1], arguments[2])
styleObj[arguments[1]] = arguments[2]
})
attr.value = styleObj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`
}
function gen(el) {
let text = el.text
if (el.type === 1) { // element: 1 text: 3
return generate(el)
} else {
if (!defaultTagRE.test(text)) {
return `_v('${text}')`
} else {
// 拆分
let tokens = []
let match;
let lastIndex = defaultTagRE.lastIndex = 0;
while (match = defaultTagRE.exec(text)) {
// 看有没有匹配到
let index = match.index // 开始索引
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
console.log('tokens', tokens)
return `_v(${tokens.join('+')})`
}
}
}
function genChildren(el) {
let children = el.children
if (children) {
return children.map(c => gen(c)).join(',')
}
}
function generate(el) {
console.log('---------------', el)
let children = genChildren(el)
// 遍历🌲,将🌲拼接成字符串
let code = `_c('${el.tag}', ${el.attrs.length ? genProps(el.attrs) : 'undefined'
})${children ? `,${children}` : ''}`
return code
}
export { generate };