跳到主要内容

Vue3

组合式 API

vue3 新增setup()语法,包括两个参数propscontext。定义的变量和方法等都在setup()里面。

编码技巧

  1. 使用组合式 API。按功能块写代码,避免将一堆 ref 变量写在一块
  2. 使用 setup 语法糖,即<script setup>编写组件

需要掌握的 API:

  • defineProps()defineEmits()
  • defineModel()
  • defineExpose()
  • defineOptions()
  1. 使用 ref 声明响应式变量,尽量避免使用 reactive

安装官方的 VScode 插件:Vue-Official,然后勾选上 Auto-complete Ref value with .value 就可以自动补全 .value

  1. v-ifv-for不能在同一元素上使用,可以使用template标签
<ul>
<template v-for="item in list" :key="item.id">
<li v-if="item.isActive">{{ item.name }}</li>
</template>
</ul>

ref 和 reactive

ref

refreactive 二者都能定义响应式变量。官方建议使用 ref() 作为声明响应式状态的主要 API。

ref 可以定义基本类型和引用类型,将其变为响应式变量。

在 script 中使用时,变量名要带上.value,在 template 中使用时不用。

<template>
<button v-for="(item, index) in user" :key="index" @click="clickName(index)">{{ index }} : {{ item }}</button>
<h1>name: {{ name }}</h1>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const user = ref<string[]>(['tom', 'jack', 'ivan']);
const name = ref<string>('');
const clickName = (index: number) => {
name.value = user.value[index];
};
</script>

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

若要避免这种深层次的转换,可以使用 shallowRef() 来替代。

下面示例中可以看出,通过 ref 包裹的对象,内部是通过reactive()将其变为响应式对象的。

import { ref, reactive } from 'vue';

const msg = ref('Hello World!');
console.log(msg);

const foo = ref({ name: 'vue' });
console.log(foo);

const bar = reactive({ name: 'vue' });
console.log(bar);

要获取原始值可以通过 toRaw()

const list = ref([1, 2, 3]);
console.log(list.value); // Proxy

const raw = toRaw(list.value);
console.log(raw); // [1, 2, 3]

reactive

  • 返回一个对象的响应式代理
  • 不能用于原始类型,只能用于对象类型
  • 使用时不带.value
  • 可以用toRefs()将其转换成ref
<template>
<button v-for="(item, index) in data.list" :key="index" @click="data.btnFun(index)">{{ index }} : {{ item }}</button>
<h1>{{ data.listName }}</h1>
</template>

<script setup lang="ts">
import { reactive } from 'vue';

interface DataProps {
list: string[];
listName: string;
btnFun: (index: number) => void;
}

const data: DataProps = reactive({
list: ['first', 'second', 'third'],
listName: '',
btnFun: (index: number) => {
data.listName = data.list[index];
}
});
</script>

reactive 在重新分配对象时,会失去响应式。

let state = reactive({ count: 0 });

// 修改里面的属性没问题,依然是响应式
state.count = 1;

// 丢失响应性,上面的 { count: 0 } 引用将不再被追踪
state = { count: 1 };

// 即使用reactive包裹,依然会丢失响应式。这时就是一个新的响应式对象
state = reactive({ count: 1 });

在实际开发中,如果是通过接口返回的数据,不可能按照属性一个个的修改,如下所示:

const ajaxData = { count: 2, foo: 3, bar: 4 };
state.count = ajaxData.count;
state.foo = ajaxData.foo;
state.bar = ajaxData.bar;
// ...

这时可以通过使用 Object.assign() 来处理。

Object.assign(state, ajaxData);

使用 ref 可以直接替换:

let state = ref({ count: 0 });
state.value = ajaxData;

reactive 解构后会丢失响应性,可以使用 toRefs()

let person = reactive({ name: 'zgh', age: 18 });

// 这么解构会丢失响应性
let { name, age } = person;
name = 'lrx';

// 通过 toRefs() 解决
let { name, age } = toRefs(person);
name.value = 'lrx';
// 这时 person.name 的值也变为 lrx

在 template 中,可以使用 {{ name }},也可以使用 {{ person.name }}

ref 和 reactive 的区别

  1. ref 可以定义基本类型和引用类型,而 reactive 只能定义引用类型。
  2. ref 取值要用 .value,reactive 可以直接访问。
  3. ref 解构后仍然保持响应性,reactive 解构后会丢失响应性,可以使用 toRefs()

为什么 ref 需要 .value 属性

因为 Vue3 使用 Proxy 实现响应式。 Proxy 对对象或数组的每个属性进行深度代理,因此可以追踪嵌套属性的变化。而 Proxy 无法直接处理基本数据类型(如 number、string、boolean ),这使得 reactive 无法用于基本数据类型。为了实现基本数据类型的响应式,Vue 设计了 ref ,它将基本数据类型封装为一个包含 value 属性的对象,并通过 getter 和 setter 进行依赖追踪和更新。当访问或修改 ref.value 时,Vue 会触发依赖更新。

计算属性

计算属性 computed(),默认是只读的。使用 gettr 和 setter 可读可写。

import { ref, computed } from 'vue';

const firstName = ref('zhang');
const lastName = ref('san');

// 只读
const fullName = computed(() => firstName.value + lastName.value);

// 可读可写
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value;
},
set(newValue) {
const [n1, n2] = newValue.split(' ');
firstName.value = n1;
lastName.value = n2;
}
});

// 注意使用计算属性得到的是一个 ref
function changeFullName() {
fullName.value = 'Li si';
}

watch 监听

先导入import { watch } from 'vue'。第一个参数是要监听的变量,第二个是回调函数。如果要监听多个变量,那么第一个参数传入数组

监听类型

  1. ref 定义的变量
const count = ref(0);
watch(count, newVal => {
console.log(newVal);
});

// 获取旧值
watch(count, (newVal, old) => {
console.log(newVal, old);
});

注意:监听 ref 定义的变量时,不用写.value,因为用了.value就是要监听对象的属性,是一个具体值,而不是一个响应式对象。

  1. 一个有返回值的函数
watch(
() => count.value,
newVal => {
console.log(newVal);
}
);
  1. 一个数组
const count = ref(0);
const num = ref(0);

// newVal可以解构赋值
watch([count, num], newVal => {
console.log(newVal); // [1, 0] ...
});

watch([count, () => num.value], newVal => {
console.log(newVal);
});
  1. reactive 定义的变量,不能直接监听,需要返回一个函数
const obj = reactive({ a: 1, b: 2 });
watch(
() => obj.a,
newVal => {
console.log(newVal);
}
);

立即监听

const count = ref(0);

watch(
count,
(newValue, oldValue) => {
// 立即执行,且当 count 改变时再次执行
},
{ immediate: true }
);

深度监听

  1. 直接传入会隐式创建深度监听。任何属性变化了都会触发回调函数。
const obj = reactive({ a: 1, b: 2 });

watch(obj, newVal => {
console.log(newVal.b);
});

function btn() {
obj.a++;
}
  1. 返回一个函数,监听返回的属性,只有这个属性发生了变化,才会触发回调函数。也可以显式地加上 deep 选项,强制转成深层侦听器。
watch(
() => obj.a,
newVal => {
console.log(newVal);
},
{ deep: true }
);

生命周期

setup()开始创建组件之前,在beforeCreate()created()之前执行

<script setup lang="ts">
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onRenderTriggered,
onRenderTracked,
onBeforeUnmount,
onUnmounted
} from 'vue';

console.log('开始创建组件');

onBeforeMount(() => {
console.log('挂载前');
});

onMounted(() => {
console.log('完成挂载');
});

onBeforeUpdate(() => {
console.log('更新前');
});

onUpdated(() => {
console.log('完成更新');
});

onRenderTriggered(event => {
console.log('状态触发');
});

onRenderTracked(event => {
console.log('状态跟踪');
});

onBeforeUnmount(() => {
console.log('卸载之前');
});

onUnmounted(() => {
console.log('卸载完成');
});
</script>

vue2 和 vue3 生命周期对比:

beforeCreate  -> setup()
created -> setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
activated -> onActivated
deactivated -> onDeactivated
errorCaptured -> onErrorCaptured

响应式原理

vue3 的响应式是基于 ProxyReflect

参考 Vue3 为什么用 Proxy 代替 Object.defineProperty

简易实现对象的响应性

Proxy + Reflect 实现:

function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key);
}
});
}

调用一下:

const obj = { foo: 1, bar: 1 };

let p = reactive(obj);
p.foo++;
delete p.bar;

console.log(obj);

在深层对象里,如 const p = reactive({ foo: { bar: 1 } }),这时调用 p.foo.bar = 2 则不会触发 setter,所以会丢失响应性。

最终代码:

function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);

// 将深层对象变成响应式数据
if (typeof res === 'object' && res !== null) {
return reactive(res);
}

console.log('GET', key, res);
return res;
},
set(target, key, newVal, receiver) {
console.log('触发了setter');

const oldVal = target[key];

// 对象属性的增加和修改都会触发 set,所以需要区分
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD';
const res = Reflect.set(target, key, newVal, receiver);

if (oldVal !== newVal) {
console.log(type, key, newVal);
}

return res;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);

const res = Reflect.deleteProperty(target, key);

if (res && hadKey) {
console.log('DELETE', key, res);
}

return res;
}
});
}

const obj = {
foo: 1,
bar: 2,
interest: { count: 2 }
};
const p = reactive(obj);
p.a = 1;
p.foo++;
delete p.bar;

p.interest.count = 3;

console.log(target);

简易版本 Vue3 双向数据绑定

功能:通过 v-model 绑定一个值的同时,v-bind 的 dom 元素可以实现双向数据绑定。

代码如下:

<div id="container">
用户名:
<input type="text" id="user" v-model="name" />
密码:
<input type="password" v-model="pwd" />
<h1 v-bind="name"></h1>
<h2 v-bind="pwd"></h2>
<button onclick="onChange()">change</button>
</div>

<script>
const container = [...document.querySelector('#container').children];

const target = { name: '', pwd: '' };

let proxyObj = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, newVal, receiver) {
container.forEach(dom => {
if (dom.getAttribute('v-bind') === key) {
dom.innerHTML = newVal;
}
if (dom.getAttribute('v-model') === key) {
dom.value = newVal;
}
});

return Reflect.set(target, key, newVal, receiver);
}
});

function onChange() {
proxyObj.name = 'hello';
proxyObj.pwd = 'world';
}

container.forEach(dom => {
if (dom.getAttribute('v-model') in proxyObj) {
dom.addEventListener('input', function () {
proxyObj[this.getAttribute('v-model')] = this.value;
});
}
});
</script>

首先获取到所有的 dom 节点,然后使用Proxy代理{text: "", password: ""}对象。 遍历所有的 dom 节点,如果某个节点有v-model属性,且属性值在代理对象中,那么就监听输入框的变化, 将该节点的值(input 框内的值)赋值给代理对象对应的属性,从而实现简单的双向数据绑定

提示
  • v-modelv-bind的属性值要相同,如都是 text 或都是 password
  • dom.addEventListener("input", function() {})这里不能使用箭头函数,否则 this 指向 Window 对象

v-model 原理

在 3.4 版本以后,使用 defineModel()

在 3.4 版本以前:

Parent.vue
<Child v-model="foo" />
Child.vue
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>

<template>
<input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

父组件中的 v-model="foo" 将被编译为:

Parent.vue
<Child :modelValue="foo" @update:modelValue="$event => (foo = $event)" />
备注

如果在 TS 中,可以断言 $event.targetHTMLInputElement 类型,而不是 null

(<HTMLInputElement>$event.target).value;

自定义 hooks

如果在一个组件里定义了很多的 data 和方法,写法就类似 vue2 的写法了。可以按功能将数据和方法放到一个单独的文件中,文件名以 use 开头,比如 useCounter.tsuseSum.ts 等。

例如,在 src 目录下新建 hooks 文件夹,建立一个useMousePosition.ts文件,功能是获取鼠标位置

import { ref, onMounted, onUnmounted } from 'vue';

export default function mousePosition() {
const x = ref(0);
const y = ref(0);

function update(e: any) {
x.value = e.pageX;
y.value = e.pageY;
}

onMounted(() => {
window.addEventListener('mousemove', update);
});

onUnmounted(() => {
window.removeEventListener('mousemove', update);
});

return { x, y };
}

然后在使用它的组件中导入,即可在模板中使用

<template>
<h1>鼠标位置:{{ x }} - {{ y }}</h1>
</template>

<script setup lang="ts">
import mousePosition from '@/hooks/useMousePosition';

const { x, y } = mousePosition();
</script>

customRef()

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

例如:在输入框中输入,1 秒后数据才变化。如果使用 ref ,输入时就立即变化了。

<template>
<h1>{{ msg }}</h1>
<input v-model="msg" />
</template>

<script setup>
import { customRef } from 'vue';

let initValue = 'hello';
let timer = null;

const msg = customRef((track, trigger) => {
return {
get() {
// 依赖追踪,告诉vue要关注msg的变化,一变化就去更新页面
track();
return initValue;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
initValue = newValue;
// 触发更新,通知vue数据变化了
trigger();
}, 1000);
}
};
});
</script>

继续将上面的功能封装成一个自定义的 hook,叫 useDebounceRef.ts,其实就是防抖

import { customRef } from 'vue';

export default function (initValue: string, delay: number) {
let timer: number;

return customRef((track, trigger) => {
return {
get() {
track();
return initValue;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
initValue = newValue;
trigger();
}, delay);
}
};
});
}

使用:

<script setup>
import { useDebouncedRef } from './useDebounceRef';
const text = useDebouncedRef('hello');
</script>

<template>
<input v-model="text" />
</template>

unplugin-auto-import

unplugin-auto-import 是一个 Vite 插件,用于自动导入 Vue 3 的 API 以及常用的第三方库。

pnpm add -D unplugin-auto-import

用了这个插件,在组件中就不用写import {} from 'vue'了,直接使用 API 即可。

// import { computed, ref } from 'vue';

const count = ref(0);
const doubled = computed(() => count.value * 2);

配置步骤:

  1. vite.config.ts中添加插件:
vite.config.ts
import AutoImport from 'unplugin-auto-import/vite';

export default defineConfig({
plugins: [
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: './auto-imports.d.ts'
})
]
});
  1. 添加dts: './auto-imports.d.ts'后,会在项目根目录自动生成 auto-imports.d.ts文件,自动生成类型声明。
  2. 如果有报错说 API 找不到,可以配置tsconfig.app.json文件
{
"include": ["*.d.ts", "src/**/*", "src/**/*.vue"]
}

如果是 Vite 生成的项目,这里可能只有一个env.d.ts声明文件,改为通配符*.d.ts即可。

Eslint 配置

vue3 结合 TypeScript 使用时,如果在组件的首行报错:Component name "index" should always be multi-word.eslint,意思是你的组件名称应该始终是多词的,如把index.vue变成indexView.vue。或者修改.eslintrc.cjs禁用这个规则:

  1. 要禁用所有文件中的规则:
module.exports = {
rules: {
'vue/multi-word-component-names': 0
}
};
  1. 仅禁用src/views/**/*.vue的规则:
module.exports = {
overrides: [
{
files: ['src/views/**/*.vue'],
rules: {
'vue/multi-word-component-names': 0
}
}
]
};

挂载全局属性

挂载方式

在 Vue2 的时候通常是在 main.js 文件导入全局方法,使用 Vue.prototype.fn = fn 进行全局挂载,然后用 this.fn 进行调用。

// 挂载
import Vue from 'vue';
Vue.prototype.$axios = axios;

// 使用
this.$axios();

在 Vue3 是使用 app.config.globalProperties.fn = fn 进行挂载。

import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
// 挂载
app.config.globalProperties.$axios = axios;

// 使用
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
proxy.$axios();

结合 TS 使用

1、先在 main.ts 中挂载属性和方法,例如:

app.config.globalProperties.$hong = 'hong666';
app.config.globalProperties.$fn = key => key + ',666';
app.config.globalProperties.$axios = axios;

2、创建 src/types/typing.d.ts,内容如下:

import axios from 'axios';

export {};

declare module 'vue' {
interface ComponentCustomProperties {
$hong: string;
$http: typeof axios;
$fn: (key: string) => string;
}
}

3、需要确保tsconfig.json里包含了该文件:

{
"include": ["*.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.d.ts"]
}

4、在模版中使用:

<template>
<h1>{{ $hong }}</h1>
</template>

5、在<script setup lang="ts">中使用:

import { getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';

const { proxy } = getCurrentInstance() as ComponentInternalInstance;
proxy?.$fn('123');
注意

getCurrentInstance() 是一个内部 API,要谨慎使用!

获取 DOM

在模版中给元素绑定ref="screenBodyRef"属性,这个值要和下面声明的screenBodyRef同名称

const screenBodyRef = ref<HTMLElement | null>(null);

// 非空断言
screenBody.value!.offsetLeft;

// 类型断言
(screenBody.value as HTMLElement).offsetLeft;

插槽

默认插槽

Parent.vue
<template>
<Foo>
<h1>插槽内容</h1>
</Foo>
</template>
Foo.vue
<template>
<div>
<slot></slot>
</div>
</template>

插槽里可以放默认内容,如<slot>默认内容</slot>

具名插槽

Foo.vue
<template>
<div>
<slot name="header"></slot>
</div>
<div>
<slot name="footer"></slot>
</div>
</template>

使用:

<template>
<Foo>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
<template v-slot:footer>
<!-- footer 插槽的内容放这里 -->
</template>
</Foo>
</template>

v-slot:header可以简写成以 # 号开头的形式:#header

默认名字是 defalut,使用:#defalut

作用域插槽

Foo.vue
<template>
<div>
<slot :name="name" :count="count"></slot>
</div>
</template>

<script setup>
const name = ref('header');
const count = ref(1);
</script>
Parent.vue
<template>
<Foo v-slot="slotProps">
<h1>{{ slotProps.name }}</h1>
<p>{{ slotProps.count }}</p>
</template>

插件

插件是能在全局使用的工具代码。

插件可以是一个有 install方法的对象,也可以是一个函数。

例如,想在组件中使用 $translate 方法,支持多语言

App.vue
<h1>{{ $translate('greetings.hello') }}</h1>

编写插件:

plugin/i18n.js
export default {
install(app, options) {
app.config.globalProperties.$translate = key => {
return key.split('.').reduce((o, i) => {
if (o) return o[i];
}, options);
};
}
};

main.js 中注册插件:

main.js
import { createApp } from 'vue';
import i18nPlugin from './plugins/i18n.js';

const app = createApp({});

app.use(i18nPlugin, {
greetings: {
hello: '你好'
}
});

element-plus

form

获取表单实例,用于校验、重置操作

将 ref 通过参数传入重置函数

// <el-form ref="formRef">
// <el-button @click="resetForm(formRef)">重置</el-button>
import type { FormInstance } from 'element-plus';
const formRef = ref<FormInstance>();

const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};

直接重置:

formRef.value?.resetFields();

提交时校验:

formRef.value?.validate((valid, fields) => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!', fields);
}
});

样式穿透

以下三种语法都已经弃用:

  • >>>
  • /deep/
  • ::v-deep

使用 ::v-deep() 替代,简写 :deep()

  • 全局选择器::global
  • 插槽选择器::slotted