vue源码学习(8):codegen如何将ast语法树转换成render字符串?

970 阅读2分钟

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
}

上面的代码实际上做了一件事情:

image.png

对属性进行处理

// "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('+')})`
        }
    }
}

image.png

{
	"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 };

处理结果

image.png

Git地址

github.com/haimingyue/…