前言
虽然现在已经是Vue3的版本,也已经用了相当一段时间的Vue3。但想起来Vue2的源码之前断断续续也看了蛮长的时间,就再回头整理一遍。有始有终。
我这里把接下来要写的内容分为编译时 和运行时。
这是第二篇,我们将继续探索,Vue2编译时从parse()
得到AST树之后,将继续干什么。
我们使用这个AST树帮助我们加深认识和理解:
json
{
tag: 'div',
type: 1,
attrs: [{name: 'class', value: 'container'}, {name: 'style', value: 'color:red; font-size:14px'}],
parent: null,
children: [
{
tag: 'div',
type: 1,
attrs: [],
parent,
children: []
},
{
type: 3,
text: 'hello{{name}}'
}
]
}
代码逻辑
在拿到这个ast树之后,Vue2将处理它成为一串代码,这个过程就是genCode
,在Vue2仓库中的内容要更详细,使用更多gen
函数处理各种情况。在这里先做一个简要的版本来理解其中的思想,所以跟源码相比会简略不少。
js
/**
* @param {ast-node} node
* @returns code
*/
export default function generate(ast) {
let code;
if (ast) {
if (ast.atg === 'script') {
code = 'null';
} else {
code = genElement(ast); // 这里我们仅考虑这种情况
}
} else {
code = '_c("div")';
}
return {
render: `with(this){return ${code}}`
}
}
根据传入的元素节点,genElement
将生成渲染函数的code,就是形如_c('div')
这样的代码文本
js
function genElement(el) {
let code = `_c(${el.tag}${
el.attrs.length ? `,${genProps(el.attrs)}` : '' // attrs
}${
children ? `,${children}` : '' // children
})`
return code
}
接下来我们需要处理ast.children
中的子节点和属性
子节点
js
function genElement(el) {
let children = genChildren(el); // 处理子节点,children为undefined或形如 `[_v('xxx'+_s(yyy)),_v('xxx'+_s(yyy))]`
let code = `_c(${el.tag}${
el.attrs.length ? `,${genProps(el.attrs)}` : '' // attrs
}${
children ? `,${children}` : '' // children
})`;
return code
}
function genChildren(el) {
const children = el.children;
if (children) {
return `[${children.map((c) => gen(c)).join(",")}]`
}
}
function gen(node) {
if (node.type == 1) {
// 元素
return genElement(node); // 递归处理
} else {
// 文本
}
}
节点内容处理:
js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{ }} 捕获花括号里面的内容
function gen(node) {
if (node.type == 1) {
// 元素
return genElement(node); // 递归处理
} else {
// 文本
let text = node.text
if (!defaultTagRE.test(text)) { // 非花括号变量表达式
return `_v(${JSON.stringify(text)})`
}
// 正则是全局模式 每次需要重置正则的lastIndex属性 不然会引发匹配bug
// 文本
let text = node.text
if (!defaultTagRE.test(text)) { // 非花括号变量表达式
return `_v(${JSON.stringify(text)})`
}
// 正则是全局模式 每次需要重置正则的lastIndex属性 不然会引发匹配bug
let lastIndex = (defaultTagRE.lastIndex = 0)
let tokens = []
let match, index
// eg.假设text = 'Hello, {{ name }}! Today is {{ date }}.'
while ((match = defaultTagRE.exec(text))) {
index = match.index; // 匹配到花括号变量的位置
if (index > lastIndex) {
// 匹配到的 {{ 位置 在 tokens 里面放入普通文本
tokens.push(JSON.stringify(text.slice(lastIndex, index))); // eg.截取'Hello, '
}
// 放入捕获到的变量内容
tokens.push(`_s(${match[1].trim()})`); // eg.match[1]捕获到 name
// 匹配指针后移
lastIndex = index + match[0].length;
}
// eg.结束后tokens = ['Hello, ', '_s(name)', '! Today is ', '_s(date)']
// 如果匹配完了花括号 text里面还有剩余的普通文本 那么继续push
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex))); // eg.还剩下'.',需要再放入tokens数组中
}
// tokens形如['xxx', '_s(yyy)']
return `_v(${tokens.join("+")})`; // 拼接返回
}
}
属性
接下来是属性的处理:
js
function genElement(el) {
let children = genChildren(el);
let code = `_c(${el.tag}${
el.attrs.length ? `,${genProps(el.attrs)}` : '' // attrs
}${
children ? `,${children}` : '' // children
})`;
return code
}
function genProps(attrs) {
let str = ""
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i]
// 对attrs属性里面的style做特殊处理
if (attr.name === "style") {
let obj = {};
attr.value.split(":").forEach((item) => {
let [key, value] = item.split(":")
obj[key] = value
})
attr.value = obj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
// 返回值形如:{class:"container",style:{color:"red",font-size:"14px"}}
return `{${str.slice(0, -1)}}`
}
以上简略介绍了Vue2是如何将AST树转化成渲染函数的,它们最终会得到形如_c('div', {'class':'container', style: {'color':'red','font-size':'14px'}}, [_v('xxx'+_s(yyy))])
的字符串并在generate()
中
js
return {
render: `with(this){return ${code}}`
}
进行包装。
结语
在这一章中我们通过简略的代码了解Vue2的编译时是如何将ast树转换为渲染函数的,在完成这一步之后,就可以到运行时去绘制页面了(当然我们省略了很多处理,实际使用中要处理<script>
的内容,打包时其实还要处理文件依赖......但主要目的是理解Vue2的运行逻辑)。
总结一下我们这章做的:
- 通过
generate()
、genElement()
处理ast节点 - 递归处理子节点
- 处理节点内容
- 处理
attrs
属性
下一篇,我们将进入Vue的运行时,了解在浏览器中的Vue2运行逻辑。
【参考】