Skip to main content

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() 作为声明响应式状态的主要 API。

ref

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 的解构:

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}}

计算属性

计算属性 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 为什么用 Proxy 代替 defineProperty

Object.defineProperty() 能够监听对象属性的获取和修改,但无法监听属性的添加和删除。

Proxy 可以捕获包括属性添加和删除在内的各种操作,解决 Object.defineProperty() 的局限性。

响应式原理

vue3 的响应式是基于 ProxyReflect

例如:实现 Object 的响应性

function reactive(target) {
return new Proxy(target, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value;
},
deleteProperty(target, key) {
delete target[key];
}
});
}

使用 target[key] 这种写法虽能获取到值,但存在一定的隐患(如 this 问题),所以使用 Reflect 改造:

function reactive(target) {
return new Proxy(target, {
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 target = {
foo: 1,
bar: 1
};

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

console.log(target);

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

最终代码:

function reactive(target) {
return new Proxy(target, {
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 target = {
foo: 1,
bar: 2,
interest: { count: 2 }
};
const p = reactive(target);
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 框内的值)赋值给代理对象对应的属性,从而实现简单的双向数据绑定

tip
  • 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)" />
note

如果在 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>

概念

命令式:是一种关注过程的编程范式,描述了完成一个功能的详细步骤

声明式:是一种关注结果的编程范式,不关注完成一个功能的详细步骤。如 vue 中的 <div>{{ msg }}</div>

  • 在性能上,命令式比声明式更好。声明式需要编译
  • 在开发体验和可维护性上,声明式比命令式更好

运行时

编译时

<head>
<meta charset="UTF-8" />
<title>Document</title>
<script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
// 运行时
// const { render, h } = Vue;
// const vnode = h(
// 'div',
// {
// class: 'test'
// },
// 'hello render'
// );
// const container = document.querySelector('#app');
// render(vnode, container);

// 自己实现
// const vNode = {
// type: 'div',
// props: {
// class: 'test'
// },
// children: 'hello world'
// };
// function render(vnode) {
// const el = document.createElement(vnode.type);
// el.className = vnode.props.class;
// el.textContent = vnode.children;
// document.getElementById('app').append(el);
// }
// render(vNode);

// 编译器
const { compile, createApp } = Vue;
const template = `<div class="test">hello world</div>`;
const renderFn = compile(template);
const app = createApp({
render: renderFn
});
app.mount('#app');
</script>

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');
warning

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