Vue是一个框架,也是一个生态。其功能覆盖了大部分前端开发需求
Vue的组件可以按照两种不同的风格书写:选项式API(Vue2写法)和组合式API(Vue3写法)
大部分的核心概念在这两种风格之间都是相通的。
import { createApp } from 'vue'
createApp({
data() {
return {
count: 0
}
}
}).mount('#app')
import { createApp, ref } from 'vue'
createApp({
setup() {
return {
count: ref(0)
}
}
}).mount('#app')
构建工具让我们能使用Vue单文件组件(SFC)。Vue官方的构建流程是基于Vite的,一个现代轻量的构建工具
安装15.0或更高版本的Node.js
npm init vue@latest
# 在cmd下运行
这一指令会安装和执行create-vue
,它是Vue官方的项目脚本架工具。你会开看到一些可选功能提示
Need to install the following packages:
[email protected]
Ok to proceed? (y) y
Vue.js - The Progressive JavaScript Framework
√ Project name: ... vue-1 #项目名称不要大写
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... No / Yes
创建后按照提示安装依赖并打开服务器
cd
npm install
npm run dev
npm create-vue project
npm create vue project
npm create-react project
npm init vue@latest
Vue使用一种基于HTML的模板语法,使我们能够声明式的将其组件实例的数据绑定到出现的DOM上。所有的Vue模板都是语法层面合法的HTML,可以被符合规范的浏览器和HTML解析器解析。
最基本的数据绑定形式使文本插值,它使用的是“Mustache”语法(双大括号)
<span>Message: {{ msg }}span>
每个绑定仅支持单一表达式,也是一段能被求值的JS代码。一个简单的判断方法是是否可以合法的写在return后面
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`">div>
因此,下面的例子都是无效的:
<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}
<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html
指令:
<p>Using text interpolation: {{ rawHtml }}p>
<p>Using v-html directive: <span v-html="rawHtml">span>p>
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind
指令:
<div v-bind:id="dynamicId">div>
v-bind
指令指示 Vue 将元素的 id
attribute 与组件的 dynamicId
属性保持一致。如果绑定的值是 null
或者 undefined
,那么该 attribute 将会从渲染的元素上移除。
因为 v-bind
非常常用,我们提供了特定的简写语法:
<div :id="dynamicId">div>
<script>
export default{
data(){
return{
msg:"active",
style:"app"
}
}
}
</script>
<template>
<div :class="msg" :id="style">测试</div>
</template>
<style>
#app{
color: red;
}
</style>
指令是带有 v-
前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind
和 v-html
。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for
、v-on
和 v-slot
)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if
为例:
<script>
export default{
data(){
return{
msg:false
}
}
}
</script>
<template>
<div v-if="msg">你能看见我吗</div>
<div v-else>那你看看我</div>
</template>
v-if
和v-show
的区别v-if
也是惰性的:如果在初次渲染条件值为false,则不会做任何事。条件区只有当条件首次变为true时才被渲染。
相比之下,v-show
简单许多,元素无论初始条件如何,始终会被渲染,只有CSSdisplay
属性会被切换。
总的来说,v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用v-show
较好;如果在运行时绑定条件很少改变,则v-if
会更合适
我们可以使用v-for
指令基于一个数组来渲染一个列表。v-for
指令的值需要使用item in items
形式来遍历。
<script>
export default{
data(){
return{
names:["小丁","小海","小段"]
}
}
}
</script>
<template>
<p v-for="i in names">{{ i }}</p>
</template>
v-for
支持使用可选的第二个参数标识当前项的位置索引
<p v-for="(i,index) in names">{{ i }}-{{ index }} p>
可以使用of
作为分隔符来代替in
,这样更接近JS的迭代器语法
<p v-for="(i,index) of names">{{ i }}-{{ index }} p>
v-for
支持使用可选的第二个参数标识当前项的名称
<p v-for="(i,key,index) in names">{{ i }}-{{ key }}-{{ index }} p>
遍历对象的时候名称要不一样,如果names:{name:"小丁",name:"小海",name:"小段"}
只会输出一个小段
export default{
data(){
return{
names:{name:"小丁",age:"小海",id:"小段"}
}
}
}
Vue默认按照“就地更新”的策略来更新通过v-for
渲染的元素列表。当数据项的顺序改变时,Vue不会随之移动DOM元素的顺序,而是就地更新每一个元素,确保它们在原本指定的索引位置上渲染。
为了给Vue一个提示,以便他可以跟踪每一个节点的标识,从而重用和重新排序现有的元素,你需要为每一个元素对应的块提供一个唯一的key
:
<p v-for="(i,index) of names" :key="index">{{ i }}p>
key
在这里是一个提供v-bind
绑定的特殊attribute推荐在任何可行的时候为
v-for
提供一个key
attribute
key
绑定的值期望是一个基础类型的值,例如字符串或number类型
Vue能够侦测响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
变更方法,就是会对调用它们的原数组进行变更。相对的,也有一些不可变方法,如filter()
,concat()
和slice()
,这些都不会更改原数组,而总是返回一个新数组。
{{ i }}
我们可以使用v-on
指令(简写为@)来监听DOM事件,并在事件触发时执行对应的JS。用法:v-on:click="methodName"
或@click="handler"
事件处理器的值可以是
onclick
类似)<template>
<button v-on:click="count++">add</button>
<p>{{ count }}</p>
</template>
<script>
export default{
data(){
return{
count:0
}
}
}
</script>
<template>
<button @click="add">add</button>
<p>{{ count }}</p>
</template>
<script>
export default{
data(){
return{
count:0
}
},
methods:{
add(){
this.count+=1
}
}
}
</script>
在处理事件时调用event.preventDefault()
或event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注与数据逻辑而不用处理DOM事件的细节会更好
为解决这一问题,Vue为v-on
提供了事件修饰符,常用有以下几个:
.stop
.prevent
.once
.enter
<template>
<a href="" @click="one">阻止默认事件</a>
</template>
<script>
export default{
data(){
return{
}
},
methods:{
one(e){
e.preventDefault();
console.log("1");
}
}
}
<a href="" @click.prevent="one">阻止默认事件a>
<template>
<div @click="one">
<p @click="two">阻止冒泡事件</p>
</div>
</template>
<script>
export default{
data(){
return{
}
},
methods:{
one(e){
console.log("1");
},
two(e){
e.stopPropagation();
console.log("2");
}
}
}
</script>
<div @click="one">
<p @click.stop="two">阻止冒泡事件p>
div>
<template>
<p>{{ person.content.length > 0 ? "yes" : "no"}}</p>
</template>
<script>
export default{
data(){
return{
person:{
name:"小丁",
content:["1","2","3"]
}
}
}
}
</script>
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。因此我们推荐使用计算属性来描述响应式状态的复杂逻辑
<template>
<p>{{ getcontent }}</p>
</template>
<script>
export default{
data(){
return{
person:{
name:"小丁",
content:["1","2","3"]
}
}
},
computed:{
getcontent() {
return this.person.content.length > 0 ? "yes" : "no"
}
},
methods:{
getcontent2() {
return this.person.content.length > 0 ? "yes" : "no"
}
}
}
</script>
重点区别:
计算属性:计算属性值会基于其相应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算
方法:方法调用总是会在重渲染发生时再次执行函数
数据绑定的一个常见需求场景是操作的CSS class 列表,因为class
是attribute,我们可以和其它attribute一样使用v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue专门为class
的v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
<div :class="{ active: isActive }">div>
上面的语法表示 active
是否存在取决于数据属性 isActive
的真假值。
你可以在对象中写多个字段来操作多个 class。此外,:class
指令也可以和一般的 class
attribute 共存。举例来说,下面这样的状态:
const isActive = ref(true)
const hasError = ref(false)
class会绑定对象、数组
:style
支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style
属性:
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">div>
尽管推荐使用 camelCase,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称),例如:
<div :style="{ 'font-size': fontSize + 'px' }">div>
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
<div :style="styleObject">div>
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
我们还可以给 :style
绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
<div :style="[baseStyles, overridingStyles]">div>
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 watch
函数在每次响应式状态发生变化时触发回调函数:
<template>
<p>{{ msg }}</p>
<button @click="change"></button>
</template>
<script>
export default{
data(){
return{
msg:"hello"
}
},
methods:{
change(){
this.msg="world"
}
},
watch:{
msg(newValue,oldValue){
//数据发生变化,自动执行的函数
console.log(newValue,oldValue);
}
}
}
</script>
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
<input
:value="text"
@input="event => text = event.target.value">
v-model
指令帮我们简化了这一步骤:
<input v-model="text">
另外,v-model
还可以用于各种不同类型的输入,、
元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
和
元素会绑定 value
property 并侦听 input
事件;
和
会绑定 checked
property 并侦听 change
事件;
会绑定 value
property 并侦听 change
事件。注意
v-model
会忽略任何表单元素上初始的value
、checked
或selected
attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API来声明该初始值。
虽然Vue的声明性渲染模型为你抽象了大部分对DOM的直接操作,但在某些情况下,我们仍然需要直接访问底层DOM元素。要实现这一点,我们可以使用特殊的ref
attri
挂载结束后引用都会被暴露在this.$refs
之上
<script>
export default {
mounted() {
this.$refs.input.focus()
}
}
</script>
<template>
<input ref="input" />
</template>
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
<template>
<header1/>
</template>
<script>
//引用组件并注册组件
import header1 from './components/header.vue'
export default {
components:{
header1
}
}
</script>
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
若要将导入的组件暴露给模板,我们需要在 components
选项上注册它。这个组件将会以其注册时的名字作为模板中的标签名。
组件可以被重用任意多次:
<h1>Here is a child component!h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
在main.js
下设置全局变量
import header2 from './components/header.vue'
const app = createApp(App)
//在这中间写组件注册
app.component('header2',header2)
app.mount('#app')
<template>
<header2/>
template>
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。
Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 props
选项:
<!--container.vue-->
<template>
<h3>container</h3>
<p>{{ title }}</p>
</template>
<script>
export default{
data(){
return{
}
},
props:['title']
}
</script>
<!--header.vue-->
<template>
<h3>header</h3>
<container :title="title"></container>
</template>
<script>
import container from './container.vue'
export default{
data(){
return{
title:"ncao"
}
},
components:{
container
}
}
</script>
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
export default {
// ...
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
}
}
这种情况下,我们可以使用 v-for
来渲染它们:
可以通过子类来验证接收数据的类型和默认值
props:{
title:{
type:String,
default:"默认值"
}
}
注意:
prop是只读的,不能修改父元素传递过来的数据,只能修改子元素自身的数据
methods:{
updateMethode(){
//错误操作,不允许修改父元素传递过来的数据
this.title = "新元素!"
}
}
在组件的模板表达式中,可以直接使用$emit
方法触发自定义事件
触发自定义事件的目的是组件之间传递数据
通过监听子类的
v-model
获取数据,$emit
将数据从子类传递给父类(自定义方法名为$emit("search",this.search)
中设置好的变量名)
<!--one.vue-->
<template>
<h3>one</h3>
<p>搜索内容为:{{ msg }}</p>
<two @search="getdata"/>
</template>
<script>
import two from './two.vue'
export default{
data(){
return{
msg:""
}
},
components:{
two
},
methods:{
getdata(data){
this.msg = data
}
}
}
</script>
<!--two.vue-->
<template>
<h3>two</h3>
<input type="text" v-model="search">
</template>
<script>
export default{
data(){
return{
search:""
}
},
watch:{
search(newValue,oldValue){
this.$emit("search",this.search)
}
}
}
</script>
在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
举例来说,这里有一个
组件,可以像这样使用:
<template>
<one>
<p>插槽内容</p>
</one>
</template>
<script>
import one from './components/one.vue'
export default{
data(){
return{
}
},
components:{
one
}
}
</script>
下接收为:(这样可以在
中显示插槽内容
)
<slot>slot>
如果没有数据传递,slot设置默认值
<slot>插槽默认值slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
<template>
<one>
<p>{{ msg }}</p>
</one>
</template>
<script>
import one from './components/one.vue'
export default{
data(){
return{
msg:"插槽内容"
}
},
components:{
one
}
}
</script>
具名插槽(通过具体变量名来传递数据)
<one>
<template v-slot:a>
<p>{{ msg }} a</p>
</template>
<template v-slot:b>
<p>{{ msg }} b</p>
</template>
</one>
<!-- === v-slot:a可以缩写成#a === -->
<template #a>
<slot name="a">插槽默认值slot>
<slot name="b">插槽默认值slot>
传递数据给父类方法一
可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<template>
<h3>one</h3>
<slot name="a" :text="msg">插槽默认值</slot>
</template>
<script>
export default{
data(){
return{
msg:"one传递数据给父类"
}
}
}
</script>
当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面我们将先展示默认插槽如何接受 props,通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props 对象:
<template v-slot:a="slotProps">
<p>{{ slotProps.text }}p>
template>
方法二
<slot name="b" text="one传递数据给父类b">插槽默认值slot>
<template v-slot:b="{text}">
<p>{{ text }}p>
template>
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个
组件,它的模板长这样:
template
<button>click mebutton>
一个父组件使用了这个组件,并且传入了 class
:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">click mebutton>
这里,
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了
的根元素上。
class
和 style
的合并如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。如果我们将之前的
组件的模板改成这样:
<button class="btn">click mebutton>
则最后渲染出的 DOM 结果会变成:
<button class="btn large">click mebutton>
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
举例来说,mounted
钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
export default {
mounted() {
console.log(`the component is now mounted.`)
}
}
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 mounted
、updated
和 unmounted
。
所有生命周期钩子函数的 this
上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过 this
获取组件实例。
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
异步组件就是异步请求,防止大项目中组件过多加载容量大,加载缓慢。
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const two = defineAsyncComponent(
() => import('./two.vue')
)
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
注意,虽然这里的 组件可能根本不关心这些 props,但为了使
能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。
provide
和 inject
可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
provide:{
prop: "依赖注入"
}
inject:['prop']
this上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过
this` 获取组件实例。
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
[外链图片转存中…(img-Xpyvylt0-1694591464564)]
异步组件就是异步请求,防止大项目中组件过多加载容量大,加载缓慢。
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const two = defineAsyncComponent(
() => import('./two.vue')
)
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
[外链图片转存中…(img-7iLSgRfl-1694591464564)]
注意,虽然这里的 组件可能根本不关心这些 props,但为了使
能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。
provide
和 inject
可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
[外链图片转存中…(img-em5RVjJN-1694591464565)]
provide:{
prop: "依赖注入"
}
inject:['prop']