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的源 ...
随机推荐
- JS逆向实战14——猿人学第二题动态cookie
声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 目标网站 https:// ...
- odoo开发教程十:Actions
actions定义了系统对于用户的操作的响应:登录.按钮.选择项目等. 一:窗口action(ir.actions.act_window ) 最常用的action类型,用于将model的数据展示出来. ...
- linux 递归和函数实验
递归 作用:自己调用自己 1.例子:阶乘 2.遍历目录下所有文件 函数 1.函数能够接受一个参数,参数为用户名: 判断一个用户是否存在 如果存在,就返回此用户的shell 和 UID :并返回正常状态 ...
- Kubernetes(k8s)定时任务:CronJob
目录 一.系统环境 二.前言 三.Kubernetes CronJob简介 四.kubernetes CronJob和Linux crontab对比 五.CronJob表达式语法 六.创建CronJo ...
- Kubernetes(k8s)包管理工具Helm:Helm包管理
目录 一.系统环境 二.前言 三.包管理工具Helm简介 四.安装部署helm 五.配置helm以及helm常用命令 六.使用helm安装应用 七.搭建helm私有仓库 八.总结 一.系统环境 本文主 ...
- ChatGPT的ABAP能力如何?
ChatGPT是最近的热门话题,作为语言模型,它擅长处理各种语言相关的问题.显然,ABAP也是一种语言,ABAP开发者的很大一部分工作就是把自然语言和ABAP语言做互相转换,这应该也是在ChatGPT ...
- [ARM 汇编]高级部分—性能优化与调试—3.4.3 使用模拟器进行调试与测试
在ARM汇编程序开发过程中,使用模拟器(emulator)进行调试和测试是一种非常有效的方法.模拟器可以在不同的处理器上测试代码,帮助我们发现潜在的问题,并提供丰富的调试功能.本节将介绍如何使用QEM ...
- 力扣 662 https://leetcode.cn/problems/maximum-width-of-binary-tree/
需要了解树的顺序存储 如果是普通的二叉树 ,底层是用链表去连接的 如果是满二叉树,底层用的是数组去放的,而数组放的时候 会有索引对应 当前父节点是索引i,下一个左右节点就是2i,2i+1 利用满二叉树 ...
- allure报告生成
allure生成测试报告 1.生成xml文件 pytest 测试文件所在路径 --alluredir 生成的测试结果数据保存的目录 pytest --alluredir=resport/xml/ D ...
- 本地数据local storage和session storage
随着互联网的快速发展,基于网页的应用越来越普遍,同时也变的越来越复杂,为了满足各种各样的需求,会经常性在本地存储大量的数据, HTML5规范提出了相关解决方案. 本地存储特性 1.数据存储在用户浏览器 ...