学习Vue 2.0也有一段时间了,从前面的学习过程中,也知道在Vue中使用模板的基础知识。我们知道如何使用字符串插值在Vue中输出数据。其实在Vue的模板中,我们还可以做一些扩展,比如可以在字符串插值语法中使用简单的JavaScript表达式。之所以说简单的JavaScript表达式,是因为只能包含一个表达式,因此,不能使用循环或任何复杂的逻辑。不管怎么说,这样的逻辑不属于我们的模板,在Vue实例中放置一个方法会比较好。这我们后面会深入学习到这方面的知识点。
那么现在,我们系统的来学习一下Vue中的模板。
模板语法
Vue使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层Vue实例的数据。所有Vue的模板都是合法的HTML,所以能被遵循规范的浏览器和HTML解析器解析。
在底层的实现上,Vue将模板编译成虚拟DOM渲染函数。结合响应系统,在应用状态改变时,Vue能够智能地计算出重橷渲染组件的最小代价并应用到DOM操作上。
如果你熟悉虚拟DOM并且偏爱JavaScript的原始力量,你也可以不用模板,直接写渲染函数,使用可选的JSX语法。
比如,我们有这样一个简单的例子。假设我们的数据对象data
中有一个包含任意数字的age
属性。我们可以在Vue的模板中通过{{ }}
将age
插入进去,这个时候,在页面中就会渲染出age
的属性值:
<div id="app">
<h1>{{ age }}</h1>
</div>
let app = new Vue({
el: '#app',
data () {
return {
age: 27
}
}
})
效果如下所示:
这里我们使用了模板中最简单的文本插值方式,也是数据绑定最常见的形式。这种方法常称为双大括号(Mustache)语法。Mustache标签将会被替代为对应数据对象上age
属性的值。无论何时,绑定的数据对象上age
属性发生了改变,插值处的内容都会更新。
这种文本插值绑定数据虽然简单,但其也有一点的不足之处,那就是当你的页面渲染慢,或者你的JS失效时。页面会将{{ age }}
这样的字符渲染出来,给你的用户造成误解,有种不友好的用户体验。
前面提到过,文本插值绑定数据的方式,只要age
发生变化,那么页面渲染出来的数据就会发生变化。但也可以通过v-once
指令,让文本插值只执行一次,使用了这个指令,当数据改变时,插值处的内容并不会更新。但请留心这会影响到该节点上所有的数据绑定:
<div id="app">
<h1>未使用v-once指令:{{ age }}</h1>
<h1 v-once>使用v-once指令:{{ age }}</h1>
</div>
从上面的效果中可以看出,没有添加v-once
指令的文本插值会变化,添加了的则不会再有任何变化:
不管是出于什么原因,我想把这个数字乘以2
。在文本插值语法中我们可以在模板内直接添加一个简单的JavaScript表达式来实现:
<div id="app">
<h1>{{ age * 2 }}</h1>
</div>
这里只是将age
属性乘以2
,如果运行代码,我们就可以看到期望的数字输出,如上图所示。请记住,我们可以在模板中直接访问age
属性,因为Vue代理我们的数据属性,因此不必显式地访问数据对象上的属性。事实上,如果你试着这样做,它就不会起作用了。
接着让我们一起看一下如何在模板中使用布尔逻辑。由于只能使用单个表达式,因此不能使用正常的if
语句。然而,你可以做的是使用if
的简写语法,三元表达式。假设我们想根据data
中的age
值进行判断,超过60
输出“你老了”
,反之输出“你还年轻”
。那么我们可以在模板中这样做:
<div id="app">
<h1>{{ age > 60 ? '你老了' : '你还年轻' }}</h1>
</div>
运行上面的代码,你将看到下面这样的结果:
为了验证我们上面的说法是否正确,咱位可以在浏览器的控制台上修改age
的值,比如将age
的值修改成80
,你将看到的效果如下:
再来看一个表达式的例子。我将在data
中添加一个name
的属性,并且将我的全名Airen Liao
作为name
的值。
let app = new Vue({
el: '#app',
data () {
return {
age: 27,
name: 'Airen Liao'
}
}
})
data
中name
的值包含了我的第一个和最后一个名字,但是我只想在页面上显示我的第一个名字。我能做的就是通过split
方法把name
用空格分开。
<div id="app">
<h1>{{ name.split('') }}</h1>
</div>
上面的方法只是把name
中的值以空格分隔符将值以数组的形式输出,比如下图所示:
事实上并未得到我的第一名"Airen"
。我们在上面的基础上,添加一个数组的索引号0
,像这样:
<div id="app">
<h1>{{ name.split('')[0] }}</h1>
</div>
这样一来,得到期望得的效果:
这只是表达式的另一个例子。你可以用一个JavaScript表达式做很多事情,但要尽量保持简单。如果你需要更杂的逻辑,那么你就不应该尝试在模板中使用,比如嵌套的if
语句。另外,如果你需要在模板中多次使用相同的表达式,那么最好也不要将它嵌入到模板内,而更应该选择在Vue实例中使用。可以考虑使用Vue的方法来完成,我们后面会深入的学习这方面的知识。
除了使用文本插值{{}}
将数据值插入到模板之外,还可以考虑使用v-text
和v-html
这样的指令来插入数据。有关于v-text
和v-html
指令在Vue模板中的使用,可以阅读《Vue 2.0学习笔记:v-text
和v-html
》一文。这里不再做过多的阐述。
当然很多时候,还想在模板中根据一定的条件进行渲染,这个时候可以考虑使用v-if
或v-show
这样的指令来帮助大家。另外对于列表性的渲染,使用v-for
能帮我们省下不少的时间。
熟悉React的同学应该知道JSX,其实在Vue中也可以使用JSX。至于JSX是什么,不做过多阐述。
JSX就是一种对JavaScript的补充,用来描述组件的UI部分,类似模板语言,但它完整支持JavaScript本身的语法特性。 —— 关于JSX的介绍
JSX只是对JavaScript的补充并没有得到浏览器的支持,所以你需要用Babel搭配babel-preset-vue来获得完整的Vue JSX功能。
看一个简单的示例,如果我们使用render
函数,我们一般这样写:
// script.js file
new Vue({
el: '#app',
data: {
msg: 'Show the message'
},
methods: {
hello () {
alert('Here is the message')
}
},
render (createElement) {
return createElement(
'span',
{
class: { 'my-class': true },
on: {
click: this.hello
}
},
[ this.msg ]
);
},
});
<!-- index.html file -->
<div id="app">
<!--span will render here-->
</div>
换成JSX之后:
// script.js file
new Vue({
el: '#app',
data: {
msg: 'Show the message.'
},
methods: {
hello () {
alert('This is the message.')
}
},
render(h) {
return (
<span class={{ 'my-class': true }} on-click={ this.hello } >
{ this.msg }
</span>
)
}
});
<!-- index.html file -->
<div id="app">
<!--span will render here-->
</div>
有关于这方面的详细介绍,这里就不做过多的阐述,如果你对这方面东西感兴趣,可以阅读下面这些文章:
- Render Functions & JSX
- What’s New in Vue.js 2.0 - Beyond Templates With JSX
- Using JSX with Vue.js
- Writing Vue.js Render Functions in JSX
- Vue JSX 使用指南
- 用JSX写Vue组件
模板渲染
Vue 2.0的模板渲染借鉴了React的Virtual DOM。并且基于Virtual DOM,它还可以支持服务端渲染(SSR),也支持JSX语法。
在了解Vue的模板渲染方面的知识前,先上一张图:
从这张图中,我们可以初步看到一个Vue的应用是如何运行起来的,模板通过编译生成AST,再由AST生成Vue的渲染函数,渲染函数结合数据生成Virtual DOM树,对Virtual DOM进行diff
和patch
后生成新的UI。
我们要对一些相关的知识有所了解:
- Vue的模板
- AST数据结构
- VNode数据结构
- Virtual DOM
createElement
函数render
函数- 观察者(Watcher)
Vue的模板
前面提到过,Vue的模板基于纯HTML,基于Vue的模板语法,我们可以比较方便地声明数据和UI的关系
AST 数据结构
AST是Abstract Syntax Tree的简写,俗称抽象语法树,是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而Vue就是将模板代码映射为AST数据结构,进行语法解析。
Vue使用了HTML Parser将HTML模板解析为AST,并且对AST进行一些优化的标记处理,提取最大的静态树,方便Virtual Dom时直接跳过diff
。
VNode
VNode
可以理解为Vue的虚拟DOM的基类,通过new
实例化的VNode
大致可以分为:
EmptyVNode
: 没有内容的注释节点TextVNode
: 文本节点ElementVNode
: 普通元素节点ComponentVNode
: 组件节点CloneVNode
: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned
属性为true
一个VNode
的实例对象包含了以下属性:
tag
: 当前节点的标签名data
: 当前节点的数据对象children
: 数组类型,包含了当前节点的子节点text
: 当前节点的文本,一般文本节点或注释节点会有该属性elm
: 当前虚拟节点对应的真实的DOM节点ns
: 节点的namespace
context
: 编译作用域functionalContext
: 函数化组件的作用域key
: 节点的key
属性,用于作为节点的标识,有利于patch
的优化componentOptions
: 创建组件实例时会用到的选项信息child
: 当前节点对应的组件实例parent
: 组件的占位节点raw
: Raw HTMLisStatic
: 静态节点的标识isRootInsert
: 是否作为根节点插入,被<transition>
包裹的节点,该属性的值为false
isComment
: 当前节点是否是注释节点isCloned
: 当前节点是否为克隆节点isOnce
: 当前节点是否有v-once
指令
下面是 Vue 2.0 源码中 VNode 数据结构的定义
constructor {
this.tag = tag //元素标签
this.data = data //属性
this.children = children //子元素列表
this.text = text
this.elm = elm //对应的真实 DOM 元素
this.ns = undefined
this.context = context
this.functionalContext = 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
}
Virtual DOM
Virtual DOM树,Vue的Virtual DOM Patching算法是基于snabbdom实现的,并在此基础上作了很多的调整和改进。那么我们有真实的DOM,为什么要使用Virtual DOM。其中最大的原因就是document.createElement
这个方法创建的真实DOM元素会带来性能上的损失。而VNode
就是简化版的真实DOM元素,关联着真实的DOM,比如属性elm
,只包括我们需要的属性,并新增了一些在diff
过程中需要使用的属性,例如isStatic
。
createElement
函数
createElement
函数也经常被叫做h
函数,它被用来创建一个VNode
(虚拟DOM节点)。可以通过this.$createElement
访问它但同时它也是render
函数的第一个参数。
render
函数
这个函数是通过编译模板文件得到的,其运行结果是VNode。render
函数与JSX类似,Vue 2.0中除了Template也支持JSX的写法。大家可以使用Vue.compile(template)
方法编译下面这段模板。
<div id="app">
<header>
<h1>I am a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>
方法会返回一个对象,对象中有 render
和 staticRenderFns
两个值。看一下生成的 render
函数:
(function() {
with(this){
return _c(
// 创建一个 div 元素
'div',
{
attrs:{"id":"app"} //div 添加属性 id
},
[
// 静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
_m(0),
// 空的文本节点
_v(""),
// 三元表达式,判断 message 是否存在
// 如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
// 如果不存在,创建 p 元素,元素里面有文本,值为 No message.
(message) ? _c('p',[_v("\n "+_s(message)+"\n ")]) : _c('p',[_v("\n No message.\n ")])
]
)
}
})
要看懂上面的 render
函数,只需要了解 _c
,_m
,_v
,_s
这几个函数的定义,其中:
_c
是createElement
(创建元素)_m
是renderStatic
(渲染静态节点)_v
是createTextVNode
(创建文本DOM)_s
是toString
(转换为字符串)
除了 render
函数,还有一个 staticRenderFns
数组,这个数组中的函数与 VDOM 中的 diff
算法优化相关,我们会在编译阶段给后面不会发生变化的 VNode 节点打上 static
为 true
的标签,那些被标记为静态节点的 VNode 就会单独生成 staticRenderFns
函数:
// 上面 render 函数 中的 _m(0) 会调用这个方法
(function() {
with(this){
return _c('header',[_c('h1',[_v("I'm a template!")])])
}
})
其实render
函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中, Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
观察者 (Watcher)
每个Vue组件都有一个对应的Watcher,这个Watcher将会在组件render
的时候收集组件所依赖的数据,并在依赖有更新的时候,触发组件重新渲染。我们根本不需要写shouldComponentUpdate
,Vue就会自动优化并更新需要更新的UI
上图中,咱们可以以render
函数作为一道分割线,render
函数左边可以称之为编译期,将Vue的模板转换为渲染函数。render
函数的右边是Vue的运行时,主要是基于渲染函数生成Virtual DOM树,然后对Virtual Dom树进行diff
和patch
。
接下来再上一张Vue模板渲染过程的图:
Vue模板的渲染主要经历以下几个过程:
new Vue()
:实例化Vue$mount()
: 获取模板,并且在这过程中通过调用相关方法_count
(new Watcher()
实现数据响应式,当Watcher监听到数据变化,就会执行render
函数输出一个新的 VNode 树形结构的数据(VNode对象即Virtual DOM)compileToFunction()
: 将template
编译成render
函数。首先读缓存(在compileToFunction()
中,会创建一个对象,把complice
编译完后的对象的render
和staticRenderFns
两个属性分别转换成函数缓存在对象中,然后把对象存进缓存,没有缓存就调用compile
方法拿到render
函数的字符串形式,在通过new Function
的方式生成真正的渲染函数compile
:将template
编译成render
函数的字符串形式,这个函数主要有三个步骤组成:parse
,optimize
和generate
,最终输出一个包含ast
,render
和staticRenderFns
的对象。compile
函数主要是将template
转换为 AST,优化 AST,再将 AST 转换为render
函数字符串,render
函数与数据通过 Watcher 产生关联。update()
:update
判断是否首次渲染,是则直接创建真实DOM,否则调用patch()
,并且进行触发钩子和更新引用等其他操作patch()
:新旧 VNode 对比的diff
函数,对两个树结构进行完整的diff
和patch
的过程,最终只有发生了变化的节点才会被更新到真实 DOM 树上。destroy()
:完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发beforeDestroy
和destroyed
的钩子。在大多数场景中你不应该调用这个方法。最好使用v-if
和v-for
指令以数据驱动的方式控制子组件的生命周期。
有关于Vue模板的渲染,我也是看得云里来,雾里去。不过不要紧,对于初学者,咱们能整明白怎么使用Vue的模板,就行了。如果你想深入了解底层的渲染原理,可以阅读下面几篇文章:
总结
这篇文章从Vue模板使用入手,了解怎么在Vue中使用模板,然后一起学习了Vue模板渲染的相关知识。如果你和我一样是Vue的初学者,不必太过纠结是否能整明白模板的渲染的原理,我们首要的条件就是学会怎么在Vue中使用模板,得到我们想要的Web运用。
如果你在这方面有更多的经验,欢迎在下面的评论中与我们一起分享,如果文章有不对之处,还请各路大婶拍正。
如需转载,烦请注明出处:https://www.w3cplus.com/vue/vue-template.html