其他章节请看:

vue 快速入门 系列

模板

前面提到 vue 中的虚拟 dom 主要做两件事:

  1. 提供与真实节点对应的​vNode​
  2. 新旧 vNode 对比,寻找差异,然后​更新视图​

①、vNode 从何而来?

前面也说了声明式框架只需要我们描述状态​与 dom​ 之间的映射关系。状态到视图的转换,框架会给我们做。

②、用什么描述状态与 dom 之间的映射关系?

​Tip​:jQuery 是命令式的框架,现代的 vue、react属于声明式框架。

简介

首先公布问题 ② 的答案:用​模板​描述状态与 dom 之间的映射关系。

于是我们知道这三者之间的关系:

graph LR 状态 --> 模板 --> dom

模板编译器

请先看一个模板的示例:

<span>Message: {{ msg }}</span>

<h1 v-if="awesome">Vue is awesome!</h1>

<ul id="example-1">
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>

v-if、v-for、{{}} 是什么?html 中根本不存在这些东西。

我们知道 javascript 代码只有 javascript 引擎认识,同理,模板也只有类似​模板引擎​的东西认识它。

在 vue 中,类似模板引擎的叫做​模板编译器​。通过模板编译器将模板编译成​渲染函数​,而执行渲染函数就会使用当前最新的状态生成一份 ​vnode。

graph LR 模板 -- 编译 --> 渲染函数 -- 执行 --> vNode

至此,问题 ① 的答案显而易见了,​vNode 由渲染函数生成。

模板和虚拟 dom 所处位置

我们根据上文,能轻易的知道模板所处位置:

flowchart LR 状态 --> 模板 subgraph a[模板] 模板 -- 编译 --> 渲染函数 -- 执行 --> vNode end vNode --> 视图

在 虚拟 dom 的作用​ 中,我们知道虚拟 dom 所处位置:

flowchart LR 状态 --> a subgraph a[虚拟 dom] vNode patch end a --> 视图

最后,我们将这两个图合并成一个即可:

flowchart LR 状态 --> 模板 subgraph a[模板] 模板 -- 编译 --> 渲染函数 end 渲染函数 -- 执行 --> b subgraph b[虚拟 dom] vNode patch end b --> 视图

​Tip​: 将渲染函数指向虚拟 dom,是因为 vue 官网有这么一句话:“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 ​VNode​ 树的称呼

模板是如何编译成渲染函数,以及为什么执行渲染函数就可以生成 vNode?请继续看下文。

渲染函数

将模板编译成​渲染函数​,只需要 3 步:

  1. ​解析器​:将HTML字符串转换为AST
  • AST 就是一个普通的 javascript 对象,描述了该节点的信息以及子节点的信息,类似 vNode
  1. ​优化器​:遍历 AST,标记静态节点,用于提高性能
  • <p>hello</p> 是静态节点,渲染之后不会再改变
  • <p>{{hello}}</p> 不是静态节点,因为状态会影响它
  1. ​生成器​:使用AST​ 生成渲染函数
  • 执行渲染函数就会根据现在的状态生成一份虚拟 dom(vNode)

为什么是这 3 步?​不重要​,这只是一种算法而已。

​Tip​:倘若我们能理解这 3 步确实能将模板编译成渲染函数,而渲染函数执行后能生成 vNode。那么 vue 中​模板​这一部分,也算是入门了。

分析

我们采用最直接的方法,即运行一段代码,看看 AST​ 是什么?优化器​做了什么?渲染函数​是什么?渲染函数又是如何生成 vNode 的?

代码很简单,一个 html 页面,里面引入 vue.js​,然后在 vue.js​ 中打上一个断点(输入 debugger),最后运行 test.html:

// test.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src='vue.js'></script>
</head>
<body>
<!-- 模板 -->
<div id='app'>
<p title='testTitle' @click='say'>number: {{num}}</p>
</div>
<!-- /模板 -->

<script>
const app = new Vue({
el: '#app',
data: {
num: 0
},
methods: {
say(){
this.num += 1;
}
}
})
</script>
</body>
</html>
// vue.js
// 打上断点(行{1})

var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
debugger // {1}
// 解析器
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
// 优化器
optimize(ast, options);
}
// 生成器
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});

AST

执行完 var ast = parse(template.trim(), options);,ast 为:

// ast:
{
"type":1,
"tag":"div",
"attrsList":[
{
"name":"id",
"value":"app"
}
],
"attrsMap":{
"id":"app"
},
"children":[
{
"type":1,
"tag":"p",
"attrsList":[
{
"name":"title",
"value":"testTitle"
},
{
"name":"@click",
"value":"say"
}
],
"attrsMap":{
"title":"testTitle",
"@click":"say"
},
"children":[
{
"type":2,
"expression":"'number: '+_s(num)",
"tokens":[
"number: ",
{
"@binding":"num"
}
],
"text":"number: {{num}}"
}
],
"plain":false,
"attrs":[
{
"name":"title",
"value":"testTitle"
}
],
"hasBindings":true,
"events":{
"click":{
"value":"say"
}
}
}
],
"plain":false,
"attrs":[
{
"name":"id",
"value":"app"
}
]
}

于是我们知道,AST​ 就是一个​普通的 javascript 对象​,类似虚拟节点或 dom Node,里面有节点的类型、属性、子节点等等。

优化器的作用

将 ast 交给优化器处理后(optimize(ast, options);​),ast 为:

// 优化器:(在上一步的基础上增加 static 和 staticRoot 两个属性)
{
"type":1,
"tag":"div",
"attrsList":[
{
"name":"id",
"value":"app"
}
],
"attrsMap":{
"id":"app"
},
"children":[
{
"type":1,
"tag":"p",
"attrsList":[
{
"name":"title",
"value":"testTitle"
},
{
"name":"@click",
"value":"say"
}
],
"attrsMap":{
"title":"testTitle",
"@click":"say"
},
"children":[
{
"type":2,
"expression":"'number: '+_s(num)",
"tokens":[
"number: ",
{
"@binding":"num"
}
],
"text":"number: {{num}}",
"static":false
}
],
"plain":false,
"attrs":[
{
"name":"title",
"value":"testTitle"
}
],
"hasBindings":true,
"events":{
"click":{
"value":"say"
}
},
"static":false,
"staticRoot":false
}
],
"plain":false,
"attrs":[
{
"name":"id",
"value":"app"
}
],
"static":false,
"staticRoot":false
}

优化器给 ast​ 增加 static​ 和 staticRoot 两个属性,用于标记静态节点。

生成器

接着将 ast​ 交给生成器处理(var code = generate(ast, options);​),code 为:

// code
{"render":"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('p',{attrs:{\"title\":\"testTitle\"},on:{\"click\":say}},[_v(\"number: \"+_s(num))])])}","staticRenderFns":[]}

将 code.render 字符串格式化:

// code.render
with(this) {
return _c(
'div',
{
attrs: {
"id": "app"
}
},
[
_c(
'p',
{
attrs: {
"title": "testTitle"
},
on: {
"click": say
}
},
[
_v("number: " + _s(num))
]
)
]
)
}

code.render 这个字符串导出到外界,会放到一个函数中,这个函数就是​渲染函数。

不理解?没关系,我们先看另一个示例:

new Function ([arg1[, arg2[, ...argN]],] functionBody)

const obj = {name: 'ph'}
const code = `with(this){console.log('hello: ' + name)}`
const renderFunction = new Function(code)
renderFunction.call(obj)

// 等同于

const obj = {name: 'ph'}
function renderFunction(){
with(this){console.log('hello: ' + name)}
}
renderFunction.call(obj) // hello: ph

这下理解了吧。我们将 code.render​ 指向的字符串导出到外界,外界利用 new Function() 创建渲染函数。

前面提到执行​渲染函数​会生成 vNode​。看看 code.render ​就能知晓,里面出现的 _v​ 和 _c,分别用于生成​元素类型​的 vNode 和​文本类型​的 vNode。请看相关源码:

// 创建文本类型的 vNode
target._v = createTextVNode;
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}

// 创建元素类型的 vNode
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
...
return _createElement(context, tag, data, children, normalizationType)
}

​Tip​: 关于 vue 中解析器、优化器和生成器里面具体是如何实现的,本系列就不展开了。

其他章节请看:

vue 快速入门 系列

作者:彭加李

欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。