Vue 模板

在Vue中,Vue模板对应的就是Vue中的 View(视图) 部分,也是Vue重中之一,而在Vue中要了解Vue模板我们就需要从两个方面来着手,其一是Vue的 模板语法 ,其二就是 模板渲染 。Vue模板语法是Vue中常用的技术之一,除非在应用程序中不用渲染视图或者你的程序直接采用的是 渲染函数 render() )。相较而言,模板语法较简单一点,但对于模板的渲染(模板编译)就会更为复杂一些,如果需要了解模板渲染就需要对Vue的渲染函数,响应式原理之类的要有所了解。当然,如果你跟我一样是初学者的话,建议你先花一点时间阅读一下下面几篇文章:

那咱们接下来先从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>

在布尔特性的情况下,它们的存在即暗示为 truev-bind 工作起来略有不同,比如:

<button v-bind:disabled="isButtonDisabled">Button</button>

如果 isButtonDisabled 的值是 nullundefinedfalse ,则 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-htmlv-oncev-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 ResigHTML 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( 虚拟DOMVirtual 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初始化 部分概括为将 eltemplaterender() 函数通过一系列的函数,比如 compileToFunctions()compile() 函数转换为 render 函数并最终生成真实DOM的过程;而 DOM更新 就是数据发生变化后,DOM进行更新的过程。结合生命周期的图和前面所掌握的知识点,我们现在来尝试着将Vue模板渲染过程用图绘制出来。

上图是根据自己阅读相关资料整理的,难免有错,欢迎路过的大婶拍正。

如果想正确的绘制出Vue模板渲染过程的路线图的话,还是需要去尝试阅读Vue的源码。这样会更清楚,更深层的知识点。扩展阅读:

小结

这篇文章整理了学习Vue模板的一些心得和笔记。除了介绍了模板语法相关的知识点之外,更多的是花了不少时间去理解Vue模板编译(渲染)的一个过程。根据相关的文档和自己的理解把模板渲染的过程绘制成了图。毕竟没有熟读源码,难免有错,欢迎路过的大神指正。或者您有这方面的经验,欢迎在下面的评论中一起分享。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章