在上一节《一、template 字符串编译为抽象语法树 AST》中,我们实现了template
渲染为AST
的逻辑,距离最终目标「渲染为真实DOM」更近了一步!
这一节,我们来继续实现 AST 编译为渲染函数 render() 。
本节目标
- 将抽象语法树 AST 编译为渲染函数
render()
完整代码:在线示例:抽象语法树 AST 编译为渲染函数render() - JS Bin
也就是将上一节我们编译出来的 AST 对象:
{
"type": 1,
"tag": "div",
"children": [
{
"type": 2,
"expression": "_s(msg)",
"tokens": [
{
"@binding": "msg"
}
],
"text": "{{msg}}"
}
]
}
复制代码
编译为渲染函数render()
:
function render() {
with(this) {
return _c('div',[_v(_s(msg))])
}
}
复制代码
暂时不必理解渲染函数的含义,后续我们会深入了解。
什么是渲染函数render()
?
渲染函数是 AST 到虚拟 DOM 节点的中间媒介,本质上就是 JS 的函数,执行后会基于『运行时』返回虚拟节点的对象。
在 Vue.js 2 中,通过执行「渲染函数」获得了虚拟 DOM 节点,用于虚拟节点 Diff 并最终生成真实 DOM。
Vue.js 源码链接:lifecycle.js#L189-L191
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
复制代码
上述3行源码中,调用的vm._render()
即是「渲染函数」,其返回值即为「虚拟 DOM 节点」。
将虚拟 DOM 节点作为参数传给vm._update()
后,就开始了著名的『虚拟 DOM Diff』。
核心原理
1. 把字符串函数体转化为函数
写 JS 时,我们可以通过声明
或表达式
的形式创造函数。
但是在 JS 的执行过程中「创造函数」我们需要new Function()
API,即JS中函数的构造函数。
通过调用函数的构造函数,我们可以将「字符串」类型的函数体,转化为一个可执行的JS函数:
const func = new Function('console.log(`新函数`)')
/*
func ===
ƒ anonymous() {
console.log(`新函数`)
}
*/
func() // 打印 `新函数`
复制代码
通过new Function()
API,我们就拥有了在 JS 执行过程中生成函数体,并最终声明函数的能力。
2. 基于AST生成字符串格式的函数体
有了声明函数的能力,我们就可以把 AST 编译为「字符串格式的函数体」,再将之转化为可执行的函数。
例如,我们有一个<div />
对应的 AST:
{
"type": 1,
"tag": "div",
"children": [],
}
复制代码
想要把 AST 编译为渲染函数的函数体:_c('div')
。
我们只需要对 AST 进行遍历,根据tag
属性就可以拼接出想要的函数体:
function generate(ast) {
if (ast.tag) {
return `_c('${ast.tag}')`
}
}
复制代码
如果 AST 的children
属性不为空,我们继续对其进行深度优先递归搜索,就可继续增加渲染函数的函数体,最终生成各种复杂的渲染函数,渲染出复杂的 DOM,例如:
const render = function () {
with (this) {
return _c(
'div', {attrs: {"id": "app"}},
[
_c('h1', [_v("Hello vue-template-babel-compiler")]),
_v(" "),
(optional?.chaining)
? _c('h2', [_v("\\n Optional Chaining enabled: " + _s(optional?.chaining) + "\\n ")])
: _e()
]
)
}
}
复制代码
如果有兴趣,可以找到自己项目中的
node_modules/vue-template-compiler/build.js
第4815行:var code = generate(ast, options);
加上console.log(code)
,npm run serve
运行后,就可以在控制台中看到自己写的.vue
文件编译出的渲染函数。
具体步骤
这次的代码逻辑更加简单,总共只需要写 41 行代码。
1. 增加CodeGenerator
类及其调用
我们用CodeGenerator
封装编译AST为渲染函数的逻辑,其带有一个generate(ast)
方法,
传入 AST 作为参数,调用后会返回带有 render() 函数作为属性值的对象:
class CodeGenerator {
generate(ast) {
debugger
var code = this.genElement(ast)
return {
render: ("with(this){return " + code + "}"),
}
}
}
复制代码
拼接render
时的with(this) {}
有什么用?
with(this)
关键字就是 Vue.js 单文件组件(.vue 文件,SFC)中不用写this
关键字,就能渲染出this.msg
的秘密。
通过在渲染函数中使用with(this)
关键字,可以把this
作为其中作用域的全局变量(类似于window, global
),{}
花括号内的变量都会直接取this
对应的属性。
例如:
with (Math) {
val = random()
}
console.log(val) // 调用Math.random()的返回值
复制代码
2. 编译 AST 中的父元素
我们再为类添加一个genElement
方法,
这个方法接受一个 AST 节点,做2件事:
- 继续编译 AST 节点的子节点
children
- 拼接字符串,将当前 AST 节点编译为渲染函数
genElement(el) {
var children = this.genChildren(el)
const code = `_c('${el.tag}'${children ? `,${children}` : ''})`
return code
}
复制代码
genElement
用于将AST:
{
"type": 1,
"tag": "div",
"children": [],
}
复制代码
编译为字符串函数体:_c('div')
3. 编译 AST 中的子元素
接下来我们编译子元素ast.children
children
是一个数组,可能有多个子元素,所以我们需要对其进行.map()
遍历,分别处理每一个子元素。
genChildren (el, state) {
var children = el.children
if (children.length) {
return `[${children.map(c => this.genNode(c, state)).join(',')}]`
}
}
复制代码
我们再为类添加一个genElement
方法,用于调用genChildren
:
genElement(el) {
debugger
var children = this.genChildren(el)
const code = `_c('${el.tag}'${children ? `,${children}` : ''})`
return code
}
复制代码
4. 分别处理每一个子元素
我们用genNode(node)
方法处理子元素,
生产环境中,子元素有多种,可能是文本、注释、HTML元素,所以需要用if (node.type === 2)
判断类型,在分情况处理。
genNode(node) {
if (node.type === 2) {
return this.genText(node)
}
// TODO else if (node.type === otherType) {}
}
复制代码
我们此次需要处理的只有「文本」(node.type === 2
)这一种,所以我们再增加一个genText(text)
来处理。
genText(text) {
return `_v(${text.expression})`
}
复制代码
在编译 AST 阶段,我们已经把{{msg}}
编译为了一个 JS 对象:
{
"type": 2,
"expression": "_s(msg)",
"tokens": [
{
"@binding": "msg"
}
],
"text": "{{msg}}"
}
复制代码
现在我们只要取expression
属性,就是其对应的渲染函数。
简而言之_s()
是 Vue.js 内置的一个方法,可以把传入的字符串生成一个对应的虚拟 DOM 节点。
后续我们将详细介绍_s(msg)
的含义及其实现。
5. 拼接为字符串函数体、生成渲染函数
经过以上各步骤,我们已将 AST 对象解析成了渲染函数的函数体字符串:with(this){return _c('div',[_v(_s(msg))])}
,
为了将仍然是字符串函数体的render
属性,转化为可执行的函数,我们再增加一段new Function(code)
逻辑,
并把createFunction (code)
声明到VueCompiler
类,以便于最终调用:
createFunction (code) {
try {
return new Function(code)
} catch (err) {
throw err
}
}
复制代码
最后我们来统一调用。
在VueCompiler
类的compile(template)
中添加CodeGenerator
实例及this.CodeGenerator.generate(ast)
调用:
class VueCompiler {
HTMLParser = new HTMLParser()
CodeGenerator = new CodeGenerator()
compile(template) {
const ast = this.parse(template)
console.log(`一、《template 字符串编译为抽象语法树 AST》`)
console.log(`ast = ${JSON.stringify(ast, null, 2)}`)
const code = this.CodeGenerator.generate(ast)
const render = this.createFunction(code.render)
console.log(`二、《抽象语法树 AST 编译为渲染函数 render()》`)
console.log(`render() = ${render}`)
return render
}
}
复制代码
基于我们前一节已经写好的this.compiler.compile(this.options.template)
,最终我们就能看到控制台打印出来的渲染函数render() =
:
完整代码:在线示例:抽象语法树 AST 编译为渲染函数render() - JS Bin
《8分钟学会 Vue.js 原理》系列,共计5部分:
- 一、template 字符串编译为抽象语法树 AST
- 二、AST 编译 render() 实现原理
- 三、执行渲染函数 render() 生成虚拟节点 vnode
- 四、虚拟节点 vnode 生成真实DOM
- 五、数据驱动 DOM 更新 - Watcher Observer 和 Dep
正在热火朝天更新中,欢迎交流~ 欢迎催更~