vue是由Evan You开源的轻量级前端框架,诞生于2014年,Vue.js 3采用MVVM架构,支持声明式编程、组件化开发、前端路由、单向数据流和数据双向绑定,同时提供了非常多的内置指令来简化对页面的操作。得益于这些特性,Vue.js很快就在前端圈火起来了。2016年10月,Evan You发布了Vue.js 3版本,新增了setup语法、CompositionAPI、TypeScript等特性
Vue.js
是一套用于构建用户页面的渐进式框架,也就是可以在项目中一点点引入和使用Vue.js
,而不需要用Vue.js
来开发整个项目
特点
- 数据双向绑定:采用声明式编程,可以通过更改数据自动触发视图更新,与React类似
- 指令:
Vue.js
提供了许多内置指令,方便快速操作页面 - 组件化:支持组件化开发和封装可复用的代码,与
React
和Angular
类似 - 前端路由:支持前端路由、构建单页面应用(SPA)、服务器端渲染(SSR)应用,与
React
和Angular
类似 - 单向数据流:组件状态管理采用了单向数据流、与React类似
Vue.js 2和Vue.js 3优缺点对比
Vue.js 2缺点
- 对TypeScript支持不友好,对大型项目构建不利
- Mixin混入缺陷,Vue.js 2使用Mixin混入来抽取相同代码逻辑,但当一个组件有多个Mixin时,代码会变得难以阅读,因为不知道某个属性来自哪个Mixin,并且多个Mixin中的属性容易发生冲突
- 响应式系统缺陷,Vue.js 2采用
Object.definedProperty
来进行数据劫持,这种方式存在一些缺陷,比如无法劫持和监听对象添加或删除属性时的变化,无法遍历对象的每个属性,包括对于对象属性的对象需要进行深度遍历,导致性能低下 - 逻辑零散,Vue.js 2使用Options API编写组件,当实现某个功能时,对应的代码逻辑会被拆分到各个属性中,一旦组件变得复杂,逻辑就会变得零散
为了克服上述Vue.js 2存在的缺点,2020年9月Evan You发布了Vue.js 3,改版本带来了非常多新的变化
Vue.js 3优点
- monorepo源码管理。mono → \rightarrow → 单个,repository → \rightarrow →仓库。monorepo的意思就是将许多项目的代码存储在同一个repository中。具体做法是将不同的模块拆分到packages目录下的子目录中,每个模块都可以看作一个独立的项目,具有自己的类型定义、API、测试用例等
- 采用TypeScript进行重构。Vue.js 2整个项目使用Flow进行类型检测,Flow在很多复杂场景中对类型的支持不是非常友好。从Vue.js 3开始,项目全面采用TypeScript进行重构。在TypeScript的加持下,Vue.js 3可以编写更加健壮的代码,同时更容易开发大型项目
- 采用Proxy进行数据劫持,这种方式会存在一些缺陷,比如当给对象添加或者删除属性时,是无法进行劫持和监听的;而Vue.js 3采用Proxy实现数据劫持。当给对象添加或者删除属性时,Proxy可以劫持和监听到,因为Proxy劫持的是整个对象,而且Proxy能劫持的类型比Object.definedProperty更丰富,不仅可以劫持set、get方法,还支持劫持in、delete操作等
- 编译阶段的优化,使得Vue.js 3在性能和效率方面有较大的提升。包括生成Block Tree、slot编译优化、diff算法优化等。具体可参考相关资料
- Composition API。Vue.js 2使用Options API来编写组件,其中包含data、props、methods、computed和生命周期等选项。在实现某个功能时,对应的代码逻辑会被拆分到各个属性中,一旦组件变得更大或更复杂,逻辑就会变的非常分散,需要在多个选项之间寻找,这不利于后期的维护和扩展;相比之下,Vue.js 3主要采用Composition API来编写组件,同时兼容Options API。Composition API包含ref、reactive、computed、watchEffect、watch等函数,Composition API可以将相关的代码放在同一处进行处理,封装成一个Hook函数来支持数据的响应式,并避免Mixins混入带来的缺陷。这样可以更加方便的实现在多个组件之间共享逻辑,也能够提高代码的可读性和可维护性
- 移除了一些非必要API,Vue.js 3移除了Vue实例中的
$on
、$off
和$once
API,还移除了一些特性,比如filter和内联模版等
架构
根据官方说法,Vue的整个设计受到MVVM架构的启发
- MVVVM架构模式是一种软件体系结构,代表Model、View和ViewModel,如下图所示
- View代表视图层,用于编写页面布局
- ViewModel负责把Model层的数据绑定到View层,将View层产生的DOM事件绑定到Model层
- Model是模型层,用于提供模型和数据
createApp的对象参数
- Vue实例是通过
Vue.createApp
函数创建的,该函数需要接收一个对象作为参数,该对象可以添加template、data、methods等属性
template属性
- Vue.js 3中的template属性用于定义需要渲染的模板内容,其中包括HTML标签或组件,并最终将其挂载到
<div id="app"></div>
元素上,相当于为innerHTML
赋值。在模板中,也会使用一些语法,例如{{}}
和@click
等 - 在template属性中,使用字符串的方式来编写HTML页面,vs code并没有提供智能提示,这会降低编码效率和体验。为了解决这个问题,Vue.js 3提供了两种方式来优化模板的编写。
- 使用
<script>
标签,将其类型标记为x-template
,并为其添加id
属性。 - 使用任意标签(通常是
<template>
标签,因为它不会被浏览器渲染),并为其添加id
属性
<template>
标签或元素是一种用于保存客户端内容的机制。当页面加载时,该内容不会呈现,但可以在运行时使用javascript进行实例化
innerText
:获取或设置元素的文本内容,不包含 HTML 标签。
textContent
:获取或设置元素的文本内容,包括所有子节点。
outerHTML
:获取或设置元素及其所有子元素的 HTML 内容。
innerHTML
:获取或设置元素的 HTML 内容,但不包括元素本身。
- 接下来让我们尝试使用不同方法,实现一个简单计数器。如果用原生javascript方式实现,代码可能是下面这样的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2 class="counter"></h2>
<button class="increment">+1</button>
<button class="decrement">-1</button>
<script>
const counterE1 = document.querySelector(".counter")
const increment = document.querySelector(".increment")
const decrement = document.querySelector(".decrement")
let counter = 100;
counterE1.innerHTML = counter;
// 监听按钮点击事件
increment.addEventListener("click", () => {
counter += 1;
// 点击按钮后进行变量赋值
counterE1.innerHTML = counter;
})
decrement.addEventListener("click", () => {
counter -= 1;
counterE1.innerHTML = counter;
})
</script>
</body>
</html>
- 如果用
vue.js 3
实现,代码可能是下面这样的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h2> {{ counter }}</h2>
<button @click="increment"> +1 </button>
<button @click="decrement"> -1 </button>
</div>
<script src="./js/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
counter: 100
}
},
methods: {
increment() {
this.counter += 1;
},
decrement() {
this.counter -= 1;
}
}
})
app.mount("#app")
</script>
</body>
</html>
- 用
<script>
标签的写法如下。这需要引入Vue.js 3
源码。可以通过CDN或者本地引入,链接为https://unpkg.com/vue@3.4.38/dist/vue.global.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
// 添加type="x-template"和id="here"两个属性
<script type="x-template" id="here">
<div>
<h2> {{ message }}</h2>
<h2> {{ counter }}</h2>
<button @click="increment"> +1 </button>
<button @click="decrement"> -1 </button>
</div>
</script>
<script src="./js/vue.js"></script>
<script>
const app = Vue.createApp({
template: '#here', // 通过id选择器选中页面上的模板,底层执行document.querySelector("#here")查找
data() {
return {
message: 'script',
counter: 100
}
},
methods: {
increment() {
this.counter += 1;
},
decrement() {
this.counter -= 1;
}
}
})
app.mount("#app")
</script>
</body>
</html>
- 使用
<template>
标签的写法可能是这样的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
<template id="here">
<div>
<h2> {{ message }}</h2>
<h2> {{ counter }}</h2>
<button @click="increment"> +1 </button>
<button @click="decrement"> -1 </button>
</div>
</template>
<script src="./js/vue.js"></script>
<script>
Vue.createApp({
template: "#here",
data() {
return {
message: 'template',
counter: 100,
}
},
methods: {
increment() {
this.counter += 1
},
decrement() {
this.counter -= 1
}
}
}).mount('#app')
</script>
</body>
</html>
data属性
- 可以看到,上面的
Vue.createApp
的参数里面有一个data
属性,它会用于为Vue.js
组件定义响应式数据,该属性需要传入一个函数,该函数需要返回一个对象。该对象会被Vue.js
响应式系统劫持,之后对该对象的修改或访问都会在劫持中被处理,所以该对象中定义的数据都是响应式的。比如在template
中通过使用{{counter}}
,可访问到该对象中、定义的counter
;当修改counter
时,template
中的{{counter}}
也会发生改变
methods属性
- methods属性需要传入一个对象,通常在这个对象中会定义很多方法。这些方法会绑定到模板中,例如上面反复定义的
increment
和decremnet
方法。在方法中,可以使用this
关键字直接访问data
返回对象的属性 - methods属性中定义的方法不能使用箭头函数。否则无法使用
this
直接访问data中的数据,因为this
在箭头函数中会在自己的上层作用域中查找this