Vue3
组合式 API
vue3 新增setup()
语法,包括两个参数props
和context
。定义的变量和方法等都在setup()
里面。
编码技巧
- 使用组合式 API。按功能块写代码,避免将一堆 ref 变量写在一块
- 使用 setup 语法糖,即
<script setup>
编写组件
需要掌握的 API:
defineProps()
和defineEmits()
defineModel()
defineExpose()
defineOptions()
- 使用 ref 声明响应式变量,尽量避免使用 reactive
安装官方的 VScode 插件:Vue-Official,然后勾选上 Auto-complete Ref value with .value
就可以自动补全 .value
v-if
和v-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'
。第一个参数是要监听的变量,第二个是回调函数。如果要监听多个变量,那么第一个参数传入数组
监听类型
- ref 定义的变量
const count = ref(0);
watch(count, newVal => {
console.log(newVal);
});
// 获取旧值
watch(count, (newVal, old) => {
console.log(newVal, old);
});
注意:监听 ref 定义的变量时,不用写.value
,因为用了.value
就是要监听对象的属性,是一个具体值,而不是一个响应式对象。
- 一个有返回值的函数
watch(
() => count.value,
newVal => {
console.log(newVal);
}
);
- 一个数组
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);
});
- 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 }
);
深度监听
- 直接传入会隐式创建深度监听。任何属性变化了都会触发回调函数。
const obj = reactive({ a: 1, b: 2 });
watch(obj, newVal => {
console.log(newVal.b);
});
function btn() {
obj.a++;
}
- 返回一个函数,监听返回的属性,只有这个属性发生了变化,才会触发回调函数。也可以显式地加上
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()
的局限性。
响 应式原理
例如:实现 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 框内的值)赋值给代理对象对应的属性,从而实现简单的双向数据绑定
v-model
和v-bind
的属性值要相同,如都是 text 或都是 passworddom.addEventListener("input", function() {})
这里不能使用箭头函数,否则 this 指向 Window 对象
v-model 原理
- 表单输入绑定:https://cn.vuejs.org/guide/essentials/forms.html
- 组件的 v-model:https://cn.vuejs.org/guide/components/v-model.html
- 参考 vue2 的 v-model 原理
在 3.4 版本以后,使用 defineModel()
宏
在 3.4 版本以前:
<Child v-model="foo" />
<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"
将被编译为:
<Child :modelValue="foo" @update:modelValue="$event => (foo = $event)" />
如果在 TS 中,可以断言 $event.target
为 HTMLInputElement
类型,而不是 null
(<HTMLInputElement>$event.target).value;
自定义 hooks
如果在一个组件里定义了很多的 data 和方法,写法就类似 vue2 的写法了。可以按功能将数据和方法放到一个单独的文件中,文件名以 use
开头,比如 useCounter.ts
、useSum.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);
配置步骤:
- 在
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'
})
]
});
- 添加
dts: './auto-imports.d.ts'
后,会在项目根目录自动生成auto-imports.d.ts
文件,自动生成类型声明。 - 如果有报错说 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
禁用这个规则:
- 要禁用所有文件中的规则:
module.exports = {
rules: {
'vue/multi-word-component-names': 0
}
};
- 仅禁用
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;
插槽
默认插槽
<template>
<Foo>
<h1>插槽内容</h1>
</Foo>
</template>
<template>
<div>
<slot></slot>
</div>
</template>
插槽里可以放默认内容,如<slot>默认内容</slot>
具名插槽
<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
作用域插槽
<template>
<div>
<slot :name="name" :count="count"></slot>
</div>
</template>
<script setup>
const name = ref('header');
const count = ref(1);
</script>
<template>
<Foo v-slot="slotProps">
<h1>{{ slotProps.name }}</h1>
<p>{{ slotProps.count }}</p>
</template>
插件
插件是能在全局使用的工具代码。
插件可以是一个有 install
方法的对象,也可以是一个函数。
例如,想在组件中使用 $translate
方法,支持多语言
<h1>{{ $translate('greetings.hello') }}</h1>
编写插件:
export default {
install(app, options) {
app.config.globalProperties.$translate = key => {
return key.split('.').reduce((o, i) => {
if (o) return o[i];
}, options);
};
}
};
在 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