Vue学习笔记

Vue(发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架,vue两大特点:

  • 声明式渲染,基于标准HTML拓展一套模板语法,声明式描述最终HTML和JS的关系
  • 响应式,自动跟踪JS状态变化并响应式更新DOM

此时不学 更待何时,不想毕业就失业的我 (っ °Д °;) っ

构建方式

  1. vite-cdn

npm init -y

yarn add vite -D

在html中引入vue的cdn

  1. vue脚手架

npm init vue@latest

Vue 基础

初识 Vue

vue 的特性

  1. 数据驱动视图

    • 单向的数据绑定
    • vue监听数据变化,自动渲染页面结构
  2. 双向数据绑定

    • js 数据的变化,会被自动渲染到页面上
  • 页面上表单采集的数据发生变化的时候,会被 vue 自动获取到,并更新到 js 数据中

数据驱动视图 和 双向数据绑定 的底层原理是MVVM(Model 数据源、View 视图、ViewModel 就是vue的实例)

组件

vue应用

应用实例和根组件

应用实例是app,根组件的实例是vm,每个组件都有自己的组件实例,一个应用中所有的组件都共享一个应用实例

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例

传入 createApp 的对象实际上是一个根组件,其他组件将作为其子组件。

1
2
3
4
5
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'

const app = createApp(App)

大多数真实的应用都是由一棵嵌套的、可重用的组件树组成的。例如,一个待办事项 (Todos) 应用的组件树可能是这样的

1
2
3
4
5
6
7
8
App (root component)
├─ TodoList
│ └─ TodoItem
│ ├─ TodoDeleteButton
│ └─ TodoEditButton
└─ TodoFooter
├─ TodoClearButton
└─ TodoStatistics

挂载应用

应用实例必须在调用了 .mount() 方法后才会渲染出来,.mount() 方法应该始终在整个应用配置和资源注册完成后被调用

应用实例会被挂载到参数对应的节点中

1
app.mount('#app')

应用配置

应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,它将捕获所有由子组件上抛而未被处理的错误:

1
2
3
app.config.errorHandler = (err) => {
/* 处理错误 */
}

模板语法

模板渲染全流程:开发者写的template –> 分析HTML字符串 –> AST树 –> 把表达式/自定义属性/指令转成浏览器看得懂的语法 –> 虚拟DOM节点 –> 解析真实DOM –>render

  • vue的模板是基于HTML,模板中直接写HTML是可以被直接解析的

  • v- 作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,将为渲染的 DOM 应用特殊的响应式行为

  • 虚拟DOM存在的意义在于 让新的虚拟数据和旧的虚拟数据做对比,如果有差异,做更新,如果没差异,就不做更新

Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀,它也为内部属性保留 _ 前缀。

插值表达式

  • 双花括号内部可以使用表达式
  • 插值表达式 里的变量必须是实例中的数据,不可以用全局变量
  • :href=”” 双引号里面的属性值也可以使用表达式
  • :[]=”” 属性加上一个方括号,也可以使用表达式
    • 动态属性名参数不能出现空格和引号,因为HTML合法属性名不能出现空格和引号

表达式的使用规则如图所示:

image-20221030224306357

1
2
3
4
5
{{ number + 1}}

{{ ok ? 'yes' : 'no' }}

<div :id=" 'list-' + id"> </div>

data

data 必须是一个函数,Vue 在创建实例的过程中调用data函数,返回数据对象,通过响应式包装后存储在实例的 $data 中,并且实例可以直接越过 $data 访问属性

$data 是响应式数据对象,本质是通过 代理机制 生成一种数据响应式机制

$,_ ,__ 这些都是Vue提供的内置API,开发者尽量避免用这些前缀命名自己的变量和方法

为什么data 一定是个函数?

因为它要确保每一次执行函数后的返回来的数据引用都是独一无二的,这样多个实例之间的引用对象就不会相互影响

当然写源码的时候也可以通过深拷贝的方式去达到这种目的,但是没有通过每次执行函数 这种方式来的直接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 实现原理
function Vue(options){
this.$data = options.data;
var _this = this;

for(var key in this.$data){
(funtion(k){
//方法一:Object上的静态方法
Object.defineProperty(_this,k,{
get: function(){
return _this.$data[k];
},
set: function(newValue){
return _this.$data[k] = newValue;
}
})
//方法二:实例继承的原型方法
_this.__defineGetter__(k, function(){
return _this.$data[k];
});
_this.__defineSetter__(k, function(){
return _this.$data[k] = newValue;
});
})(key);
}
}

指令

为什么叫做指令?模板应该按照怎么的逻辑去渲染或绑定行为

v-once

一次插值,永不更新,不建议这么做,可以用es6字符串中的${}方式来实现单次插值绑定

v-html

  • {{ }},“Mustache”语法 ,插值表达式,内容占位符
  • v-html,可以将包含标签的字符串,渲染成真正的HTML内容

双花括号不会解析HTML,因为插值表达式是JS表达式,没有对于DOM的操作

不要试图用 v-html 做子模板,子模板中一些稍复杂的指令可能就无法渲染处理,比较好的解决方案是 把子模板放到子组件中

注意:在网站上动态渲染任意 HTML 是非常危险的,因为使用的方式是innerHTML,可以注入脚本工具,这非常容易造成 XSS 漏洞

仅在内容安全可信时再使用 v-html

v-bind

v-bind: 指令,简写为 :,作用:为元素属性添加 变量值

:bind就是为属性绑定插值,属性前不加冒号 后面的属性值统一视作 字符串

有时候,为了后面跟一个 JavaScript 表达式,也可以加一个v-bind,即使没有变量值绑定

1
2
3
//举例来说,如果你的组件实例有一个数据属性 attributeName,其值为 "href" 这个绑定就等价于 v-bind:href
// 动态属性名参数不能出现空格和引号,因为HTML合法属性名不能出现空格和引号
<a :[attributeName]="url"> ... </a>

布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。

1
<button :disabled="isButtonDisabled">Button</button>

isButtonDisabled真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略

v-on

v-on: 可以被简写为 @ 使用方法 @事件名称 = “事件处理函数 (参数)”

事件处理函数 在Vue实例下的 methods 中定义,this指的就是 创建的Vue实例对象

vue 内置变量 $event,它就是原生DOM 事件对象e,如果默认事件对象e被覆盖了,则手动传递$event

Vue 自动为 methods 绑定了永远指向组件实例的 this

不应该在定义 methods 时使用箭头函数,因为箭头函数this引用的是定义箭头函数的上下文,无论是methods:{}还是Vue.createApp({}),他们都是 一个对象,不是一个单独的作用域,箭头函数的this是指向Windows

1
2
3
4
5
6
7
8
<button @click="add(2, $event)">+1</button>
...
methods: {
add(step, e) {
e.target.style.backgroundColor = 'red'
this.count += step
}
}

事件修饰符

在事件处理函数中调用 event.preventDafault() 或 event.stopPropagation()是常见的需求,因此,vue提供了事件修饰符来辅助对事件触发进行控制,常用命令有:

  • .prevent ,阻止默认行为(如网页跳转、表单提交)
  • .stop,阻止事件冒泡
  • .capture,捕获模式触发当前的事件处理函数
  • .once,只触发一次
  • .self,只有在event.target 是当前元素自身时触发事件处理函数

按键修饰符

监听键盘,.enter .tab .delete .esc .space .up .down .ctrl .alt….很多

1
2
3
4
5
6
7
<input @keyup.enter='submit'> 
<input @keyup.esc='clearinput'>

methods{
submit(){},
clearinput(){}
}

v-model

只有表单数据能与用户产生交互,故表单数据使用v-model才有意义,适用于input、textarea、select

为了方便对用户输入的内容进行处理,v-model提供了3个修饰符,分别是

修饰符 作用 示例
.number 将用户输入转为数值类型 <input v-model.number="age" />
.trim 删除输入的首尾空白字符 <input v-model.trim="msg">
.lazy 当失去焦点时,才更新数据,类似防抖 <input v-model.lazy="msg">

v-if v-show

按需控制DOM的显示与隐藏,两者的显示效果一样,但仍有区别

可以使用 v-else-ifv-elsev-if 添加一个“else 区块”

实现原理不同:

  • v-if 通过创建或删除 DOM 元素来控制元素的显示与隐藏
  • v-show 通过添加或删除元素的 style="display: none" 样式来控制元素的显示与隐藏

性能消耗不同:

  • v-if 切换开销更高,如果运行时条件很少改变,使用 v-if 更好
  • v-show 初始渲染开销更高,如果切换频繁,使用 v-show 更好

image-20221030223638221

v-for

v-for 基于一个数组来循环渲染相似的UI结构

v-for 的使用需要 item in items 的特殊语法,可选的第二个参数表示属性名或者索引,即(item , index) in items,可选的第三个参数表示位置索引(value,key,index ) in items

使用key维护列表状态

  • 当列表数据变化时,vue复用已存在的DOM元素,从而提升渲染的性能,但这种默认性能优化策略,导致有状态的列表无法被正确更新
  • 为了让vue跟踪每个节点的身份,保证有状态的列表被正确更新的前提下,提升渲染性能,需要为每项提供一个唯一的key属性

key 的注意事项:

  • key 的值只能是字符串数字类型
  • key 的值必须具有唯一性(即:key 的值不能重复)
  • 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
  • 使用 index 的值当作 key 的值没有意义(因为 index 的值不具有唯一性)
  • 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)

数组变化侦测:

Vue 能够侦听响应式数组的变更方法,对原数组进行变更,包括push()pop()shift()unshift()splice()sort()reverse()

filter()concat()slice()这些方法为不可变方法,即不更改原数组,而是返回一个新数组

过滤器 (3版本已废弃)

过滤器本质就是JavaScript函数

  • filters常用于文本的格式化,被添加在JavaScript表达式的尾部,由“管道符 | ”进行调用,用于插值表达式和v-bind属性绑定上。
  • 过滤器只在 vue 2.xvue 1.x 中支持,vue 3.x 废弃了过滤器,官方建议使用计算属性或方法代替过滤器。

过滤器分为私有过滤器和全局过滤器,私有在实例vue对象里通过filters来定义,全局在main.js中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义私有过滤器
const vm = new Vue({
el:'#app',
data: {}
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase()
}
})

// 定义全局过滤器
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
})

如果私有过滤器和全局过滤器冲突,按照就近原则调用私有过滤器

连续调用多个过滤器

过滤器从左往右调用,前一个过滤器的结果交给下一个过滤器继续处理。

1
<p>{{ text | capitalize | maxLength }}</p>

methods属性

  1. Vue 创建实例时,自动为 methods 绑定了永远指向组件实例的 this,保证在事件监听时,回调始终指向当前组件实例,方法要避免使用箭头函数

  2. @函数名(),这里的括号不是执行符号,而是传入实参的容器;

    onClick(实参) 相当于 onClick = { () => change(实参) }

  3. 实例中直接挂载methods中的每一个方法,这些方法并不向外暴露

组件基础

单页面应用程序

single page application SPA,所有的功能与交互都在这唯一的页面中进行

特点

  • 仅在web页面初始化时加载相应的资源
  • SPA 进行页面重加载或跳转,利用JavaScript动态变化HTML内容

优缺点

优点 缺点
1. 良好交互体验
2. 良好的前后端工作分离模式
3. 减轻服务器压力
1. 首屏加载慢
2. 不利于SEO

首屏加载慢的处理办法

  • 路由懒加载
    • 将路由对应的组件打包成一个个js代码块,只有在这个路由被访问到的时候,才加载对应的组件
  • 代码压缩
  • CDN加速
    • CND加速主要是加速静态资源,如网站上面上传的图片、媒体,以及引入的一些Js、css等文件,需要依靠各个网络节点
  • 网络传输压缩

不利于SEO的处理办法

  • SSR 服务器端渲染

    Server Side Render,在服务端完成页面的 html 拼接处理, 然后再发送给浏览器,将不具有交互能力的 html 结构绑定事件和状态,在客户端展示为具有完整交互能力的应用程序。

vite目录结构及运行流程

通过main.js 把 App.vue 渲染到 index.html 的指定区域中

  • App.vue,待渲染的模板结构
  • index.html,需要预留el区域
  • main.js,把 App.vue 渲染至预留区

vue 组件

Vue 组件即单独的一个 .vue 文件,简称为 SFC

  • template,组件模板结构,必选

  • script节点,组件的JavaScript行为

    • name节点,说明该组件的名称
    • data节点,渲染期间需要用到的数据,不能直接定义,需要作为一个函数return出去,要确保每一次执行函数后的返回来的数据引用都是独一无二
  • style节点,组件样式 (scoped防止样式冲突,lang指定css语法)

    • <style>标签的lang属性默认是css,表示支持普通的css语法,可选值还有less、scss等

组件的注册使用

  • 组件引用原则:先注册后使用
  • 组件注册 分为 全局注册 和 局部注册
    • 全局注册,在main.js中 使用app.component() 方法注册,直接以标签的形式进行使用
    • 局部注册,在import导入,再使用component节点的键值对形式,后以标签的形式进行使用
  • 定义组件注册名称的方式只有两种
    • 短横线命名,my-search
    • 大驼峰命名,MySearch

组件的生命周期

创建、渲染、更新与销毁,创建后created会发Ajax请求初始数据,初次渲染后mounted会操作DOM元素

在实际开发中,created是最常用的生命周期函数!

mounted 只在元素第一次插入DOM时被调用,当DOM更新时mounted 不会被触发;updated函数会在每次DOM更新完成后被调用

如果mounted 和updated函数任务逻辑完全相同,可以简写为

1
2
3
app.directive('focus',(el)=>{
el.focus()
})

scoped解决组件间样式冲突

默认情况下,vue组件下的样式会全局生效,容易导致多个组件间的样式冲突问题,其原因是:

  • 单页面程序中,所有组件的DOM结构,都是基于唯一的html页面呈现的
  • 每个组件的样式,都会影响唯一页面的DOM元素

style节点的 **scoped **属性,为每个组件分配唯一的自定义属性,从而防止组件之间的样式冲突问题。

但如果想让某些样式对子组件生效,可以使用 深度选择器 :deep()

1
2
<style lang="less" scoped>
</style>

class与style绑定

数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。

classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。

1
2
3
4
5
<h3 :class="isItalic ? 'italic' : ''"></h3>

<h3 :class="[isItalic ? 'italic':'','isDelete' ? 'delete':'']"</h3>

<h3 :style="{color:color,fontSize:fsize}"></h3>

props属性

props是组件的自定义属性,通过props把数据从父组件传递到子组件内部供其使用

  • 使用 v-bind 或缩写 : 来进行动态绑定的 props
  • 所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
  • 不应该在子组件中去更改一个 prop,更改一个 prop 的需求通常来源于以下两种场景
    • prop 被用于传入初始值
    • 需要对传入的 prop 值做进一步的转换

props的使用

  1. 在子组件中定义声明
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<h3>title:{{title}}</h3>
<h3>author:{{author}}</h3>
</div>
</template>

<script>
export default {
name:'MyArticle'
props:['title','author']
}
</script>
  1. 在父组件中导入引用
1
2
3
4
5
6
7
8
9
10
// 动态绑定的 props
<MyArticle :title="post.title" :author="post.author"></article>

<script>
import MyArticle from './Article.vue'
export default {
name:'MyApp'
component:{ MyArticle }
}
</script>

props校验规则

使用对象类型的props节点,可以对外界传递的props数据进行合法性校验

  • 可以使用字符串数组、对象的形式来声明 prop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
export default {
name:'MyArticle'
props:{ //8种类型:String、Number、Boolean、Array、Object、Date、Function、Symbol
//多种数据类型 用列表
title: [String,Number],
//必填项校验
propA:{
type:String,
required:true
},
//指定默认值
propA:{
type:String,
default:100
}
}
}
</script>

计算属性

计算属性本质就是function,监听数据,返回新值,以function形式声明到组件的computed选项中

计算属性值会基于其响应式依赖被缓存,仅会在其响应式依赖更新时才重新计算,即计算属性会缓存计算的结果,性能会更好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<p>{{ count }}乘以2的值为{{ plus }}</p>

<script>
export default {
name:'MyArticle',
data(){
return { count : 1}
},
computed:{
plus(){
return this.count*2
}
}
}
</script>

计算属性默认是只读的,在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[this.firstName, this.lastName] = newValue.split(' ')
}
}
}
}

组件事件(子传父)

子组件中的触发

this.$emit( ‘自定义事件的名称’, 参数 ) ,所有传入 $emit() 的额外参数都会被直接传向监听器。举例来说,$emit('foo', 1, 2, 3) 触发后,监听器函数将会收到这三个参数值。

  1. 可以在模板表达式中,直接使用 $emit 方法触发自定义事件
  2. 也可以在组件实例上定义
1
2
3
4
5
6
7
8
9
10
<button @click="$emit('change',count)">click me</button>

<script>
export default {
emits:['change'],
methods:{
this.$emits('change', this.count)
}
}
</script>

事件校验

事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit 的内容,返回一个布尔值来表明事件是否合法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
emits: {
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
}

父组件的监听

父组件通过 v-on (缩写为 @) 来监听事件,也支持 .once 修饰符:

1
<MyComponent @some-event.once="callback" />

无冒泡机制

组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案

组件上的v-model

需要维护内外组件数据同步时,可以使用v-model指令

v-model不适用于双向绑定至props值,props值是只读的,需要的话,可以把props值转存到data中

父传子 ,v-bind + props

  • 父组件通过 v-bind: 属性绑定的形式,把数据传递给子组件

  • 子组件中,通过 props 接受父组件传递过来的数据

子传父,v-model: + props + emits:[‘update: xxx’]

  • v-bind: 指令前添加v-model指令
  • 在子组件中声明 emits 自定义事件,格式为 update:xxx
  • 调用 $emit( update:xxx ) 触发自定义事件,更新父组件中的数据

watch侦听器

监听数据变化,针对数据变化做特定操作,在watch节点下定义,将变量名直接当成方法来调用

计算属性vs侦听器,侧重的应用场景不同:

  • 计算属性:监听多个值变化,计算后返回新值
  • 侦听器:监听单个值变化,执行特定业务,不需要有返回值

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组,不能直接侦听响应式对象的属性值

深层侦听:当watch侦听的是一个对象,如果对象中的属性值发生了变化,此时需要加上deep选项才可被监听

监听单个属性:只想监听对象中单个属性变化,则可以 'info.username':{ }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
// 加上 deep 选项,深层侦听器
watch(
() => state.someObject,
(newValue, oldValue) => {
},
{ deep: true }
)

watchEffect

watch() 是懒执行的:仅当数据源变化时,才会执行回调,但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。这时就用到了watchEffect

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项:

1
2
3
4
5
6
7
watch(source, callback, {
flush: 'post'
})

watchEffect(callback, {
flush: 'post'
})

停止侦听器

侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>

组件之间的数据共享

父子共享

父传子,父通过v-bind向子共享,子需要props接受数据;子传父,子通过自定义事件向父共享;父子之间的双向同步,使用 v-model 指令维护组件内外数据的双向同步

兄弟共享

EventBus,借助第三方包mitt来创建eventBus对象

后代关系组件间的数据共享(依赖注入)

后代关系,指的是 父组件向其子孙组件共享数据,使用 provide 和 inject 实现。

  • 父组件通过 provide节点 向子孙共享数据
  • 子孙组件使用 inject 数组 接受数据

provide()函数接收两个参数

  • 第一个参数被称为注入名,可以是一个字符串或是一个Symbol,第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref

  • 除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖

  • 正在构建大型的应用,包含非常多的依赖提供,建议最好使用 Symbol 来作为注入名以避免潜在的冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
location.value = 'South Pole'
}

provide('location', {
location,
updateLocation
})
</script>

<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
<button @click="updateLocation">{{ location }}</button>
</template>

inject

在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似

1
2
// 如果没有祖先组件提供 "message",// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

插槽

插槽slot,组件封装期间,为用户预留的内容占位符

使用场景:在封装组件的时候,如果不确定组件的DOM渲染成什么样子,同时需要把数据交给用户,可以通过作用域插槽传给用户,从而提高组件复用率!以下是用原生JavaScript的实现方式:

1
2
3
4
5
6
7
8
9
// 父元素传入插槽内容
FancyButton('Click me!')

// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}

渲染作用域

  • 插槽内容可以访问父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的
  • 父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

默认内容

这是后备内容,如果没有为插槽提供任何内容,则默认内容会生效

具名插槽

如果要预留多个插槽,则为每个指定具体的name名称;没有指定name名称的插槽,默认名称为 “default”;引用时需要外包裹 <template v-slot:“插槽名”> ;v-slot: 可以简写为 #

1
2
3
4
5
6
7
8
9
10
11
12
13
// 子组件BaseLayout
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

1
2
3
4
5
6
// 父组件使用
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>

作用域插槽

可以为slot插槽绑定 props数据,即作用域插槽,搭配 解构赋值使用

1
2
3
4
5
6
7
8
9
10
11
12
//定义 组件
<template>
<slot name = "header" :info=''>这是后备内容</slot>
<slot name = "footer">这是后备内容</slot>
</template>

//引用组件
<zujian>
<template v-slot:header='{info}'>
<p>1111</p>
</template>
</zujian>

自定义指令

自定义指令,声明 不需要加 v-,使用 需要加 v-,自定义指令分为私有和全局两种

在绑定指令时,可以通过 “等号” 形式为指令绑定具体参数值

引用时,(el,binding) el指该组件,binding.value为指令绑定值

  • 私有自定义指令
  • <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令
  • 在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册
  • 全局自定义指令,需要在 SPA 实例对象 main.js 里声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. 
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
2.
export default {
setup() {
},
directives: {
// 在模板中启用 v-focus
focus: {}
}
}

//3. 全局
const app = createApp(App)
app.directive('focus',{
......
})

指令钩子

定义对象可以提供几种钩子函数

created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted

1
2
3
4
const myDirective = {
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},

钩子参数

  • el:指令绑定到的元素,可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性
    • value:传递给指令的值
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用
    • arg:传递给指令的参数 (如果有的话)
    • modifiers:一个包含修饰符的对象 (如果有的话)
    • instance:使用该指令的组件实例
    • dir:指令的定义对象
  • vnode:代表绑定元素的底层 VNode
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode

组件库

路由

路由的本质就是 对应关系

  • 前端路由:又称为客户端路由,具体表现为Hash地址与组件之间的对应关系。客户端的 JavaScript 可以拦截页面的跳转请求,动态获取新的数据,然后在无需重新加载的情况下更新当前页面。

  • 后端路由:又称为服务端路由,请求方式、请求地址与function处理函数之间的对应关系。

    服务器根据用户访问的 URL 路径返回不同的响应结果。当我们在一个传统的服务端渲染的 web 应用中点击一个链接时,浏览器会从服务端获得全新的 HTML,然后重新加载整个页面。

SPA单页面项目中,不同组件内的切换通过 前端路由 来实现!

前端路由工作方式

  1. 用户点击页面上的路由链接
  2. URL地址栏中的Hash值发生变化
  3. 前端路由监听Hash地址的变化
  4. 前端路由把当前 Hash 地址对应的组件渲染到浏览器中

从头实现路由

如果你只需要一个简单的页面路由,而不想为此引入一整个路由库,你可以通过动态组件的方式,监听浏览器 hashchange 事件或使用 History API 来更新当前组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import NotFound from './NotFound.vue'
const routes = {
'/': Home,
'/about': About
}
const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => {
currentPath.value = window.location.hash
})
const currentView = computed(() => {
return routes[currentPath.value.slice(1) || '/'] || NotFound
})
</script>
<template>
<a href="#/">Home</a> |
<a href="#/about">About</a> |
<a href="#/non-existent-path">Broken Link</a>
<component :is="currentView" />
</template>

vue-router

vue-router 3.x 只能结合 vue2 进行使用;vue-router 4.x 只能结合 vue3 进行使用

  • 安装并定义路由组件 npm i vue-router@next -S
  • 声明路由链接 <router-link to='home'> 和路由占位符 <router-view>
  • 创建路由模块,导入并挂载
    • 从 vue-router 中按需导入两个方法
    • 导入所需路由控制的组件
    • 创建路由示例对象并向外共享
    • 在main.js中导入并挂载 app.use()

路由重定向

用户访问地址A时,强制用户跳转地址C,通过redirect属性来指定

被激活的路由链接

被激活的路由链接,默认应用一个 router-link-active 类名,可以用此类明选择器,来设置不同的样式

也可以基于 linkActiveClass属性,自定义被激活路由链接所应用的类名

嵌套路由

组件的嵌套展示

  1. 声明 子路由链接和子路由占位符
  2. 在父路由规则中,通过children属性嵌套声明子路由规则
  3. 子路由的hash地址不要以 / 开头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {createRouter,createWebHashHistory} from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
// 默认的router-link-active类名会被覆盖
linkActiveClass:'router-active'
routes:[
{ path:'/',redirect:'/home'},
{ path:'/home',component: Home},
{ path:'/movie',component: Movie},
{ path:'/about',component: About,
//通过 children 属性嵌套子路由规则
children:[
{ path:'tab1',component:Tab1 },
{ path:'tab2',component:Tab2 },
]
},
]
})

export default router

动态路由

把 Hash 地址中可变的部分通过 定义为 参数项,从而提高规则复用性

1
{ path:'/movie/:id', component: Movie }
$route.params 参数对象

通过动态路由匹配渲染出来的组件中,使用$route.params 来访问动态匹配的参数值

1
2
3
4
5
{ path:'/movie/:id', component: Movie}

<template>
<h3> {{ $route.params.id }} </h3>
</template>
使用props接受路由参数

vue-router允许在路由规则中开启props传参

1
2
3
4
5
6
7
8
9
10
11
{ path:'/movie/:id', component: Movie, props:true}

<template>
<h3> {{ id }} </h3>
</template>

<script>
export default{
props:['id']
}
</script>

编程式导航

  • 声明式导航:调用API实现导航的方式 eg: location.href
  • 编程式导航:点击链接实现导航的方式 <a> <router-link>

两个常用的API:

  1. this.$router.push(‘hash地址’),跳转到指定hash地址,展示对应组件
  2. this.$router.go(数值N),实现浏览历史的前进和后退
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<button @click='gotoMovie(3)'> go to movie</h3>
</template>

<script>
export default{
methods:{
gotoMovie(){
this.$router.push('/movie/${id}')
}
}
}
</script>

命名路由

通过name属性为路由规则定义名 称的方式,叫做命名路由

1
{ path:'/movie/:id',name:'mov', component: Movie }
使用命名路由实现声明式导航

name属性指定跳转路由规则,params属性指定携带参数

1
<router-link :to="{name:'mov',params:{id:3}}"></router-link>
使用命名路由实现编程式导航

调用push函数指定配置对象

导航守卫

控制路由的访问权限

  • 全局导航守卫,接受三个形参(to, from, next)
    • to,往哪去
    • from,从哪来
    • next,放行方法。如果不声明next形参,默认允许访问每一个路由;如果声明,必须调用next(),否则不允许访问任何一个路由。
    • 直接放行:next() ; 强制停留当前页面:next(false) ; 强制跳转登录页面:next(‘/login’)
1
2
3
4
5
6
7
8
9
10
const router = createRouter({......})
// fn是一个函数,每次拦截到路由请求前,都会调用fn函数
router.beforeEach((to,from,next)=>{
const token = localStorage.getItem('token')
if(to.path === '/main' && !token){
next('/login')
} else{
next()
}
})

vue组件库

前端开发者将自己封装的vue组件整理打包并发布为npm包,被称为vue组件库

vue组件库与bootstrap区别

  • bootstrap:提供原材料(css、html结构、js特效)
  • vue组件库:遵循vue语法、高度定制的现成组件

常用组件库

  1. pc端
  2. 移动端

项目引入

  • 完整引入
  • 按需引入,在官方文档中自己看,不再赘述
1
2
3
4
5
6
// main.js文件中
import Vue from 'vue'
import ElementUI from 'element-Ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

proxy跨域代理

如果项目运行地址和后端接口地址 存在着跨域问题,则不能正常访问到数据

两种解决办法:

  • 后端接口 开启CORS跨域资源共享
  • 前端通过代理解决接口的跨域问题

axios数据请求

axios,用于数据请求,几乎每个组件都会用到,一般可以在全局配置好。在main.js 入口文件中,通过app.config.globalProperties全局挂在 axios

如下图,1,2是配置 ,3 是如何使用

内置组件

KeepAlive

一般来说,多个组件间动态切换时会创建新的组件实例;但有些时候,我们需要缓存组件实例,这时就用到了keepalice, <KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。

1
2
3
<keep-alive>
<component is='要渲染的组件名称'> </component>
</keep-alive>

缓存实例的生命周期

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

一个持续存在的组件可以通过 onActivated()onDeactivated() 注册相应的两个状态的生命周期钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>

通用API

应用实例

app.config

每个应用实例都会暴露一个 config 对象,其中包含了对这个应用的配置设定。

app.config.errorHandler

为应用内抛出的未捕获错误指定一个全局处理函数,接收三个参数:错误对象、触发该错误的组件实例和一个指出错误来源类型信息的字符串

1
2
3
app.config.errorHandler = (err, instance, info) => {
// 处理错误,例如:报告给一个服务
}

app.config.warnHandler

为 Vue 的运行时警告指定一个自定义处理函数,警告仅会在开发阶段显示,因此在生产环境中,这条配置将被忽略;

将接受警告信息作为其第一个参数,来源组件实例为第二个参数,以及组件追踪字符串作为第三个参数。

1
2
3
app.config.warnHandler = (msg, instance, trace) => {
// `trace` is the component hierarchy trace
}

app.config.performance

设置此项为 true 可以在浏览器开发工具的“性能/时间线”页中启用对组件初始化、编译、渲染和修补的性能表现追踪。仅在开发模式和支持 performance.mark API 的浏览器中工作。

$nextTick方法

$nextTick(cb),会把cb回调推迟到下一个DOM更新周期之后执行。

即等到组件的DOM异步重新完成渲染后,再执行cb回调函数,从而保证回调函数可以操作到最新的DOM元素。

ref 访问DOM元素

ref 可以按需直接访问底层 DOM 元素

1
2
3
4
5
6
7
<h3 ref='input'></h2>

methods:{
getRef(){
console.log(this.$refs.input)
}
}

组合式API

setup()

setup() 钩子是在组件中使用组合式 API 的入口,在 setup() 函数中返回的对象会暴露给模板和组件实例

setup()自身并不含对组件实例的访问权,即在setup()中访问this会是undefined

setup() 有两个参数

  • 第一个参数是组件的 propssetup 函数的 props 是响应式的,推荐通过 props.xxx 的形式来使用其中的 props,不要做解构;
  • 第二个参数是一个 Setup 上下文对象
1
2
3
4
5
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态。

1
2
3
4
5
6
7
8
import { h, ref } from 'vue'

export default {
setup() {
const count = ref(0)
return () => h('div', count.value)
}
}

返回一个渲染函数将会阻止我们返回其他东西,此时想暴露组件方法给父组件,可以调用expose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { h, ref } from 'vue'

export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value

expose({
increment
})

return () => h('div', count.value)
}
}

响应式基础

reactive

使用 reactive()函数创建一个响应式对象或数组,reactive() 返回的是一个原始对象的 Proxy,reactive 是允许多层嵌套的深度代理!

1
2
3
4
5
const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

reactive() API 有两点注意:

  1. 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型,而对 stringnumberboolean 这样的 原始类型 无效。
  2. 不可以随意地“替换”一个响应式对象,不可以将响应式对象的属性赋值或解构,以上都会失去响应性

ref

ref()方法来允许我们创建可以使用任何值类型的响应式 refref() 将传入参数的值包装为一个带 .value 属性的 ref 对象

1
2
3
4
5
6
7
const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
  • ref 被解构时,不会丢失响应性,即没有 reactive的限制性
  • 当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value

isref

检查某个值是否为 ref,返回布尔值

unref

unref就是一个语法糖,unref( info ) === isRef(info) ? info.value : info

toRef

创建一个对应的 ref,创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然

toRefs()

每个单独的 ref 都是使用 toRef() 创建的,常用于结构

渲染机制

虚拟DOM

虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。

这里的 vnode 即一个纯 JavaScript 的对象 (一个“虚拟节点”),它代表着一个 <div> 元素。它包含我们创建实际元素所需的所有信息。它还包含更多的子节点,这使它成为虚拟 DOM 树的根节点。

1
2
3
4
5
6
7
8
9
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}

渲染管线

编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。

挂载:一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。

更新:如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。

模板 vs. 渲染函数

Vue 模板会被预编译成虚拟 DOM 渲染函数。Vue 也提供了 API 使我们可以不使用模板编译,直接手写渲染函数。

在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,可以完全地使用 JavaScript 来构造想要的 vnode。

渲染函数基本用法

h函数

Vue 提供了一个 h() 函数用于创建 vnodes,h()hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”,更准确名称是 createVnode()

Vnodes 必须唯一,多个child,不要使用同一个h返回的虚拟节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { h } from Vue;

export default {
render(){
return h(
// {String | Object | Function} tag 必需的。
'div',
// {Object} props 与 attribute、prop 和事件相对应的对象。没有props,传null或{}
{},
// {String | Array | Object} children 可选的。子 VNodes, 使用 `h()` 构建, 或使用字符串获取 "文本 VNode" 或者
'123'
)
}
}

JSX

JSX 是 JavaScript 的一个类似 XML 的扩展,create-vue 和 Vue CLI 都有预置的 JSX 语法支持。在 JSX 表达式中,使用大括号来嵌入动态值;单标签必须要闭合

1
const vnode = <div id={dynamicId}>hello, {userName}</div>

Vue 的 JSX 编译方式与 React 中 JSX 的编译方式不同,具体包括:

  • 可以使用 HTML attributes 比如 classfor 作为 props - 不需要使用 classNamehtmlFor
  • 传递子元素给组件 (比如 slots) 的方式不同

几个常见的用等价的渲染函数 / JSX 语法,实现模板功能的案例:

1
2
3
4
5
6
7
8
v-if
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
// 等价于使用如下渲染函数 / JSX 语法:
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>

DIFF算法

对比新老虚拟节点树,以最小的代价去修改DOM

不会跨级对比,平级对比看索引

深度优先,不是广度优先

参考资料