在Vue中,Vue模板对应的就是Vue中的View(视图)部分,也是Vue重中之一,而在Vue中要了解Vue模板我们就需要从两个方面来着手,其一是Vue的模板语法,其二就是模板渲染。Vue模板语法是Vue中常用的技术之一,除非在应用程序中不用渲染视图或者你的程序直接采用的是渲染函数(render()
)。相较而言,模板语法较简单一点,但对于模板的渲染(模板编译)就会更为复杂一些,如果需要了解模板渲染就需要对Vue的渲染函数,响应式原理之类的要有所了解。当然,如果你跟我一样是初学者的话,建议你先花一点时间阅读一下下面几篇文章:
- Vue的模板
- Vue.js 定义组件模板的七种方式
- Vue的
render
函数 - 在Vue中如何用数据来驱动用户界面
- 从JavaScript属性描述器剖析Vue.js响应式视图
- Vue中的响应式
- 深入理解Vue.js响应式原理
- Vue的双向绑定原理及实现
- Vue双向绑定的实现原理
Object.defineproperty
- Vue2.0 源码阅读:响应式原理
那咱们接下来先从Vue模板语法开始入手,应该这部分相对来说较简单一点。
Vue模板语法
先来看一段最简单的代码:
<!-- App.vue -->
<template>
<div id="app">
{{ message }}
</div>
</template>
上面代码演示的仅仅Vue模板中的一种方式,也是最简单和最常见的一种模板方式。在Vue中除了上述这种方式之外还有其他几种方式,较为详细的可以阅读《Vue.js 定义组件模板的七种方式》一文。
这段代码具体的含义是什么暂不说。
在Vue中,模板语法是逻辑和视图之间的沟通桥梁,使用模板语法编写的HTML会响应Vue实例中的各种变化,简单地说,Vue实例中的逻辑可以随心所欲的渲染在页面上。正如上面的示例所示,如果我们Vue实例中逻辑让message
发生变化时,那么浏览器客户端就立即发生变化。
示例是一个最简单的模板语法,但其中有一个角色是最为重要,那就是插值(Mustache)标签,常用{{}}
符号来表示。Vue模板中插值常见的使用方法主要有:文本、原始HTML、属性、JavaScript表达式、指令和修饰符等。
文本
插值中最常见的就是文本插值,正如上面的示例中的{{ message }}
,该标签将会被替代为对应数据对象上message
属性的值。无论何时,绑定的数据对象上message
属性发生变化时,插值处的内容都会更新。
但也有一个额外的场景,那就是在模板语法中看是否使用了其他的指令,比如,在模板中要是使用了v-once
指令的话,那么该插值就是一次性地插值。也就是说,当数据改变时,插值处的内容不会更新。其使用如下所示:
<!-- App.vue -->
<template>
<div id="app">
<span v-once>{{ message }}</span>
</div>
</template>
原始HTML
插值语法中(也就是{{}}
)会将数据解释为普通文本,而非HTML代码,为了输出真正的HTML,需要使用v-html
指令,比如下面这个示例:
<!-- App.vue -->
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<div>{{rawHTML}}</div>
<div v-html="rawHTML"></div>
</div>
</template>
<script>
export default {
name: 'app',
data () {
return {
rawHTML: '<span style="color:red;">原始HTML</span>'
}
}
}
</script>
效果如下:
注意:不能使用
v-html
来复合局部模板,因为Vue不是基于字符串的模板引擎。另外动态渲染任意的HTML会有一定的危险,因为它很容易导致XSS攻击。
属性
插值语法不能作用在HTML元素的属性上,遇到这种情形需要使用v-bind
指令:
<div v-bind:id="dynamicId"></div>
在布尔特性的情况下,它们的存在即暗示为true
,v-bind
工作起来略有不同,比如:
<button v-bind:disabled="isButtonDisabled">Button</button>
如果 isButtonDisabled
的值是 null
、undefined
或 false
,则 disabled
特性甚至不会被包含在渲染出来的 <button>
元素中。
JavaScript表达式
在插值语法中,我们还可以使用JavaScript的表达式,比如:
{{number + 1}}
{{ ok ? 'Yes' : 'No'}}
<div v-bind:id="'list-' + id">
这些表达式会在所属Vue实例的数据作用域下作为JavaScript被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效:
<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}
指令
在Vue中有不少内置的指令,常常以v-
前缀的特殊特性,比如前面看到的v-html
、v-once
、v-bind
等。Vue指令的特性的值预期是单个JavaScript表达式。Vue指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于DOM。比如下面这个v-if
示例:
<p v-if="seen">现在你看到我了</p>
这里,v-if
指令将根据表达式seen
的值的真假来插入或移除<p>
元素。
在Vue中一些指令可以接收一个参数,在指令名称之后以冒号(:
)表示,比如前面提到的v-bind
指令:
<a v-bind:href="url">我是一个链接</a>
这里的href
是参数,告诉v-bind
指令将该元素的href
属性与表达式url
的值绑定。另外在V2.6开始,可以用方括号([]
)绑定一个动态参数,比如:
<a v-bind:[attributeName]="url">我是一个链接</a>
上面代码中的attributeName
会被作为一个JavaScript表达式进行动态求值,求得的值将会作为最终的参数来使用。比如,在Vue实例中有一个data
属性attributeName
,其值为"href"
,那么这个绑写下将等价于v-bind:href
。同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
同样地,当 eventName
的值为 "focus"
时,v-on:[eventName]
将等价于 v-on:focus
。
当然,在使用动态参数时有一些约束,比如:
- 对动态参数的值的约束:动态参数预期会求出一个字符串,异常情况下值为
null
。这个特殊的null
值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。 - 对动态参数表达式的约束:动态参数表达式有一些语法约束,因为某些字符,例如空格和引号,放在 HTML 特性名里是无效的。
使用Vue指令的时候,还可以采用缩写的方式,比如:
<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
它们看起来可能与普通的 HTML 略有不同,但 :
与 @
对于特性名来说都是合法字符,在所有支持 Vue 的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。
事实上,在Vue中的指令也有不少,最常见的以及其相应的使用方法可以阅读下面相关教程:
上面列的都是Vue内置的一些常见指令,除了内置的指令之外,在Vue中可以根据其相应的机制实现一些自定义的指令,比如这两篇文章中介绍的内容《自定义指令》、《Vue 自定义指令的魅力》。
修饰符
Vue中的指令后面还可以紧跟一个.
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。比如,.prevent
修饰符告诉v-on
指令对于触发的事件调用event.preventDefault()
:
<form v-on:submit.prevent="onSubmit">...</form>
上面我们仅仅是Vue中模板语法中面上的一些东东,很多同学对上面了解或掌握已经非常的熟悉了。当然也有部分同学和我类似,希望能知道一些更深的东西,比如模板渲染更深层的东西。接下来咱们尝试一起来尝试了解一下这方面的的知识点。
模板渲染
Vue的模板渲染相对而言要更为复杂,涉及更多底层的知识,如果你能熟读Vue源码,应该更易于理解。如果你和我一样对于源码阅读还有一定的难度,那么我们可以先从别的方面着手,了解模板渲染的一些基本原理。这样一来,就能更清楚Vue的模板是如何工作的,简单地说,就是如何渲染(也就是模板编译)。
在深入了解Vue模板渲染之前,有几个基础概念需要先进行了解:AST数据结构、VNode数据结构、createElement
的问题和渲染函数。
AST数据结构
AST是Abstract Syntax Tree首字母的简写,即抽象语法树的意思。是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而Vue源码中借鉴的是@John Resig的HTML Parrser对模板进行解析,得到的就是AST代码。
将
<template>
转换成抽象语法树(AST)。
其中AST是解析器中一个非常重要的概念。在Vue中,ASTNode主要分为:ASTElement(元素)、ASTText(文本)和ASTExpression(表达式)。用type
属性区分。
用一个简单的示例来做个简单的阐述:
<div id="app">
<h1>W3cplus.com</h1>
<p>{{ 1 + 1 }}</p>
</div>
这段代码生成的AST如下:
看上去是不是和DOM树有点类似。
VNode数据结构
VNode是VDOM(虚拟DOM(Virtual DOM))中的概念,是真实DOM元素的简化版,与真实DOM元素是一一对应的关系。
Vue 2.6中VNode数据结构的定义大致像下面这样:
{
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
Vue中的渲染函数的生成跟这些属性相关。
document.createElement
的问题
我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document.createElement
这个方法创建的真实 DOM 元素会带来性能上的损失。
let div = document.createElement('div');
for(let k in div) {
console.log(k);
}
document.createElement
创建的元素其属性多达 228
个(包含原型链上的属性),而这些属性有 90%
多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,关联着真实的DOM,比如属性elm
,只包括我们需要的属性,新增了一些在 diff
过程中需要使用的属性,例如 isStatic
,就可以用来对比新旧 DOM。这也是为什么要使用虚拟DOM的原因!
如果你想扩展或深入了解有关于虚拟DOM相关的内容,可以阅读下面这些文章。
渲染函数
render()
即是渲染函数,这个函数是通过编译模板文件得到的,其运行结果是VNode(虚拟DOM)。在Vue中使用 Vue.compile(template)
方法对模板进行编译。主要会经历三个步骤:
- 第一步是将 模板字符串 转换成 element ASTs(解析器)
- 第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
- 第三步是 使用 element ASTs 生成
render
函数代码字符串(代码生成器)
其对应的其实就是三个函数:
parse()
函数:用来解析<template>
,即解析器。主要功能是将template
字符串解析成 AST。前面定义了ASTElement
的数据结构,parse
函数就是将template
里的结构(指令,属性,标签等)转换为AST形式存进ASTElement
中,最后解析生成AST。optimize()
函数:用来优化静态内容,即优化器。主要功能就是标记静态节点,为后面patch
过程中对比新旧 VNode 树形结构做优化。被标记为static
的节点在后面的diff
算法中会被直接忽略,不做详细的比较。这里的静态内容指的是 和数据没有关系,不需要每次都刷新的内容。generate()
函数:用来创建render()
字符串,即代码生成器。主要功能就是根据 AST 结构拼接生成render
函数的字符串。
这三个函数也是compile()
函数中三个核心部分,根据每个步骤所起的作用,绘制了一张草图:
有了上面这些知识点,继续探究模板渲染的过程就会更易于理解了。
如果结合我们上一节学习的Vue实例的生命周期图,那么模板渲染最重要的两个过程将是 DOM初始化和 DOM的更新。
简单地说,模板中的 DOM初始化部分概括为将el
、template
和render()
函数通过一系列的函数,比如compileToFunctions()
和compile()
函数转换为render
函数并最终生成真实DOM的过程;而 DOM更新就是数据发生变化后,DOM进行更新的过程。结合生命周期的图和前面所掌握的知识点,我们现在来尝试着将Vue模板渲染过程用图绘制出来。
上图是根据自己阅读相关资料整理的,难免有错,欢迎路过的大婶拍正。
如果想正确的绘制出Vue模板渲染过程的路线图的话,还是需要去尝试阅读Vue的源码。这样会更清楚,更深层的知识点。扩展阅读:
小结
这篇文章整理了学习Vue模板的一些心得和笔记。除了介绍了模板语法相关的知识点之外,更多的是花了不少时间去理解Vue模板编译(渲染)的一个过程。根据相关的文档和自己的理解把模板渲染的过程绘制成了图。毕竟没有熟读源码,难免有错,欢迎路过的大神指正。或者您有这方面的经验,欢迎在下面的评论中一起分享。