渲染函数是 AST 到虚拟 DOM 节点的中间媒介,本质上就是 JS 的函数,执行后会基于『运行时』返回虚拟节点的对象。
在 Vue.js 2 中,通过执行「渲染函数」获得了虚拟 DOM 节点,用于虚拟节点 Diff 并最终生成真实 DOM。
1 | updateComponent = () => { |
上述3行源码中,调用的vm._render()即是「渲染函数」,其返回值即为「虚拟 DOM 节点」。
将虚拟 DOM 节点作为参数传给vm._update()后,就开始了著名的『虚拟 DOM Diff』。
generate
generate 会将 AST 转化成 render funtion 字符串,最终得到 render 的字符串以及 staticRenderFns 字符串。
render函数的就是返回一个_c(‘tagName’,data,children)的方法
- 第一个参数是标签名
- 第二个参数是他的一些数据,包括属性/指令/方法/表达式等等。
- 第三个参数是当前标签的子标签,同样的,每一个子标签的格式也是_c(‘tagName’,data,children)。
1 | function render() { |
generate就是通过不断递归形成了这么一种树形结构。

这里对后面会用到的方法以及函数做一个简单的介绍
genElement:用来生成基本的render结构或者叫createElement结构
genData: 处理ast结构上的一些属性,用来生成data
genChildren:处理ast的children,并在内部调用genElement,形成子元素的_c()方法
render字符串内部有几种方法:
- _c:对应的是 createElement 方法,顾名思义,它的含义是创建一个元素(Vnode)
- _v:创建一个文本结点。
- _s:把一个值转换为字符串。(eg: )
- _m:渲染静态内容
1 | <template> |
核心原理
1. 把字符串函数体转化为函数
写 JS 时,我们可以通过声明或表达式的形式创造函数。
但是在 JS 的执行过程中「创造函数」我们需要new Function() API,即JS中函数的构造函数。
通过调用函数的构造函数,我们可以将「字符串」类型的函数体,转化为一个可执行的JS函数:
1 | const func = new Function('console.log(`新函数`)') |
通过new Function()API,我们就拥有了在 JS 执行过程中生成函数体,并最终声明函数的能力。
2. 基于AST生成字符串格式的函数体
有了声明函数的能力,我们就可以把 AST 编译为「字符串格式的函数体」,再将之转化为可执行的函数。
例如,我们有一个<div />对应的 AST:
1 | { |
想要把 AST 编译为渲染函数的函数体:_c('div')。
我们只需要对 AST 进行遍历,根据tag属性就可以拼接出想要的函数体:
1 | function generate(ast) { |
如果 AST 的children属性不为空,我们继续对其进行深度优先递归搜索,就可继续增加渲染函数的函数体,最终生成各种复杂的渲染函数,渲染出复杂的 DOM,例如:
1 | const render = function () { |
如果有兴趣,可以找到自己项目中的
node_modules/vue-template-compiler/build.js第4815行:var code = generate(ast, options);加上console.log(code),npm run serve运行后,就可以在控制台中看到自己写的.vue文件编译出的渲染函数。
具体步骤
1. 增加CodeGenerator类及其调用
我们用CodeGenerator封装编译AST为渲染函数的逻辑,其带有一个generate(ast)方法,
传入 AST 作为参数,调用后会返回带有 render() 函数作为属性值的对象:
1 | class CodeGenerator { |
2. 编译 AST 中的父元素
我们再为类添加一个genElement方法,
这个方法接受一个 AST 节点,做2件事:
- 继续编译 AST 节点的子节点
children - 拼接字符串,将当前 AST 节点编译为渲染函数
1 | genElement(el) { |
genElement用于将AST:
1 | { |
编译为字符串函数体:_c('div')
3. 编译 AST 中的子元素
接下来我们编译子元素ast.children
children是一个数组,可能有多个子元素,所以我们需要对其进行.map()遍历,分别处理每一个子元素。
1 | genChildren (el, state) { |
我们再为类添加一个genElement方法,用于调用genChildren:
1 | genElement(el) { |
4. 分别处理每一个子元素
我们用genNode(node)方法处理子元素,
生产环境中,子元素有多种,可能是文本、注释、HTML元素,所以需要用if (node.type === 2)判断类型,在分情况处理。
1 | genNode(node) { |
我们此次需要处理的只有「文本」(node.type === 2)这一种,所以我们再增加一个genText(text)来处理。
1 | genText(text) { |
在编译 AST 阶段,我们已经把{{msg}}编译为了一个 JS 对象:
1 | { |
现在我们只要取expression属性,就是其对应的渲染函数。
简而言之_s()是 Vue.js 内置的一个方法,可以把传入的字符串生成一个对应的虚拟 DOM 节点。
后续我们将详细介绍_s(msg)的含义及其实现。
5. 拼接为字符串函数体、生成渲染函数
经过以上各步骤,我们已将 AST 对象解析成了渲染函数的函数体字符串:with(this){return _c('div',[_v(_s(msg))])},
为了将仍然是字符串函数体的render属性,转化为可执行的函数,我们再增加一段new Function(code)逻辑,
并把createFunction (code)声明到VueCompiler类,以便于最终调用:
1 | createFunction (code) { |
最后我们来统一调用。
在VueCompiler类的compile(template)中添加CodeGenerator实例及this.CodeGenerator.generate(ast)调用:
1 | class VueCompiler { |