好家伙,
Vue源码学习(三):渲染第二步,创建ast语法树 ,
在上一篇,我们已经成功将
我们的模板
转换为ast语法树
接下来我们继续进行操作
1.方法封装
由于代码太多,为了增加代码的可阅读性
我们先将代码进行封装
index.js
import { generate } from "./generate"
import { parseHTML } from "./parseAst"
export function compileToFunction(el) {
//1. 将html元素变为ast语法树
let ast = parseHTML(el)
//2. ast语法树变成render函数
//(1) ast语法树变成字符串
//(2) 字符串变成函数
let code = generate(ast) // _c _v _s
console.log(code)
//3.将render字符串变成函数
let render = new Function(`with(this){return ${code}}`)
console.log(render,'this is render')
return render
}
今天我们将注意力集中在generate方法,
ast参数长什么样子?
2.渲染函数generate()作用
首先我们确认,这个generare()方法是干什么的?
将ast语法树变成一个渲染函数
于是,我们来思考几个问题--
问一:为什么要使用generare()方法将ast语法树转换为渲染函数?
答一:在上图我们看见了,这个所谓的语法树其实更像是一个json而不是一个js,
而我们要将模板转换为可执行的JavaScript代码,才能跑
答一(官方一点的版本):
首先,将AST转换为渲染函数可以消除模板的解析和编译的开销。
在Vue的运行时版本中,没有编译器,所以将模板转换为渲染函数能够提高运行时的性能。
其次,渲染函数可以更高效地处理动态渲染。由于AST在编译时已经进行了一些静态分析,因此在渲染函数中可以更好地优化动态渲染和响应式更新的逻辑,减少运行时的开销。
最后,渲染函数的生成也是为了更好地支持vue组件的复用。
通过将模板转换为渲染函数,Vue可以更方便地缓存和复用这些函数,进一步提高组件的渲染性能。
总而言之,使用generate()
方法将AST转换为渲染函数是为了提高Vue应用的性能和效率,优化动态渲染和支持组件的复用。
3.generate()方法得到结果
我们想象一下
let code = generate(ast)
code会是什么样子的?
如果我们去翻源码,大概可以看到这步的结果长这样
然而实际上,我们的四步走: 模板解析 =》AST =》生成渲染函数 =》渲染到真实DOM
渲染函数的下一步是渲染到真实DOM
也就是要预留一个标记给"渲染到真实DOM"这一步做处理
_c 标签
_v 文本
_s 符号
4.generate.js代码解析
3.1.generate()
函数是入口函数,接受一个 AST 语法书作为参数:
export function generate(el) {
console.log(el,'|this is el')
let children = genChildren(el)
console.log(children, "|this is children")
let code = `_c('${el.tag}',${el.attrs.length?`${genPorps(el.attrs)}`:'undefined'},${
children?`${children}`:''
})`
console.log(code, '|this is code')
return code
}
- (1) 调用
genChildren()
函数获取子节点代码字符串。 - (2) 拼接元素节点的标签名( genPoros()方法 )、属性和子节点代码,并返回生成的渲染函数代码。
generate.js完整代码
/**
* Hello{{msg}}
*
* _c 解析标签
* _v 解析字符串
*
* render(){
* return _c('div',{id:app},_v('hello'+_s(msg)),_c)
* }
*
*/
//处理属性
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
//genPorps()方法解析属性
function genPorps(attrs) {
// console.log(attrs)
let str = '';
//对象
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i]
if (attr.name === 'style') { //
let obj = {}
attr.value.split(';').forEach(item => {
let [key, val] = item.split(':')
// console.log(key, val, "//this is [key,val]")
obj[key] = val
})
attr.value = obj
}
//拼接
str += `${attr.name}:${JSON.stringify(attr.value)},`
// console.log(str, '|this is str')
// console.log(`{${str.slice(0,-1)}}`)
}
//首字符到倒数第二个字符,即去掉标点符号
return `{${str.slice(0,-1)}}`
}
//处理子节点
function genChildren(el) {
let children = el.children //获取元素节点的子节点
//如果存在子节点,则递归调用 gen() 函数处理每个子节点,并用逗号拼接子节点的代码。
if (children) {
//返回子节点代码的字符串。
return children.map(child => gen(child)).join(',')
}
}
//
function gen(node) { //1.元素 2.div tip:_v表示文本
// console.log(node, "this is node")
//如果节点是元素节点,递归调用 generate() 函数处理该节点,并返回结果。
if (node.type === 1) {
return generate(node)
} else { //文本
//(1) 只是文本 hello (2){{}}
let text = node.text //获取文本
//转化
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`
}
//(2)带插值表达式{{}}
//文本包含插值表达式,使用正则表达式 defaultTagRE
//查找所有 {{}} 形式的插值表达式,并解析成可执行的代码片段。
let tokens = []
//lastIndex 需要清零 否则test匹配会失败
let lastindex = defaultTagRE.lastIndex = 0
//match保存获取结果
let match
while (match = defaultTagRE.exec(text)) {
console.log(match, "|this is match")
let index = match.index
if (index > lastindex) {
tokens.push(JSON.stringify(text.slice(lastindex, index))) //内容
}
tokens.push(`_s(${match[1].trim()})`)
//lastindex处理文本长度
lastindex = index + match[0].length
}
//此处if用于处理`Hello{{msg}} xxx`中的xxx
if (lastindex < text.slice(lastindex)) {
tokens.push(JSON.stringify(text.slice(lastindex, index))) //内容
}
return `_v(${tokens.join('+')})`
}
}
export function generate(el) {
console.log(el,'|this is el')
let children = genChildren(el)
console.log(children, "|this is children")
let code = `_c('${el.tag}',${el.attrs.length?`${genPorps(el.attrs)}`:'undefined'},${
children?`${children}`:''
})`
console.log(code, '|this is code')
return code
}
(代码注释已十分完善)
方法解释(简化版本):
3.2.genChildren()
函数 处理子节点
- 获取元素节点的子节点。
- 如果存在子节点,则递归调用
gen()
函数处理每个子节点,并用逗号拼接子节点的代码。 - 返回子节点代码的字符串。
3.3. gen()
函数 根据节点类型生成代码:
- 如果节点是元素节点,递归调用
generate()
函数处理该节点,并返回结果。 - 如果节点是文本节点:返回处理后的节点代码。
- 如果文本不包含插值表达式
{{}}
,则使用_v()
方法将文本转换为可执行的字符串形式。 - 如果文本包含插值表达式,使用正则表达式
defaultTagRE
查找所有{{}}
形式的插值表达式,并解析成可执行的代码片段。
- 如果文本不包含插值表达式
- 返回处理后的节点代码。
3.4. genProps()
函数 解析属性:
- 遍历元素节点的所有属性,拼接属性名和属性值。
- 如果属性名是
"style"
,则将属性值解析为对象形式。 - 返回拼接后的属性字符串。
5.render字符串变成函数
上述操作结束后,我们得到还是字符串,现在我们将其变成一个函数
let render = new Function(`with(this){return ${code}}`)
这里为什么要用with(this) ??
答:而with(this)
是将当前组件实例(即Vue组件的this
)作为上下文绑定到函数中,这样在渲染函数中就可以访问组件实例的属性和方法,例如访问组件的数据、计算属性或方法。
调试部分以及最终结果输出