在 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 中的指令也有不少, 最常见的以及其相应的使用方法可以阅读下面相关教程:
v-bind
v-for
v-text 和 v-HTML
v-if 和 v-show
- v-model
- v-slot
- v-on
上面列的都是 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 相关的内容, 可以阅读下面这些文章.
Virtual DOM 的内部工作原理
理解 Virtual DOM
Vue 原理解读系列(一) 之 Virtual DOM and Diff
渲染函数
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 模板渲染过程用图绘制出来.
上图是根据自己阅读相关资料整理的, 难免有错, 欢迎路过的大婶拍正.
来源: https://juejin.im/entry/5c8b1c756fb9a049de6e426b