Vue源码学习(四):<templete>渲染第三步,将ast语法树转换为渲染函数
好家伙,
Vue源码学习(三):<templete>渲染第二步,创建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完整代码
/**
* <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",则将属性值解析为对象形式。 - 返回拼接后的属性字符串。
5.render字符串变成函数
上述操作结束后,我们得到还是字符串,现在我们将其变成一个函数
let render = new Function(`with(this){return ${code}}`)
这里为什么要用with(this) ??
答:而with(this) 是将当前组件实例(即Vue组件的this)作为上下文绑定到函数中,这样在渲染函数中就可以访问组件实例的属性和方法,例如访问组件的数据、计算属性或方法。
调试部分以及最终结果输出

Vue源码学习(四):<templete>渲染第三步,将ast语法树转换为渲染函数的更多相关文章
- Vue源码学习1——Vue构造函数
Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...
- Vue源码学习三 ———— Vue构造函数包装
Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...
- Vue源码学习二 ———— Vue原型对象包装
Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...
- 【Vue源码学习】依赖收集
前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...
- 最新 Vue 源码学习笔记
最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...
- VUE 源码学习01 源码入口
VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...
- Vue 源码学习(1)
概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...
- Vue源码学习(一):调试环境搭建
最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...
- 【Vue源码学习】响应式原理探秘
最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...
- 【iScroll源码学习02】分解iScroll三个核心事件点
前言 最近两天看到很多的总结性发言,我想想今年好像我的变化挺大的,是不是该晚上来水一发呢?嗯,决定了,晚上来水一发! 上周六,我们简单模拟了下iScroll的实现,周日我们开始了学习iScroll的源 ...
随机推荐
- 聊聊MAUI、WinUI3和WPF的优势及劣势
今天在群里聊到WinUI3的学习及发展,还有他那堪比玩具的使用体验,正好梳理一篇关于WinUI3.MAUI和WPF优劣势,我整理的不是很好,所以又让ChatGPT在生成了一遍,感觉整体还可以.看完可以 ...
- Rust函数参数传递的一个观点
Q5: 一个函数的观点A5: Rust中的每个函数都是自治的,在每一个函数体中,相当于重新开辟了一个新的领域.将参数传递给函数参数,与let声明一个绑定是一样的规则. 1 ``` 2 // 所有权语义 ...
- Python随机数据生成——Faker的使用
安装Faker pip install faker 导入模块及基本配置 # 导入Faker from faker import Faker # 初始化,设置locale为中文:默认是英文 fake = ...
- Oracle将用户权限移植到另一个用户上
问题描述:往往有些需求,A用户依赖于B用户创建,A用户想要获取B用户的权限,oracle没找到有命令可以直接继承,只能写一些语句来代替 1.查询用户下的权限有哪些 SET PAGESIZE 100 S ...
- Python 爬虫实战:驾驭数据洪流,揭秘网页深处
爬虫,这个经常被人提到的词,是对数据收集过程的一种形象化描述.特别是在Python语言中,由于其丰富的库资源和良好的易用性,使得其成为编写爬虫的绝佳选择.本文将从基础知识开始,深入浅出地讲解Pytho ...
- 知识图谱之《海贼王-ONEPICE》领域图谱项目实战(含码源):数据采集、知识存储、知识抽取、知识计算、知识应用、图谱可视化、问答系统(KBQA)等
知识图谱之<海贼王-ONEPICE>领域图谱项目实战(含码源):数据采集.知识存储.知识抽取.知识计算.知识应用.图谱可视化.问答系统(KBQA)等 实体关系可视化页面可视化页面尝鲜 1. ...
- java查询sql动态查询需要的字段
方法一:使用"trim"标签. <select id="selTest" parameterType="mocha.framework.enti ...
- 平时容易忽视的地方之一:java在抽取方法时,什么时候该用void
当一个类中多个方法有相同编码,或该部分编码可以作为一个整体,适合抽取出一个方法时,要注意这个抽取的方法的返回值,什么时候可以用void,什么时候不能用void? 先看代码: import lombok ...
- 飞桨paddlespeech语音唤醒推理C定点实现
前面的文章(飞桨paddlespeech语音唤醒推理C浮点实现)讲了飞桨paddlespeech语音唤醒推理的C浮点实现.但是嵌入式设备通常CPU频率低和memory小,在嵌入式设备上要想流畅的运行语 ...
- Blazor前后端框架Known-V1.2.6
V1.2.6 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行. Gitee: https://gitee.com/known/Known Gith ...