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
ref
和 reactive
二者都能定义响应式变量。官方建议使用 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 的解构:
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>