好家伙,
在上一篇,我们已经成功将
我们的模板
转换为ast语法树
接下来我们继续进行操作
由于代码太多,为了增加代码的可阅读性
我们先将代码进行封装
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参数长什么样子?
首先我们确认,这个generare()方法是干什么的?
将ast语法树变成一个渲染函数
于是,我们来思考几个问题--
问一:为什么要使用generare()方法将ast语法树转换为渲染函数?
答一:在上图我们看见了,这个所谓的语法树其实更像是一个json而不是一个js,
而我们要将模板转换为可执行的JavaScript代码,才能跑
答一(官方一点的版本):
首先,将AST转换为渲染函数可以消除模板的解析和编译的开销。
在Vue的运行时版本中,没有编译器,所以将模板转换为渲染函数能够提高运行时的性能。
其次,渲染函数可以更高效地处理动态渲染。由于AST在编译时已经进行了一些静态分析,因此在渲染函数中可以更好地优化动态渲染和响应式更新的逻辑,减少运行时的开销。
最后,渲染函数的生成也是为了更好地支持vue组件的复用。
通过将模板转换为渲染函数,Vue可以更方便地缓存和复用这些函数,进一步提高组件的渲染性能。
总而言之,使用generate()
方法将AST转换为渲染函数是为了提高Vue应用的性能和效率,优化动态渲染和支持组件的复用。
我们想象一下
let code = generate(ast)复制
code会是什么样子的?
如果我们去翻源码,大概可以看到这步的结果长这样
然而实际上,我们的四步走: 模板解析 =》AST =》生成渲染函数 =》渲染到真实DOM
渲染函数的下一步是渲染到真实DOM
也就是要预留一个标记给"渲染到真实DOM"这一步做处理
_c 标签
_v 文本
_s 符号
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 }复制
genChildren()
函数获取子节点代码字符串。
generate.js完整代码
/** * <div id="app">Hello{{msg}}</div> * * _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"
,则将属性值解析为对象形式。
上述操作结束后,我们得到还是字符串,现在我们将其变成一个函数
let render = new Function(`with(this){return ${code}}`)复制
这里为什么要用with(this) ??
答:而with(this)
是将当前组件实例(即Vue组件的this
)作为上下文绑定到函数中,这样在渲染函数中就可以访问组件实例的属性和方法,例如访问组件的数据、计算属性或方法。
调试部分以及最终结果输出
好家伙, 1.使用场景 现在来,来想一下,作为一个使用Vue的开发者,假设现在我们要使用created(),我们会如何使用 1.1. .vue文件中使用 {{ message }}