跳到主要内容

Vue

构建用户界面渐进式(工程规模由小变大)。

  • 组件化(Components) 提高代码复用率。(复用、封装)
  • 声明式编码 (Declarative Rendering) 代码更加直观。 (数据驱动视图)
  • 虚拟 DOM (Virtual DOM) + Diff 算法 提高渲染性能。
  • 响应式数据流 (Reactive Data Binding) 数据变化自动更新视图。
  • 生命周期钩子 (Lifecycle Hooks) 在特定阶段执行特定逻辑。
  • 插槽 (Slots) 提供灵活的组件组合方式。
  • 计算属性 (Computed Properties) 缓存计算结果,提高性能。
  • 侦听器 (Watchers) 监听数据变化,执行异步操作。
  • 路由 (Router) 导航和视图映射。

学习目标

  1. 先学会用数据在 template 和 style 里面学写 HTML 和 CSS 样式,在 script 里学写响应式数据,数模分离实现在 view 里面渲染
  2. 学路由规则的设计和跳转的逻辑
  3. 实现一个独立的单组件,学会跨组件、跨视图的数据通信( props + pinia ),还有表单里面的 v-model
  4. 学下 axios 组件 ,学会在 script 里面调用接口获得动态数据
  5. 理解下 jwt 的工作原理,学下通过 axios 如何添加请求拦截器,如何携带 token
  6. Vue 基础
  7. Vue CLI
  8. Vue-Router
  9. Vuex
  10. Element-UI
  11. Vue3

API 风格

选项式 API

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethodsmounted。 选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},

// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},

// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

组合式 API

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this) ,对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。

组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。

基础

模板语法

  • v-bind: 动态的绑定一个或多个 attribute,也可以是组件的 prop。
  • v-html: 更新元素的 innerHTML

<template>
<!-- v-bind:disabled ==> :disabled v-on:click ==> @click -->
<button :disabled="isButtonDisabled" @click="count++" @[eventName]="count+=2">count: {{ count }}</button>
<p>{{ msg }}</p>
<p>{{ msg.split('').reverse().join('') }}</p>
<p>{{ count == 1 ? msg : 'error' }}</p>
<p>{{ rawHtml }}</p>
<p v-bind:id="dynamicId" v-html="rawHtml"></p>
<p :id="dynamicId" v-html="rawHtml"></p>
<div v-bind="objectOfAttrs"></div>
<p v-if="seen">Now you see me</p>
<a :[attrName]="attrValue" :href="url">{{attrValue}}</a>
</template>

<script>
export default {
data() {
return {
count: 0,
msg: 'This is a message!',
rawHtml: '<span style="color: red">This should be red.</span>',
dynamicId: 'dynamicId',
isButtonDisabled: false,
objectOfAttrs: {
id: 'container',
class: 'wrapper'
},
seen: true,
eventName: 'focus',
attrName: 'title',
attrValue: '详情请咨询百度...',
url: 'http://www.baidu.com'
}
}
}
</script>

响应式基础

声明响应式状态

响应式代理 vs 原始值

export default {
data() {
return {
someObject: {}
}
},
mounted() {
const newObject = {}
this.someObject = newObject

console.log(newObject === this.someObject) // false
}
}

声明方法

export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
// 在其他方法或是生命周期中也可以调用方法
this.increment()
}
}

计算属性

computed: {  
publishedBooksMessage() {
return this.author.books.length > 0 ? 'YES' : 'NO';
}
}

计算属性缓存 vs 方法

计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。

下面的 now 永远也不会更新,因为 Date.now() 不是一个响应式依赖

computed: {
now() {
return Date.now()
}
}

可写计算属性

Class 与 Style 绑定

绑定 HTML class

绑定内联样式

条件渲染

v-if vs. v-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染

v-for

事件处理

内联事件处理器 事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。 方法事件处理器 一个指向组件上定义的方法的属性名或是路径。

<!-- 单击事件将停止传递 -->
<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>

<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>

按键别名

  • .enter
  • .tab
  • .delete
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right
  • .ctrl
  • .alt
  • .shift
  • .meta // macOS 上是 Command 键, windows 上是 win 键

.exact 修饰符

.exact 修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。

<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>

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

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

表单输入绑定

v-model="message"

生命周期

侦听器

watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:

export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// 注意:在嵌套的变更中,
// 只要没有替换对象本身,
// 那么这里的 `newValue` 和 `oldValue` 相同
},
deep: true
}
}
}

模板引用

<input ref="input">

组件基础

在 components 中注册

深入组件

组件注册

全局注册

app.component()

局部注册

使用 components 选项

<script>
import ComponentA from './ComponentA.vue'

export default {
// 注册 ComponentA,ComponentB 组件
components: {
ComponentA,
// key 名就是注册的组件名
CB : ComponentB
}
}
</script>

<template>
<ComponentA />
</template>

Props

一个组件需要显示的声明它所接受的 props ,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute 。

export default {
// props: ['foo'],
props : {
// key 是 prop 的名称, value 是 prop 的类型(构造函数)
title : String,
likes : Number,
greetingMessage : String
},
created() {
// props 会暴露到 `this` 上
console.log(this.foo)
}
}
<MyComponent gretting-message="hello" />

props 校验

export default {
props: {
// 基础类型检查
//(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或者数组应当用工厂函数返回。
// 工厂函数会收到组件所接收的原始 props
// 作为参数
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
}
}

组件事件

事件参数 声明触发的函数 事件校验

组件 v-model

v-model 在原生元素上实现双向绑定。

<input v-model="searchText" />
// 等效于
<input
:value="searchText"
@input="searchText = $event.target.value"
/>

v-model 在组件上实现双向绑定

透传 Attribute

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 propsemits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

深层组件继承

禁用 Attributes 继承

inheritAttrs: false

通过 $attrs 可以访问到透传过来的 Attributes

  • 保留原始大小写
  • @click 被暴露为 $attes.onClick 函数

多根节点的 Attributes 继承

在 JavaScript 中访问透传 Attributes

插槽 Slots

渲染作用域

默认内容

具名插槽

<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>

<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>

动态插槽名

<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>

<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>

作用域插槽

插槽同时访问父组件和子组件的数据。

<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

具名作用域插槽

高级列表组件

<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>

无渲染组件

依赖注入

Props 逐级透传问题

provideinject

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
export default {
inject: ['message'],
created() {
console.log(this.message) // injected value
},
data() {
return {
// 基于注入值的初始数据 fullMessage: this.message
}
}
}

异步组件

Promise

组合式函数

响应式基础

面试题

2、v-show 与 v-if 的区别

  1. v-show 无论初始化条件是什么,元素都会被渲染
  2. v-show 是通过 css 隐藏元素,v-if 是直接将整个 dom 删除。

3、Vue 实例挂载的过程

new Vue(options) 发生了什么?

会调用 _init 方法

  • 定义方法、事件、生命周期 调用 $mount 进行页面挂载 mountComponent 方法 执行 render 生成虚拟 DOM _update 将虚拟 DOM 生成真实 DOM 结构,并且渲染到页面。

v-for 与 v-if 的优先级

v-for 的优先级比 v-if 高

Vue 的生命周期

什么是生命周期呢?

在不同的阶段,Vue 会提供一个钩子函数,让用户来执行自己的代码。这些钩子函数被称为生命周期函数。

有哪些生命周期?

有 8 个,分别是:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。

  • new Vue() 开始创建一个 Vue 的实例对象
  • Init Events & Lifecycle 表示刚初始化一个 Vue 空的实例对象,此时对象身上只有默认的生命周期函数和事件,其他的东西都未创建。 注意:在 beforeCreate 生命周期函数执行时,date、methods都未被初始化。
  • beforeCreate
  • Init injections & reactivity:在 created 中,data 、methods 都已经被初始化,之后才能开始调用它们。
  • created
  • 模板编译
  • beforeMount 模板已在内存中编译好,但未挂载到页面中去,此时页面还是旧的。
  • create vm.$el & replace el with it 把内存中的 $el--模板,替换到页面中去。
  • mounted 若要对页面中的 DOM 进行操作,最早在 mounted 中,执行完 mounted ,就表示整个 Vue 实例以及初始化完毕,此时脱离创建阶段,进入允许阶段。
  • beforeUpdate 当执行 beforeUpdate 的时候,页面中显示的数据未更新,此时 data 中的数据是最新的。
  • Virtual DOM re-render and patch 现根据 data 中的最新数据,在内存中,重新渲染一份最新的 DOM 树。然后再 Virtual DOM re-render and patch 把更新的虚拟 DOM 重新渲染到页面中去。
  • updated 页面和 data 数据保持同步,都是最新的。
  • beforeDestroy 当执行 beforeDestroy 时,Vue 实例就已经从运行阶段进入销毁阶段;但,身上所有的 data 和 methods 以及过滤器、指令等。都处于可用状态,还未真正执行销毁的过程。
  • destroyed 当执行 destroyed 时,Vue 实例已经完全被销毁,所有的 data 和 methods 以及过滤器、指令等。都已经不可用。

Vue 高级

  • 组件传参
    • Prop 透传
    • no-prop 透传
  • 子组件继承
  • $attrs
  • emit
  • 数据透传 个人理解:数据无视级别,能直接传到任意层级的组件中。
    • props 透传
    • no-props 透传
  • v-model 与 emit 关系
  • js 对象在内存中的存储方式
  • Vue 插槽 slot
  • Vue 渲染机制
  • 使用 rander 渲染虚拟节点
  • 模板和 jsx
  • jsx 实现插槽,深入理解 Vue 插槽