Vue2
版本历史
Vue2 的最后一个版本是 2.7.16
2.7 系列的版本包含了对 Vue 3 的一些特性的兼容性改进,同时也保持了与 Vue 2 生态系统的兼容性。
2.6.14
是 Vue2 最后一个用 JS 写的版本。
工具库与 Vue2 的兼容性:
- Vue CLI:应使用
4.4.6
或更低版本的@vue/cli
- Vuex:应使用 Vuex 3.x,https://v3.vuex.vuejs.org/zh/
- Vue Router:通常是 3.x 系列,https://v3.router.vuejs.org/zh/
单页面应用和多页面应用
单页应用 SPA
- 优点:页面切换快
- 页面每次切换跳转时,页面局部刷新,JS、CSS 等公共资源仅加载一次
- 缺点:
- 首屏时间慢
- 首屏时需要请求 HTML,要加载公共资源
- SEO 效果差
- 搜索引擎只认识 HTML 里的内容,不认识 JS 的内容,而单页应用的内容都是靠 JS 渲染生成出来的
- 首屏时间慢
多页应用 MPA
- 优点:
- 首屏时间快
- 访问页面的时候,发送一个 HTTP 请求返回一个 HTML,页面就会展示出来
- SEO 效果好
- 搜索引擎通过识别 HTML 内容来给网页排名
- 首屏时间快
- 缺点:页面切换慢
- 多页面跳转需要刷新所有资源
虚拟 DOM、Diff 算法
虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构「虚拟」地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。
例如如下的一个虚拟节点,它代表一个 div 元素,是一个纯 JS 对象。至少包含标签名、属性和子元素对象
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
};
实际渲染到浏览器上的 DOM 结构是:
<div id="hello">
<!-- 更多 DOM 结构 -->
</div>
一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。
如果有两份虚拟 DOM 树,渲染器将会对比遍历它们,找出二者的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为「比对」(diffing) 或「协调」(reconciliation)。
那为什么要使用虚拟 DOM 呢?vue 需要先做 diff 对比,然后去操作实际 DOM,这样不是比直接去操作 DOM 更消耗性能吗?
- 减少 JS 操作真实 DOM 的性能消耗
- 开发者体验更好,专注于数据和逻辑,不用关注 DOM 操作
- 跨平台兼容性,虚拟 DOM 就是一个对象,可以跨平台
虚拟 DOM 进行 diff 对比会增加一些计算开销,但这种开销通常远小于直接操作真实 DOM 的开销。当在一次操作需要更新 10 个 DOM 节点时,浏览器收到第一个更新 DOM 请求后会马上执行流程,最终执行 10 次流程。而虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地的一个 JS 对象中,最终将这个对象一次性渲染到 DOM 树上,减少实际 DOM 操作的数量。
Diff 算法采用了一种称为「双端比较」的策略,通过比较新旧虚拟 DOM 树的节点,找出最小的变更集。相比于直接操作 DOM,这种算法可以显著减少实际 DOM 操作的数量。
Vue 模版编译原理
vue 中的模版无法被浏览器解析,因为 template 不是 html 标签,需要将其编译成 js 函数,将其执行后渲染出 html 元素。
vue 的模版编译是将模板字符串转换为渲染函数的过程,主要有三步:
- 解析(parse):将模版字符串转换为 AST 抽象语法树。使用正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为 AST
- 优化(optimize):标记静态节点,便于后续的渲染优化。具体是遍历 AST,找到静态节点并标记,在页面重渲染进行 diff 比较时,跳过这些静态节点,优化 runtime 的性能
- 生成代码(generate):将 AST 转换为 render 渲染函数
可以查看源码:src/compiler
data 为何声明为函数
因为组件可能被用来创建多个实例,若 data 声明为对象则所有的实例将共享引用同一个数据对象。
data 声明为函数则每次创建一个新实例后,调用 data 函数会返回初始数据的一个全新副本数据对象。
data 声明为对象:
function VueComponent() {}
VueComponent.prototype.$options = {
data: { name: 'Vue' }
};
let f1 = new VueComponent();
f1.$options.data.name = 'React';
let f2 = new VueComponent();
console.log(f2.$options.data.name); // React
data 声明为函数:
function VueComponent() {}
VueComponent.prototype.$options = {
data: () => ({ name: 'Vue' })
};
let f1 = new VueComponent();
let res = f1.$options.data();
res.name = 'React';
console.log(res); // {name: "React"}
let f2 = new VueComponent();
console.log(f2.$options.data()); // {name: "Vue"}
new Vue()
可以将data
声明为一个普通对象是因为这个类创建的实例不会被复用,只会 new 一次。而App.vue
同样是因为整个系统中App.vue
只会被使用一次,所以不存在上述的问题。
v-if 和 v-show 区别
v-if
是真正的条件渲染,会有性能开销,每次插入或者移除元素时都必须要生成元素内部的 DOM 树v-show
则不管条件是什么都会渲染元素,基于display:none
显示隐藏
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。
因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好
v-model 原理
v-model 用于在表单输入元素和应用状态之间创建双向数据绑定。v-model
其实是一个语法糖,做了两件事:
v-bind
绑定:将输入元素的值绑定到数据模型v-on
监听:监听输入事件(如 input、change 等),并在事件触发时更新数据模型
基本用法
<template>
<input v-model="message" />
</template>
<script>
export default {
data() {
return {
message: ''
};
}
};
</script>
等效于:
<template>
<input :value="message" @input="message = $event.target.value" />
</template>
<script>
export default {
data() {
return {
message: ''
};
}
};
</script>
自定义组件中的 v-model
在自定义组件中使用 v-model
时,Vue 会默认将 value
作为 prop 传递,并监听 input
事件来更新父组件的数据。
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value']
};
</script>
<template>
<child-component v-model="message"></child-component>
<p>{{ message }}</p>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return {
message: ''
};
}
};
</script>
手写 v-model
原理:使用Object.defineProperty()
实现响应式数据劫持,监听事件触发视图更新。
<input type="text" id="input" />
<p id="output"></p>
<script>
const input = document.getElementById('input');
const output = document.getElementById('output');
// 数据对象
const data = {
message: 'hello'
};
// 响应式数据劫持
function defineReactive(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
value = newValue;
// 更新视图
input.value = newValue;
output.textContent = newValue;
}
});
}
// 初始化响应式数据
defineReactive(data, 'message');
// 初始化视图
input.value = data.message;
output.textContent = data.message;
// 监听输入框的输入事件,更新数据对象
input.addEventListener('input', function (e) {
data.message = e.target.value;
});
</script>