系列文章


引入 Vue

Vue 为我们提供了四种引入的方法,各有优劣。

截至23年6月,官网已经把 CLI 和 Vite 方法替换为了 create-vue ,也就是只有下面的第1和4项可选


CDN、引入.js文件

借助 script 标签直接通过 CDN 引入全体源码的方式来使用 Vue ,具体可参考 vue3 官方文档,不建议在大型项目中使用这种方法

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

在后面的引入方法中,通常要使用 npm 命令对项目进行构建/启动,所以要先在终端上安装node.js,确保 npm 命令能够正常使用,安装可以参考网上文档,这里不再赘述。


Vite

通过 Vite 工具安装 Vue

# When has npm 7+
$ npm init vite@latest <projectName> -- --template vue

$ npm install # Install dependencies
$ npm run dev # Compile and run for development

CLI工具

vue-cli 脚手架使用 webpack 进行编译,比 Vite 构建的项目拥有更多的依赖与包,但同时速度比 Vite 要慢

$ vue create my-project
# OR
$ vue ui # Create GUI for projects

大概是22-23年,Vue CLI 进入了“维护模式”,官方推荐通过基于Vite的新引入方式创建新项目

image-20230316173109517


create-vue

create-vueVue 官方近来推荐的新脚手架工具,它基于 Vite 开发,安装上和原本的 cli 区别不大

$ npm init vue@latest
# Initing your project settings
$ cd <your-project-name>
$ npm install
$ npm run dev

npm init后,你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示(全No也能跑)

不得不说现在这新界面比原来好看多了,当然内容也更复杂了

image-20230612125759650

创建一个应用

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统


应用实例

在我们创建 vue 应用时,都是通过 createApp 函数创建一个新的应用实例并使用, main.js 内会包含这样一句话:

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

这里的 createApp 就通过 App 的数据创建了一个 vue 应用,并把这个应用 mount<div id="app"></div> 这个 DOM 内,最后再把这个 DOM 写入 html 文件,实现元素的展示。

根组件

还是同样的例子,我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

单文件组件可以直接从文件中 import 根组件使用


应用挂载

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串

.mount("#app") 的容器就是一个 <div id="app"></div> 的 DOM 元素,经过 createApp 后产生的组件内容将会渲染在这个 DOM 元素中。

.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。

同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。也就是说,它会返回一个 App 元素(createApp 后产生的根组件实例


总结来说:

import { createApp } from 'vue'

createApp(App).mount('#app')
// 应用实例 根元素 挂载 DOM 元素

组件实例 property

我们通过组件选项,将用户定义的 property 添加到组件实例中,例如 data, methods, props, computed, inject, setup 等。

export default{
data() {
return {
num: 1,
able: false
}
}
}

组件实例定义的所有 property 都可以在组件的模板template)中进行访问

Vue 还在组件中定义了一部分内置 property ,它们在开头有一个 $ 用于与用户定义的 property 相区分


模板语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

vue 会自动将模板 template 编译为优化的 js 代码,当元素变动时会动态地更改渲染的操作,实现实时变动。


文本插值

文本插值,是最基础的数据绑定形式,使用双大括号进行绑定声明,这被称为 Mustache 语法。

<span>Message: {{ msg }}</span>

双大括号内的内容会被替换为组件实例中对应的属性的值,同时绑定的值也会随着属性的更改而实时更新。如果想要单次绑定,不进行更新,则需要使用 v-once 指令进行绑定:

<span v-once>Message: {{ msg }}</span>

原始 html

若直接将数据通过 Mastache 的文本形式进行绑定,将视为直接输出字符串。如果字符串是 html 代码,并想要其根据 html 格式显示,需要使用 v-html 指令进行绑定:

<span v-html="prop1"></span>

span1: "<span style="color: red"> red </span>"

请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。


Attribute 绑定

通过 v-bind 指令,可以动态地绑定一个标签,它指示 Vue 将元素的 id 标签与组件的指定属性保持一致。

可以用 :attribute 进行简写:

<p v-bind:id="dyid1">id</p>
<p :id="dyid1">id</p>

<!-- 上下两行效果一致,id 会动态绑定为 dyid1 包含的内容 -->

Mustache & js 表达式

Vue 在下面两种数据绑定内都支持完整的 JavaScript 表达式,但只能出现单一的表达式,语句、多表达式是不合法的输入。

  • 在文本插值中 (双大括号)
  • 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中
{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

指令

指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bindv-html

指令的期望值为一个 JavaScript 表达式 (除 v-forv-onv-slot 外)

指令的任务是在其表达式的值变化时实时更新 DOM。

<p v-if="seen">Now you see me</p>

seen 作为 bool 变量,通过 v-if 控制当前元素的存在与否


参数

指令名冒号后的内容表示为参数,在 v-bind 中告知绑定的 attribute 是谁,在 v-on 中表示监听的事件名称(使用 @ 进行缩写)


动态参数

较少使用。

在指令参数中使用 [] 将属性(一般为变量)包裹,可以对属性进行动态求值,如下例:

<p :[idd]="id">example</p>
<button @cilck="idd=class">change</button>

点击按钮后,上面动态绑定的参数就会变更为 :class="id"。这样的修改同样可以应用于动态地事件绑定,也就是给 [click] 起一个变量名,随后改变其值


计算属性 & 监听器

计算属性

计算属性,是一种缓存数据的方式,计算属性会随着其依赖数据的变化而动态发生改变,其与 method 类似,但后者会反复求值,不会产生缓存。

使用计算属性可以提高在大数据范围内的运行效率

computed: {
data1: function() {
return this.data.reverse();
},
full_data: {
get: function() {
return this.data.reverse();
},
set: function(newValue) {
console.log(newValue);
this.data = newValue;
}
}
},

计算属性通常使用简写方式声明:如 data1 声明仅包含方法,返回值即该属性代表的内容。完整属性包含 gettersetter 两个方法,前者为简写方法的内容,后者不常用,但可以作为手动更新计算属性的操作(使用等号赋值)。

通常情况下,计算属性是只读的,所以一般不会调用 setter 方法


监听器

与计算属性类似,如果我们需要根据某个数据的变化来执行一些异步操作(如获取 api 等),也可以通过监听器 watch 完成。通过监听某个数据,并在数据变动时执行对应的函数。

在使用时要防止监听器的滥用,在执行异步操作上更推荐使用计算属性,并把逻辑封装在属性的 getter 内部。

// ...
watch: {
value: function(newValue, oldValue) {
console.log(newValue);
},
objecter: {
immediate: true,
handler: function(newValue, oldValue) {
console.log(newValue);
}
}
}
// ...

监听器方法中含有两个参数,命名不做限制,但顺序必须是先写新值,后写旧值。参数中 oldValue 可以不写

除了这种只包含方法的监听外,还可以直接对属性进行监听(格式类似于计算属性),如上文中对 objecter 的监听。这里的 immediate 字段说明在 vue 初始化页面时就会对初始值调用一次 handler,这里的 handler 就是开始谈到的监听函数。

也就是说这个以属性为标识的监听器实质上增加了一些额外的字段,便于进行设置


对象的深度监听

监听器可以监听数据的变化,但如果监听对象,那么当对象内的字段发生变化时,监听器是不会生效的,也就是说,监听器只进行浅层监听,如下例中改变字段 name,则不会触发监听器:

data() {
return {
user: {
name: "name1",
age: 114514
}
}
},
watch: {
user: function(newValue) {
console.log(newValue.name);
}
}

若对对象进行深度监听,可以在对象形式的监听器内部添加参数 deep: true,来进行递归的监听过程。

若想对对象的某一部分进行监听,可以在深度监听的基础上将监听器的对象名改为用字符串包裹的对应部分,如下:

// ...
watch: {
user: { // 对象全体的深度监听
handler: function(newValue) {
console.log(newValue);
},
deep: true
},
"user.name": { // 仅对某字段的监听
// ... 监听器内容
}
}
// ...

Class 与 Style 绑定

classstyle 都是元素的 attribute,它们可以通过 v-bind 进行绑定并渲染。在 vue 中不仅可以通过直接的字符串来指定这些字段,同时也可以使用对象或数组配合 bind 进行动态绑定


Class 绑定

对象方式

下面介绍通过对象绑定 class 的方法。

我们可以令 class 绑定到以下一个对象: {<类名>: <boolean 数据名>} 如若此做,在数据为真时,对应字段便会生效。这个对象不创建新的数据,冒号左侧来源于已有的 class,右侧来源于已有的 data,只不过这个对象本身也可以创建在 data 内。

在绑定时有以下几个规则:

  • 对象可以包含多个字段,并且可以有多个同时为真,此时会添加至两个类
  • 通过对象绑定而声明的 class 可以和常规字符串指定的 class 共存
  • 由于 v-bind,对象内数据的变化可以实时影响元素的 class 列表
  • 更常见的方式是使用计算属性封装对象

有以下例:

export default {
data() {
return {
data1: true,
data2: false
};
},
computed: {
objectClass: function() {
return {
by-object1: this.data1,
by-object2: this.data2
};
}
}
}
<template>
<div class="by-string"
:class: "{by-object1: data1, by-object2: data2}">
content here
</div>
</template>
<style>
.by-string {
font-size: 30px;
}
.by-object1 {
color: red;
}
.by-object2 {
color: blue;
}
</style>

这里 <div> 的 class 列表为 by-string by-object1,由于 data2false,所以 by-object2 字段没有生效


数组方式

除对象外,也可以使用数组进行样式绑定,数组元素需要为数据、数据内容为指向的 class 名,因为不容易快速更改所以不常用。数组中也可以包含对象,不过没有必要做的这么麻烦了。

<div :class="[arrayClass1]">
content
</div>

Style 样式

基础字符串样式:直接通过字符串进行内联

<p style="color:red">content</p>

对象方式

对象方式:在模板处直接声明对象或使用 data 的对象,同样使用 v-bind 绑定。同样的,推荐使用计算属性进行绑定

<p :style="{color:'red', fontSize:size + 'px'}">content1</p>
<p :style="object1">content2</p>

data() {
return {
size: 50,
object1: {
color: 'red',
fontSize: '50px',
"background-color": 'pink'
}
};
}

注意这里的数据可以使用字符串拼接,对大小 px 的处理会更加方便。

内联样式需要使用驼峰命名,而非连字符(连字符需要使用引号)。

对象中格式为 {css 属性名}: {data 中属性}


数组方式

数组方式:和 class 的数组方式相同,只不过最终内含的元素均为 {css}: {data},如:

<p :style="[object1, {color: blue}]">content3</p>

条件渲染

v-if

v-if 可以通过简单的逻辑控制指定元素的加载与否,可以搭配 v-else-ifv-else 使用

<p v-if="digit>5">digit > 5</p>
<p v-else-if="digit==5">digit = 5</p>
<p v-else="digit<5">digit < 5</p>

若想通过一个条件控制多个元素的显示,可以利用不可见的 <template> 元素进行“打包”进而控制区域内的内容


v-show

作用与 v-if 类似,但 v-show 不论如何都会加载这个元素,只不过会根据表达式的值控制其可见与否,即 display 这个 CSS 字段。

v-show 不支持对 <template> 进行控制,也不能使用 v-else


两者区别

简而言之,当元素需要经常切换、切换开销大时,使用 v-show;当切换不频繁时使用 v-if

这是因为 v-show 有更高的初始渲染开销,应尽量避免使用;v-if 有更高的切换开销,在条件不为真时就不进行加载。切换频繁时 v-if 在元素创建、销毁时开销更大。


列表渲染

v-for

我们可以使用 v-for 基于数组或对象对列表进行渲染。每次迭代都会新增一个列表的项。迭代时使用 in 作为分隔符,同时也可以用 of 代替。当数组/对象发生改变时,渲染出的列表也会同时发生变化。


数组方式

使用数组方式迭代时,支持两个参数:元素、下标。

<ui>
<li v-for="item in items">{{ item }}</li>
<li v-for="(item, index) in items">{{ item }} -> {{ index }}</li>
</ui>

对象方式

由于对象类似于字典类型,所以多支持一个参数:值、键、下标

<ui>
<li v-for="item in items">{{ item }}</li>
<li v-for="(value, key, index) in items">{{value}} -> {{key}} -> {{index}}</li>
</ui>

v-for 和 v-if

v-if 类似,v-for 也可以通过 <template> 标签渲染一个包含多元素的块。但是并不建议在一个节点上同时使用 v-forv-if(顺带一提后者的优先级更高),可以外层用 for 内层用 if 进行嵌套实现分离


状态管理与 key

Vue 默认按照“就地更新”的策略对 v-for 元素进行更新。

这样做可以提高渲染性能。

这也就是说,当数据发生顺序变动、增删等时,Vue 不会移动 DOM 元素的顺序,这可能会导致某些匹配出现错位。

为了 Vue 能够识别出每个元素,可以通过 v-bind key 属性对元素进行标注,注意这个属性必须唯一,并且需要使用基础类型,例如下标 index 或者随便一个不会重复的字段(字符串/ number)。如下例:

<ui>
<li v-for="(item, index) in items" :key="index">{{ item }} -> {{ index }}</li>
</ui>

这样一来,每个渲染出的节点就都包含了一个“标签”,与之关联的数据就不会错位了。


数组更新检测

Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:

  • push():末尾添加
  • pop():末尾删除
  • shift():首位删除
  • unshift():首位添加
  • splice():指定位置增删改
    • 模式:list.splice(修改下标, 删除元素个数, 插入元素值)
    • 删除个数为 0 即插入
  • sort():排序,默认升序
  • reverse():元素顺序反转

事件处理

v-on 监听事件

Vue 中使用 v-on 指令对事件进行监听并处理,其可以简写为 @

事件可以为点击、聚焦等。事件处理器可以为内联事件处理器方法事件处理器


事件处理器

内联事件处理器一般用于逻辑简单的事件,表现为一行内联的 JavaScript 代码。

方法事件处理器则会直接调用一个完整的方法,它可以传递参数。

<button :click="count++">{{ count }}</button>
<button :click="addCount(2)">{{ count }}</button>

事件参数

对于触发事件处理的这个事件,我们也可以将其传入方法内进行进一步的处理,需要使用带参数的方法调用,用 $event 代表这个 DOM 事件。

<!-- 使用 $event 变量-->
<button :click="addCount(2, $event)">{{ count }}</button>

<!-- 使用内联的箭头函数 -->
<button :click="(event) => addCount(2, event)">{{ count }}</button>

事件修饰符

事件修饰符是使用 . 表示的指令后缀,可以对事件进行一些说明,指定一些要求。常见的事件修饰符有以下几个:

  • .stop:停止事件的传递,通常阻止子元素的事件向父元素的同操作事件传递
  • .once:事件只执行一次,只有重新加载才能再执行一次
  • .prevent:阻止事件的默认操作,如 submit 操作对页面的重新加载
  • .self:只有当事件来自自身时才进行处理,即忽视传递而来的事件
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

使用修饰符时需要注意调用顺序,使用 @click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为(即 阻止事件+自身),而 @click.self.prevent 则只会阻止对元素本身的点击事件的默认行为(自身事件+阻止)。


按键修饰符

在监听键盘按键输入时,我们可能会监听诸如 Enter 键的输入。这时可以使用按键修饰符进行处理。

<!-- 仅在 `key` 为 `Enter` 时、松开时调用 `submit` -->
<input @keyup.enter="submit" />

Vue 为一些常用按键提供了别名:

  • .enter.tab.delete (捕获 Delete 和 Backspace 两个按键)、.esc.space
  • .up.down.left.right

除此之外还有一些控制按键也可以进行监控:

  • .ctrl.alt.shift
  • .meta:在 Mac 上是 Command,在 Windows 上是 Win 键
  • 简而言之,控制按键必须在满足事件其他条件时保持按住才能使得事件生效
<!-- Alt + Enter,松开 Enter 并保持 Alt 按住(生效) -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + 点击,保持 Ctrl 按住(生效) -->
<div @click.ctrl="doSomething">Do something</div>

鼠标按键也在修饰范围之内:

  • .left.right.middle

请注意,系统按键修饰符和常规按键不同。与 keyup 事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,keyup.ctrl 只会在你仍然按住 ctrl 但松开了另一个键时被触发。若你单独松开 ctrl 键将不会触发。


.exact 修饰符

这个修饰符用于选定系统按键。默认情况下,只要按下 ctrl 再点击,@click.ctrl 就会生效,其他的系统按键是否按下并不影响,而当 .exact 生效时,必须按下、且仅按下前缀的系统按键时才会使得事件生效,如下例:

<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>

表单输入绑定


使用

指令 v-model 为我们提供了数据双向绑定的方法。通过它,我们可以令表单输入框中的内容与 js 中的数据进行同步,最简单的使用如下例:

<input v-model="data1">

这样 input 框中输入的内容就会自动和数据 data1 实时同步。实质上 v-modelv-bindv-on 的结合:

<input :value="data1" @input="event => data1 = event.target.value">

在这里我们首先使用 v-bind 令输入框内容受 data1 控制,正向更新;随后通过 v-on 侦听输入事件并获取内容 target.value 再更新数据,实现反向更新。


v-model 在处理不同表单元素时会采用不同的绑定策略:

  • 文本类型:绑定 value,侦听 input;如 <input><textarea>
  • 单选类型:绑定 checked,侦听 change;如 <input type="checkbox"><input type="radio">
  • 选择器:绑定 value,侦听 change;如 <select>

演示实例

  1. 对多个勾选框,可以把数据导入到一个数组/集合内。在设置时先设置单选框的 value 值,传入数据时会选用这个值。对 <checkbox>(复选框) 和 <radio>(单选)均生效,单选可以不要这个 value,单选/多选选择器同理
<!-- 首先在 data 处定义 names -->

<div>Checked names: {{ names }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>

如此,在选中某个框后,names 处就会显示对应的 value,且元素顺序是单选框选中的顺序

  1. 选择器的初始(禁用)值

如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项(因为 iOS 在这种情况下不会触发一个 change 事件)。因此,我们建议提供一个空值的禁用选项,如下例:

<div>Chosen: {{ selected }}</div>

<select v-model="selected">
<option disabled value="">Please choose one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

在前面提到的向绑定元素传入值的 value 通常而言是预设的字符串,但如果这个传入的值需要是数据,我们可以使用 v-bind 进行绑定。


修饰符

v-model 共支持三个修饰符:

  • .lazy:主要对输入框使用,将更新数据的事件input 更改为 change(失焦),节省更新次数与资源开销
  • .number:自动将用户输入的值转变为 number 类型,可以使用该修饰符
    • 在转换为数字失败或输入为空时,仍会保留 string 类型
  • .trim:就是那个 trim,把两侧的空格去掉,用处不大,其实可以后期处理

NEXT:从负开始的 Vue 学习 (2)