终于搞懂了!原来 Vue 3 的 generate 是这样生成 render 函数的
前言
在之前的 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令? 文章中讲了transform
阶段处理完v-for、v-model等指令后,会生成一棵javascript AST抽象语法树。这篇文章我们来接着讲generate
阶段是如何根据这棵javascript AST抽象语法树生成render函数字符串的,本文中使用的vue版本为3.4.19
。
看个demo
还是一样的套路,我们通过debug一个demo来搞清楚render函数字符串是如何生成的。demo代码如下:
<template>
<p>{{ msg }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("hello world");
</script>
上面这个demo很简单,使用p标签渲染一个msg响应式变量,变量的值为"hello world"。我们在浏览器中来看看这个demo生成的render函数是什么样的,代码如下:
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"p",
null,
_toDisplayString($setup.msg),
1
/* TEXT */
);
}
上面的render函数中使用了两个函数:openBlock
和createElementBlock
。在之前的 vue3早已具备抛弃虚拟DOM的能力了文章中我们已经讲过了这两个函数:
openBlock
的作用为初始化一个全局变量currentBlock
数组,用于收集dom树中的所有动态节点。createElementBlock
的作用为生成根节点p标签的虚拟DOM,然后将收集到的动态节点数组currentBlock
塞到根节点p标签的dynamicChildren
属性上。
render函数的生成其实很简单,经过transform
阶段处理后会生成一棵javascript AST抽象语法树
,这棵树的结构和要生成的render函数结构是一模一样的。所以在generate
函数中只需要递归遍历这棵树,进行字符串拼接就可以生成render函数啦!
关注公众号:【前端欧阳】,解锁我更多vue原理文章。
加我微信heavenyjj0012回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。
generate
函数
首先给generate
函数打个断点,generate
函数在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。
然后启动一个debug终端,在终端中执行yarn dev
(这里是以vite举例)。在浏览器中访问 http://localhost:5173/ ,此时断点就会走到generate
函数中了。在我们这个场景中简化后的generate
函数是下面这样的:
function generate(ast) {
const context = createCodegenContext();
const { push, indent, deindent } = context;
const preambleContext = context;
genModulePreamble(ast, preambleContext);
const functionName = `render`;
const args = ["_ctx", "_cache"];
args.push("$props", "$setup", "$data", "$options");
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
indent();
push(`return `);
genNode(ast.codegenNode, context);
deindent();
push(`}`);
return {
ast,
code: context.code,
};
}
generate
中主要分为四部分:
生成
context
上下文对象。执行
genModulePreamble
函数生成:import { xxx } from "vue";
生成render函数中的函数名称和参数,也就是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
生成render函数中return的内容
context
上下文对象
context
上下文对象是执行createCodegenContext
函数生成的,将断点走进createCodegenContext
函数。简化后的代码如下:
function createCodegenContext() {
const context = {
code: ``,
indentLevel: 0,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
indent() {
newline(++context.indentLevel);
},
deindent(withoutNewLine = false) {
if (withoutNewLine) {
--context.indentLevel;
} else {
newline(--context.indentLevel);
}
},
newline() {
newline(context.indentLevel);
},
};
function newline(n) {
context.push("\n" + ` `.repeat(n));
}
return context;
}
为了代码具有较强的可读性,我们一般都会使用换行和锁进。context
上下文中的这些属性和方法作用就是为了生成具有较强可读性的render函数。
code
属性:当前生成的render函数字符串。indentLevel
属性:当前的锁进级别,每个级别对应两个空格的锁进。helper
方法:返回render函数中使用到的vue包中export导出的函数名称,比如返回openBlock
、createElementBlock
等函数push
方法:向当前的render函数字符串后插入字符串code。indent
方法:插入换行符,并且增加一个锁进。deindent
方法:减少一个锁进,或者插入一个换行符并且减少一个锁进。newline
方法:插入换行符。
生成import {xxx} from "vue"
我们接着来看generate
函数中的第二部分,生成import {xxx} from "vue"
。将断点走进genModulePreamble
函数,在我们这个场景中简化后的genModulePreamble
函数代码如下:
function genModulePreamble(ast, context) {
const { push, newline, runtimeModuleName } = context;
if (ast.helpers.size) {
const helpers = Array.from(ast.helpers);
push(
`import { ${helpers
.map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(", ")} } from ${JSON.stringify(runtimeModuleName)}
`,
-1 /* End */
);
}
genHoists(ast.hoists, context);
newline();
push(`export `);
}
其中的ast.helpers
是在transform
阶段收集的需要从vue中import导入的函数,无需将vue中所有的函数都import导入。在debug终端看看helpers
数组中的值如下图:
从上图中可以看到需要从vue中import导入toDisplayString
、openBlock
、createElementBlock
这三个函数。
在执行push
方法之前我们先来看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时生成的render函数字符串还是一个空字符串,执行完push方法后,我们来看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数中已经有了import {xxx} from "vue"
了。
这里执行的genHoists
函数就是前面 搞懂 Vue 3 编译优化:静态提升的秘密文章中讲过的静态提升的入口。
生成render函数中的函数名称和参数
执行完genModulePreamble
函数后,已经生成了一条import {xxx} from "vue"
了。我们接着来看generate
函数中render函数的函数名称和参数是如何生成的,代码如下:
const functionName = `render`;
const args = ["_ctx", "_cache"];
args.push("$props", "$setup", "$data", "$options");
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
上面的代码很简单,都是执行push
方法向render函数中添加code字符串,其中args
数组就是render函数中的参数。我们在来看看执行完上面这块代码后的render函数字符串是什么样的,如下图:
从上图中可以看到此时已经生成了render函数中的函数名称和参数了。
生成render函数中return的内容
接着来看generate
函数中最后一块代码,如下:
indent();
push(`return `);
genNode(ast.codegenNode, context);
首先调用indent
方法插入一个换行符并且增加一个锁进,然后执行push
方法添加一个return
字符串。
接着以根节点的codegenNode
属性为参数执行genNode
函数生成return中的内容,在我们这个场景中genNode
函数简化后的代码如下:
function genNode(node, context) {
switch (node.type) {
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context);
break;
}
}
这里涉及到SIMPLE_EXPRESSION
、INTERPOLATION
和VNODE_CALL
三种AST抽象语法树node节点类型:
INTERPOLATION
:表示当前节点是双大括号节点,我们这个demo中就是:{{msg}}
这个文本节点。SIMPLE_EXPRESSION
:表示当前节点是简单表达式节点,在我们这个demo中就是双大括号节点{{msg}}
中的更里层节点msg
VNODE_CALL
:表示当前节点是虚拟节点,比如我们这里第一次调用genNode
函数传入的ast.codegenNode
(根节点的codegenNode
属性)就是虚拟节点。
genVNodeCall
函数
由于当前节点是虚拟节点,第一次进入genNode
函数时会执行genVNodeCall
函数。在我们这个场景中简化后的genVNodeCall
函数代码如下:
const OPEN_BLOCK = Symbol(`openBlock`);
const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`);
function genVNodeCall(node, context) {
const { push, helper } = context;
const { tag, props, children, patchFlag, dynamicProps, isBlock } = node;
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${``}), `);
}
const callHelper = CREATE_ELEMENT_BLOCK;
push(helper(callHelper) + `(`, -2 /* None */, node);
genNodeList(
// 将参数中的undefined转换成null
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
);
push(`)`);
if (isBlock) {
push(`)`);
}
}
首先判断当前节点是不是block节点,由于此时的node为根节点,所以isBlock
为true。将断点走进helper
方法,我们来看看helper(OPEN_BLOCK)
返回值是什么。helper
方法的代码如下:
const helperNameMap = {
[OPEN_BLOCK]: `openBlock`,
[CREATE_ELEMENT_BLOCK]: `createElementBlock`,
[TO_DISPLAY_STRING]: `toDisplayString`,
// ...省略
};
helper(key) {
return `_${helperNameMap[key]}`;
}
helper
方法中的代码很简单,这里的helper(OPEN_BLOCK)
返回的就是_openBlock
。
将断点走到第一个push
方法,代码如下:
push(`(${helper(OPEN_BLOCK)}(${``}), `);
执行完这个push
方法后在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到,此时render函数中增加了一个_openBlock
函数的调用。
将断点走到第二个push
方法,代码如下:
const callHelper = CREATE_ELEMENT_BLOCK;
push(helper(callHelper) + `(`, -2 /* None */, node);
同理helper(callHelper)
方法返回的是_createElementBlock
,执行完这个push
方法后在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到,此时render函数中增加了一个_createElementBlock
函数的调用。
继续将断点走到genNodeList
部分,代码如下:
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
);
其中的genNullableArgs
函数功能很简单,将参数中的undefined
转换成null
。比如此时的props
就是undefined
,经过genNullableArgs
函数处理后传给genNodeList
函数的props
就是null
。
genNodeList
函数
继续将断点走进genNodeList
函数,在我们这个场景中简化后的代码如下:
function genNodeList(nodes, context, multilines = false, comma = true) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (shared.isString(node)) {
push(node);
} else {
genNode(node, context);
}
if (i < nodes.length - 1) {
comma && push(", ");
}
}
}
我们先来看看此时的nodes
参数,如下图:
这里的nodes
就是调用genNodeList
函数时传的数组:[tag, props, children, patchFlag, dynamicProps]
,只是将数组中的undefined
转换成了null
。
nodes
数组中的第一项为字符串p,表示当前节点是p标签。由于当前p标签没有props,所以第二项为null的字符串。
第三项为p标签子节点:{{msg}}
第四项也是一个字符串,标记当前节点是否是动态节点。
在讲genNodeList
函数之前,我们先来看一下如何使用h
函数生成一个<p>{{ msg }}</p>
标签的虚拟DOM节点。根据vue官网的介绍,h
函数定义如下:
// 完整参数签名
function h(
type: string | Component,
props?: object | null,
children?: Children | Slot | Slots
): VNode
h
函数接收的第一个参数是标签名称或者一个组件,第二个参数是props对象或者null,第三个参数是子节点。
所以我们要使用h
函数生成demo中的p标签虚拟DOM节点代码如下:
h("p", null, msg)
h
函数生成虚拟DOM实际就是调用的createBaseVNode
函数,而我们这里的createElementBlock
函数生成虚拟DOM也是调用的createBaseVNode
函数。两者的区别是createElementBlock
函数多接收一些参数,比如patchFlag
和dynamicProps
。
现在我想你应该已经反应过来了,为什么调用genNodeList
函数时传入的第一个参数nodes
为:[tag, props, children, patchFlag, dynamicProps]
。这个数组的顺序就是调用createElementBlock
函数时传入的参数顺序。
所以在genNodeList
中会遍历nodes
数组生成调用createElementBlock
函数需要传入的参数。
先来看第一个参数tag
,这里tag
的值为字符串"p"。所以在for循环中会执行push(node)
,生成调用createElementBlock
函数的第一个参数"p"。在debug终端看看此时的render函数,如下图:
从上图中可以看到createElementBlock
函数的第一个参数"p"
接着来看nodes
数组中的第二个参数:props
,由于p标签中没有props
属性。所以第二个参数props
的值为字符串"null",在for循环中同样会执行push(node)
,生成调用createElementBlock
函数的第二个参数"null"。在debug终端看看此时的render函数,如下图:
从上图中可以看到createElementBlock
函数的第二个参数null
接着来看nodes
数组中的第三个参数:children
,由于children
是一个对象,所以以当前children节点作为参数执行genNode
函数。
这个genNode
函数前面已经执行过一次了,当时是以根节点的codegenNode
属性作为参数执行的。回顾一下genNode
函数的代码,如下:
function genNode(node, context) {
switch (node.type) {
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context);
break;
}
}
前面我们讲过了NodeTypes.INTERPOLATION
类型表示当前节点是双大括号节点,而我们这次执行genNode
函数传入的p标签children,刚好就是{{msg}}双大括号节点。所以代码会走到genInterpolation
函数中。
genInterpolation
函数
将断点走进genInterpolation
函数中,genInterpolation
代码如下:
function genInterpolation(node, context) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(`)`);
}
首先会执行push
方法向render函数中插入一个_toDisplayString
函数调用,在debug终端看看执行完这个push
方法后的render函数,如下图:
从上图中可以看到此时createElementBlock
函数的第三个参数只生成了一半,调用_toDisplayString
函数传入的参数还没生成。
接着会以node.content
作为参数执行genNode(node.content, context);
生成_toDisplayString
函数的参数,此时代码又走回了genNode
函数。
将断点再次走进genNode
函数,看看此时的node是什么样的,如下图:
从上图中可以看到此时的node节点是一个简单表达式节点,表达式为:$setup.msg
。所以代码会走进genExpression
函数。
genExpression
函数
接着将断点走进genExpression
函数中,genExpression
函数中的代码如下:
function genExpression(node, context) {
const { content, isStatic } = node;
context.push(
isStatic ? JSON.stringify(content) : content,
-3 /* Unknown */,
node
);
}
由于当前的msg
变量是一个ref
响应式变量,所以isStatic
为false
。所以会执行push
方法,将$setup.msg
插入到render函数中。
执行完push
方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数基本已经生成了,剩下的就是调用push
方法生成各个函数的右括号")"和右花括号"}"。将断点逐层走出,直到generate
函数中。代码如下:
function generate(ast) {
// ...省略
genNode(ast.codegenNode, context);
deindent();
push(`}`);
return {
ast,
code: context.code,
};
}
执行完最后一个 push
方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数终于生成啦!
总结
这是我画的我们这个场景中generate
生成render函数的流程图:
执行
genModulePreamble
函数生成:import { xxx } from "vue";
简单字符串拼接生成render函数中的函数名称和参数,也就是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
以根节点的
codegenNode
属性为参数调用genNode
函数生成render函数中return的内容。此时传入的是虚拟节点,执行
genVNodeCall
函数生成return _openBlock(), _createElementBlock(
和调用genNodeList
函数,生成createElementBlock
函数的参数。处理p标签的
tag
标签名和props
,生成createElementBlock
函数的第一个和第二个参数。此时render函数return的内容为:return _openBlock(), _createElementBlock("p", null
处理p标签的children也就是
{{msg}}
节点,再次调用genNode
函数。此时node节点类型为双大括号节点,调用genInterpolation
函数。在
genInterpolation
函数中会先调用push
方法,此时的render函数return的内容为:return _openBlock(), _createElementBlock("p", null, _toDisplayString(
。然后以node.content
为参数再次调用genNode
函数。node.content
为$setup.msg
,是一个简单表达式节点,所以在genNode
函数中会调用genExpression
函数。执行完genExpression
函数后,此时的render函数return的内容为:return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg
调用push方法生成各个函数的右括号")"和右花括号"}",生成最终的render函数
关注(图1)公众号:【前端欧阳】,解锁我更多vue原理文章。
加我(图2)微信回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。
终于搞懂了!原来 Vue 3 的 generate 是这样生成 render 函数的的更多相关文章
- 大白话Vue源码系列(03):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- 大白话Vue源码系列(04):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- 终于搞懂了vue 的 render 函数(一) -_-|||
终于搞懂了vue 的 render 函数(一) -_-|||:https://blog.csdn.net/sansan_7957/article/details/83014838 render: h ...
- vue入门:(底层渲染实现render函数、实例生命周期)
vue实例渲染的底层实现 vue实例生命周期 一.vue实例渲染的底层实现 1.1实例挂载 在vue中实例挂载有两种方法:第一种在实例化vue时以el属性实现,第二种是通过vue.$mount()方法 ...
- [转]我花了一个五一终于搞懂了OpenLDAP
轻型目录访问协议(英文:Lightweight Directory Access Protocol,缩写:LDAP)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的 ...
- 探索JAVA并发 - 终于搞懂了sleep/wait/notify/notifyAll
> sleep/wait/notify/notifyAll分别有什么作用?它们的区别是什么?wait时为什么要放在循环里而不能直接用if? ## 简介 首先对几个相关的方法做个简单解释,Obje ...
- 一字一句的搞懂vue-cli之vue webpack template配置
webpack--神一样的存在.无论写了多少次,再次相见,仍是初见.有的时候开发vue项目,对尤大的vue-cli感激涕零.但是,但是,但是...不是自己的东西,真的很不想折腾.所以,我们就得深入内部 ...
- 终于搞懂了PR曲线
PR(Precision Recall)曲线 问题 最近项目中遇到一个比较有意思的问题, 如下所示为: 图中的PR曲线很奇怪, 左边从1突然变到0. PR源码分析 为了搞清楚这个问题, 对源码进行了分 ...
- hdu1711(终于搞懂了KMP算法了。。)
题意:给你两个长度分别为n(1 <= N <= 1000000)和m(1 <= M <= 10000)的序列a[]和b[],求b[]序列在a[]序列中出现的首位置.如果没有请输 ...
- Lua的闭包详解(终于搞懂了)
词法定界:当一个函数内嵌套另一个函数的时候,内函数可以访问外部函数的局部变量,这种特征叫做词法定界 table.sort(names,functin (n1,n2) return grades[n1] ...
随机推荐
- 大咖齐聚!OpenHarmony技术峰会豪华嘉宾阵容揭晓
第一届开放原子开源基金会OpenHarmony技术峰会即将来袭 重量级嘉宾和行业大咖高能集结 展示OpenHarmony操作系统技术革新 1场主论坛.6场分论坛干货拉满 2月25日,一起解锁更多精 ...
- 【直播回顾】OpenHarmony知识赋能第八期:手把手教你实现涂鸦小游戏
OpenHarmony第八期知识赋能直播已经在9月29日圆满落幕!从9月15日起,资深OS框架开发工程师巴延兴老师于每周四进行分享,通过实现涂鸦小游戏来帮助大家全面了解ArkUI框架的应用,拓宽知识 ...
- 如何解决python安装mysqlclient失败问题
在使用Django等框架来操作MySQL时,实际上底层还是通过Python来操作的,首先需要安装一个驱动程序,在Python3中,驱动程序有多种选择,比如有pymysql以及mysqlclient等. ...
- redis+lua脚本实现接口限流
写在前面 在多线程的情况下对一个接口进行访问,如果访问次数过大,且没有缓存存在的情况下大量的请求打到数据库可能会存在数据库宕机,从而造成服务的不可用性.往往我们需要对其进行限流操作用来保证服务的高可用 ...
- 函数模板 及显式具体化(C++)
函数模板 将同一种算法应用与不同类型的函数时 #include<iostream> #include<string> template <typename T> v ...
- 什么是token,为什么需要token
1.为什么需要token 随着互联网的发展,为了更安全,以及更好的用户体验,逐渐产生了token这个技术方案 之所以使用token是因为http/https协议本身是无状态的,不能进行信息的存储 (c ...
- MySQL—一条查询SQL语句的完整执行流程
MySQL-一条查询SQL语句的完整执行流程 表结构和数据如下: 我们分析的sql语句如下: select tb_id,tb_name,tb_address from tb_user where tb ...
- 力扣442(java)-数组中重复的数据(中等)
题目: 给你一个长度为 n 的整数数组 nums ,其中 nums 的所有整数都在范围 [1, n] 内,且每个整数出现 一次 或 两次 .请你找出所有出现 两次 的整数,并以数组形式返回. 你必须设 ...
- Spring Boot 微服务性能下降九成!使用 Arthas 定位根因
简介: 接收到公司业务部门的开发反馈,应用在升级公司内部框架后,UAT(预生产)环境接口性能压测不达标. 背景 接收到公司业务部门的开发反馈,应用在升级公司内部框架后,UAT(预生产)环境接口性能压测 ...
- coredump 瘦身风云
简介: minicoredump神也! 继上一篇非典型程序员青囊搞定内存泄露问题后,美美地睡了一觉.睡梦中,突然金光闪闪,万道光芒照进时光隧道,恍惚来到大唐神龙年间.青囊此时化身狄仁杰高级助理, ...