大家耳熟能详的表述如下:Vue 模板的本质其实是一种 声明式渲染 的形式,它在开发过程中提供了将组件的结构与逻辑分离的便利。
也就是说,模板 template 的存在只是为了让我们以更直观的方式描述界面的结构,然而在运行时,模板其实是不存在的,它在底层会被 Vue 编译为更高效的 渲染函数。
1. Vue 模版的本质
Vue 模板语法是基于 HTML 的拓展,它背后的运行机制是将模板编译为 JavaScript 的渲染函数。渲染函数生成 Virtual DOM(虚拟 DOM),并与真实 DOM 进行对比更新(diffing),进而提高渲染的性能。
渲染函数 render 本质上是一个 JavaScript 函数,用于描述 UI 如何与数据关联。Vue 的模板系统和 JSX、React 类似,但 Vue 的模板更加直观,不需要直接编写 JavaScript 函数。
那么 render 函数的作用是什么呢?它的主要任务是描述界面的渲染过程。通过 render 函数,Vue 可以生成虚拟节点,这些虚拟节点是对页面结构的抽象描述。也就是说,render 函数并不直接操作真实的 DOM,它返回的是一个虚拟 DOM,用于描述页面中包含的元素和内容。
在 Vue 中,常用的 h 函数(即 “createElement” 函数)负责创建这些虚拟节点,即 Vue 的虚拟节点就是在渲染函数中产生的。h 函数会根据传入的参数返回一个虚拟节点对象,而这个虚拟节点就是对页面结构的抽象表示。
编译过程:
1、模版编译:Vue 在运行时会将模板编译为渲染函数。这些渲染函数会生成虚拟 DOM。
2、虚拟 DOM 更新:当组件的状态或数据发生变化时,Vue 会重新调用渲染函数生成新的虚拟 DOM。
3、Diff 算法对比:Vue 将新生成的虚拟 DOM 与上一次生成的虚拟 DOM 进行比较,找出变化的地方。
4、真实 DOM 更新:最后,Vue 将变化更新到实际的 DOM 上。
2. Vue 模版的基本语法
1、插值表达式
插值表达式用于动态显示数据,常用 {{ }} 包裹 JavaScript 表达式:
🌰
<template>
<p>{{ message }}</p>j
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue!'
}
}
}
</script>
本质:{{ message }} 会被 Vue 编译为:
return _c('p', [_v(_s(message))])
其中 _s 是将数据转换为字符串,_v 是创建文本节点,_c 是创建元素节点。
2、指令(Directives)
Vue 的指令提供了一些特殊的 HTML 属性,以实现动态绑定。
🌰
<template>
<div v-if="isVisible">Visible Content</div>
</template>
<script>
export default {
data() {
return {
isVisible: true
}
}
}
</script>
本质:v-if 是一个条件渲染语法糖,编译后的代码可能是:
return isVisible ? _c('div', [_v('Visible Content')]) : _e()
其中:_e 是创建空的占位元素。
3、事件处理
Vue 模板中可以使用 v-on 指令来绑定事件:
🌰
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
methods: {
handleClick() {
console.log('Button clicked!')
}
}
}
</script>
本质:@click 是 v-on:click 的简写,编译后的代码类似:
return _c('button', {
on: {
click: function($event) {
return handleClick($event)
}
}
}, [_v('Click me')])
4、列表渲染
Vue 使用 v-for 来遍历列表,并且支持对象和数组的迭代:
🌰
<template>
<ul>
<li v-for="(item, index) in items" :key="index">{{ item }}</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: ['Apple', 'Banana', 'Cherry']
}
}
}
</script>
本质:v-for 循环生成多个相同结构的节点,渲染函数可能是:
return _c('ul', items.map((item, index) => {
return _c('li', { key: index }, [_v(_s(item))])
}))
3. 使用渲染函数 h
除了模板,Vue 也允许开发者直接使用渲染函数来定义组件。
举个 🌰
在页面创建一个 ul 元素,ul 内部包含 100 个 li 元素,使用 render 的写法为:
import { h } from 'vue'
export default {
setup() {
return () => {
const lis = []
for (let i = 0; i < 100; i++) {
const li = h('li', { class: 'item', key: i }, i)
lis.push(li)
}
const ul = h('ul', { class: 'list' }, lis)
return ul
}
}
}
页面结构展示:
上述代码没有任何问题,只是书写起来比较复杂,因此 Vue 给我们提供一个语法糖 Template。
<template>
<ul class="list">
<li class="item" v-for="i in 100" :key="i">{{ i }}</li>
</ul>
</template>
由此进一步引出一个问题:存在某一个东西,在某一个时间点,将上面的 template 模版转换为下面 render 函数。
到底是什么东西在什么时间点呢?
解释:
第一个问题:谁在做?
这个过程称为编译。编译是将一种代码转换成另一种代码的过程。
在 Vue.js 中,存在一个编译模块,将模板(template)转换成 JavaScript 的渲染函数(render function)。这个渲染函数是 Vue 实例化过程中用来生成虚拟 DOM 的核心。
编译器的角色
Vue.js 中的编译器负责将模板语法(如指令、插值等)转换成 JavaScript 代码。这个过程涉及到解析模板、优化和代码生成三个主要步骤:
1、解析模板:编译器首先解析模板中的指令和特殊语法,如 v-if、v-for、{{ expression }} 等。
2、优化:编译器会对解析结果进行优化,比如识别静态节点和避免不必要的 DOM 更新。
3、代码生成:最后,编译器将优化后的抽象语法树(AST)转换成可执行的 JavaScript 代码,即渲染函数。
第二个问题:什么时候编译?
编译时机是指编译过程发生的时间点,这直接影响到应用的性能和打包体积。分为以下两个:
1、运行时编译
在 Vue.js 的早期版本中,编译过程是在浏览器端运行时进行的。这意味着当用户访问应用时,浏览器需要下载 Vue.js 框架,然后动态地将模板编译成渲染函数。这种方式的缺点是:
1)性能开销:运行时编译会增加页面加载和渲染的时间,因为浏览器需要在运行时执行编译任务。
2)打包体积:由于需要包含模板编译模块,应用的打包体积会相对较大。
2、编译时编译
为了解决运行时编译带来的问题,Vue.js 引入了预编译的概念。在这个阶段,编译过程被移到了构建阶段,通常是在开发者执行 npm run build 命令时自动进行的。这种方式的优点是:
1)性能提升:由于编译过程在构建时已经完成,运行时浏览器只需要执行渲染函数,大大减少了页面加载和渲染的时间。
2)打包体积减小:因为模板编译模块只在构建时需要,所以最终的打包结果中不需要包含这部分代码,从而减少了应用的体积。
这也就解释了为什么工程化好的原因啦!