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