前端开发者学堂 - fedev.cn

Vue 2.0学习笔记:Vue的模板

发布于 大漠

学习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'
        }
    }
})

dataname的值包含了我的第一个和最后一个名字,但是我只想在页面上显示我的第一个名字。我能做的就是通过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-textv-html这样的指令来插入数据。有关于v-textv-html指令在Vue模板中的使用,可以阅读《Vue 2.0学习笔记:v-textv-html》一文。这里不再做过多的阐述。

当然很多时候,还想在模板中根据一定的条件进行渲染,这个时候可以考虑使用v-ifv-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>

有关于这方面的详细介绍,这里就不做过多的阐述,如果你对这方面东西感兴趣,可以阅读下面这些文章:

模板渲染

Vue 2.0的模板渲染借鉴了React的Virtual DOM。并且基于Virtual DOM,它还可以支持服务端渲染(SSR),也支持JSX语法。

在了解Vue的模板渲染方面的知识前,先上一张图:

从这张图中,我们可以初步看到一个Vue的应用是如何运行起来的,模板通过编译生成AST,再由AST生成Vue的渲染函数,渲染函数结合数据生成Virtual DOM树,对Virtual DOM进行diffpatch后生成新的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 HTML
  • isStatic: 静态节点的标识
  • 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>

方法会返回一个对象,对象中有 renderstaticRenderFns 两个值。看一下生成的 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 这几个函数的定义,其中:

  • _ccreateElement(创建元素)
  • _mrenderStatic(渲染静态节点)
  • _vcreateTextVNode(创建文本DOM)
  • _stoString (转换为字符串)

除了 render 函数,还有一个 staticRenderFns 数组,这个数组中的函数与 VDOM 中的 diff 算法优化相关,我们会在编译阶段给后面不会发生变化的 VNode 节点打上 statictrue 的标签,那些被标记为静态节点的 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树进行diffpatch

接下来再上一张Vue模板渲染过程的图:

Vue模板的渲染主要经历以下几个过程:

  • new Vue():实例化Vue
  • $mount(): 获取模板,并且在这过程中通过调用相关方法_countnew Watcher()实现数据响应式,当Watcher监听到数据变化,就会执行render函数输出一个新的 VNode 树形结构的数据(VNode对象即Virtual DOM)
  • compileToFunction(): 将 template 编译成 render 函数。首先读缓存(在compileToFunction()中,会创建一个对象,把complice编译完后的对象的renderstaticRenderFns 两个属性分别转换成函数缓存在对象中,然后把对象存进缓存,没有缓存就调用 compile 方法拿到 render 函数的字符串形式,在通过new Function 的方式生成真正的渲染函数
  • compile:将 template 编译成 render 函数的字符串形式,这个函数主要有三个步骤组成:parseoptimizegenerate,最终输出一个包含 astrenderstaticRenderFns的对象。compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为 render函数字符串,render 函数与数据通过 Watcher 产生关联。
  • update()update判断是否首次渲染,是则直接创建真实DOM,否则调用patch(),并且进行触发钩子和更新引用等其他操作
  • patch():新旧 VNode 对比的 diff 函数,对两个树结构进行完整的diffpatch的过程,最终只有发生了变化的节点才会被更新到真实 DOM 树上。
  • destroy():完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发 beforeDestroydestroyed 的钩子。在大多数场景中你不应该调用这个方法。最好使用 v-ifv-for 指令以数据驱动的方式控制子组件的生命周期。

有关于Vue模板的渲染,我也是看得云里来,雾里去。不过不要紧,对于初学者,咱们能整明白怎么使用Vue的模板,就行了。如果你想深入了解底层的渲染原理,可以阅读下面几篇文章:

总结

这篇文章从Vue模板使用入手,了解怎么在Vue中使用模板,然后一起学习了Vue模板渲染的相关知识。如果你和我一样是Vue的初学者,不必太过纠结是否能整明白模板的渲染的原理,我们首要的条件就是学会怎么在Vue中使用模板,得到我们想要的Web运用。

如果你在这方面有更多的经验,欢迎在下面的评论中与我们一起分享,如果文章有不对之处,还请各路大婶拍正。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/vue/vue-template.htmlair max 90 essential fashion