文章目录
- template 的变化
- 新增语法
- setup
- ref
- reactive
- toRefs
- 生命周期的变化
- watch 的变化
- computed 的变化
- 新增标签
- Teleport
- Suspense
- 自定义 HOOKS
- 结合 TypeScript 开发组件
template 的变化
在 Vue2 中,每个 template 节点只能有一个根节点:
<template>
<div>123</div>
</template>
而在 Vue3 中,可以有多个根节点:
<template>
<div>123</div>
<div>456</div>
</template>
这种变化,让我们在开发过程中,减少了不必要html标签的书写。例如下面这个例子:
<template>
<table>
<tr>
<columns />
</tr>
</table>
</template>
在这里,<columns />组件需要返回多个<td>元素。如果在<columns />组件中使用了父 div:
<template>
<div>
<td>Hello</td>
<td>World</td>
</div>
</template>
得到一个<Table />输出:
<template>
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
</template>
而这段生成的 HTML 是无效的。Vue3 中 Template 的变化解决了这个问题。
新增语法
setup
setup()可以代替了原来的data(),且只执行一次。
setup()可以接收两个参数,
props和context(没有用到的时候可以省略不写)。
<template>
<div>
<h1>num:{{ num }}</h1>
</div>
<button @click="add">加1</button>
</template>
<script lang="js">
export default {
name: 'App',
setup() {
let num = 0;
const add = () => {
num++;
};
console.log(num);
return {
add,
num,
};
},
};
</script>
上述例子是一个基本的setup的过程。但我们试着点击一下加1按钮,会发现num并没有发生改变。看一下控制台,发现输出的是一个number的值,但我们点击并没有发生改变。为了解决这个问题,我们将用到一个新的 API:Ref。
ref
我们改造下上面的代码:
<template>
<div>
<h1>msg:{{ msg }}</h1>
<h1>num:{{ num }}</h1>
</div>
<button @click="add">加1</button>
</template>
<script>
import { ref } from 'vue';
export default {
name: 'App',
setup() {
const msg = ref(0);
let num = 0;
const add = () => {
msg.value++;
num++;
};
console.log(msg);
console.log(num);
return {
msg,
add,
num,
};
},
};
</script>
先看下ref的导入方式,可以看到这里是按需导入。我们再看下这里的控制台的输出,就会发现:msg打印是RefImpl,num打印是数值0。点击按钮发现,msg可以改变,而num不行。
RefImpl是什么呢?我把它理解为代理对象。就比如我们知道Vue2中data的数据是通过Object.defineProperty()来进行拦截。从而达到数据响应式的目的。而Vue3是利用了ES6中的proxy达到数据响应式的目的。
关于proxy的使用,举一个简单的例子:
const target = {
message1: "hello",
message2: "everyone"
};
const handler = {
get: function(target, prop, receiver) {
return "world";
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message1); // world
console.log(proxy.message2); // world
ref是一个函数,它接受一个参数,返回的就是一个响应式对象。例子中,我们初始化的这个0作为参数包裹到这个对象中去,在未来操作这个值的时候,可以检测到改变并作出对应的相应。
reactive
创建一个对象的反应式状态,就要使用reactive方法。示例代码如下:
<template>
<div>
<h1>count:{{ obj.count }}</h1>
<h1>double:{{ obj.double }}</h1>
</div>
<button @click="obj.increase">加1</button>
</template>
<script>
import { reactive, computed } from 'vue';
export default {
name: 'App',
setup() {
const obj = reactive({
count: 0,
increase: () => {
obj.count++;
},
double: computed(() => obj.count * 2), // 这里是computed在vue3中的用法
});
return {
obj,
};
},
};
</script>
这时候我们点击按钮就可以改变状态了。但像{{obj.count}}这样写的时候有些繁琐,这里我们可以解构下obj:
<template>
<div>
<h1>count:{{ count }}</h1>
<h1>double:{{ double }}</h1>
</div>
<button @click="increase">加1</button>
</template>
<script lang="js">
import { reactive, computed } from 'vue';
export default {
name: 'App',
setup() {
const obj = reactive({
count: 0,
increase: () => {
obj.count++;
},
double: computed(() => obj.count * 2),
});
return {
...obj,
};
},
};
</script>
但这时我们点击按钮,会发现状态改变不了,为什么呢?这是因为,解构会破坏代理,把他变成一个普通值。就跟上面的ref的例子一样,所以点击按钮并没有发生变化。这时候,就要请出另外一个新加的api了,toRefs:
toRefs
使用起来 比较简单,就返回的时候加上就可以:
<!-- html和上面一样的,省略了。 -->
<script lang="js">
import { reactive, computed, toRefs } from 'vue';
export default {
name: 'App',
setup() {
const obj = reactive({
count: 0,
increase: () => {
obj.count++;
},
double: computed(() => obj.count * 2),
});
return {
...toRefs(obj),
};
},
};
</script>
toRefs从组合函数返回反应对象时,此函数很有用,以便使用组件可以对返回的对象进行解构/扩展而不会失去反应性。
生命周期的变化
| Vue2 | Vue3 |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
示例:
<script lang="js">
import { onMounted } from 'vue';
export default {
setup() {
// mounted
onMounted(() => {
console.log('Componentis mounted!');
});
},
};
</script>
watch 的变化
watch 在vue3中的用法与在vue2中的用法类似。
computed 的变化
computed 在vue3中的用法与在vue2中的用法类似。但在vue3中,可以写在reactive内部,也可写在外部。
watch和computed的用法如下:
<script lang="js">
import { reactive, computed, watch, toRefs } from 'vue';
export default {
setup() {
const data = reactive({
count: 0,
increase: () => {
data.count++;
},
double: computed(() => data.count * 2),
});
const conComputed = computed(() => data.count * 2);
const number = ref(0);
watch(data, () => {
console.log(data);
document.title = 'updated ' + data.count;
});
watch(number, () => {
console.log(number);
});
return {
number,
conComputed,
...toRefs(data),
};
},
};
</script>
在
vue3中,watch和computed都要先引入才能使用。
新增标签
Teleport
平时我们的遮罩层都存在于某个多级标签下面,这样其实是不合理的。Teleport的出现可以让我们写的组件移动到指定标签下面。to是要移动到哪个标签下,它支持选择器。
示例代码如下:
<template>
<teleport to="#modal">
<div id="center">
<h1>this is a modal</h1>
</div>
</teleport>
</template>
<script lang="js">
export default {
name: 'modal',
};
</script>
<style scoped>
#center {
width: 200px;
height: 200px;
background: red;
position: fixed;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
</style>
<template>
<div id="modal">
<div>
<div>
<modal v-if="show"></modal>
</div>
</div>
</div>
<button @click="show = !show">show</button>
</template>
<script lang="js">
import { ref } from 'vue';
import Modal from './components/modal.vue';
export default {
name: 'App',
components: {
Modal,
},
setup() {
const show = ref(false);
return {
show,
};
},
};
</script>
点击按钮之前:
点击按钮之后:
观察点击按钮前后的DOM结构,我们会发现,即使modal组件在div#modal下被嵌套了多层div后才使用,当显示时,它依然移动到div#modal下面。
Suspense
Suspense 在异步请求的场景下是很实用的。示例如下:
// MyAsyncComponent.vue
<template>
<h1>I have some async work to do before I can render</h1>
</template>
<script>
export default {
name: 'MyAsyncComponent',
async setup() {
await someAsyncWork();
}
}
</script>
// SuspenseWithError.vue
<template>
<slot v-if="error" name="error"></slot>
<Suspense v-else>
<template #default>
<slot name="default"></slot>
</template>
<template #fallback>
<slot name="fallback"></slot>
</template>
</Suspense>
</template>
<script>
import { ref, onErrorCaptured } from 'vue'
export default {
name: 'SuspenseWithError',
setup() {
const error = ref(null);
onErrorCaptured((e) => {
error.value = e;
return true;
});
return { error };
}
}
</script>
<template>
<SuspenseWithError>
<template #default>
<MyAsyncComponent />
</template>
<template #fallback>
<span>Loading... Please wait.</span>
</template>
<template #error>
<h1>I failed to load</h1>
</template>
</SuspenseWithError>
</template>
<script>
import MyAsyncComponent from '@/components/MyAsyncComponent.vue';
import SuspenseWithError from '@/components/SuspenseWithError.vue';
export default {
name: 'App',
components: { MyAsyncComponent, SuspenseWithError },
}
</script>
自定义 HOOKS
示例如下:
<script lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
const useMousePosition = () => {
const x = ref(0);
const y = ref(0);
const updateMouse = (e: MouseEvent) => {
x.value = e.pageX;
y.value = e.pageY;
};
onMounted(() => {
document.addEventListener('mousemove', updateMouse);
});
onUnmounted(() => {
document.removeEventListener('mousemove', updateMouse);
});
return { x, y };
};
export default useMousePosition;
</script>
<script lang="js">
import useMousePosition from '@/hooks/useMousePosition';
export default {
setup() {
const { x, y } = useMousePosition();
return {
x,
y,
};
},
};
</script>
结合 TypeScript 开发组件
在vue3中,创建组件需要用defineComponent包裹。
示例如下:
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
// 把接口类型导出。在使用的过程中导入接口,对接口进行定义
export interface ColumnProps {
id: number;
title: string;
avatar?: string;
des: string;
}
export default defineComponent({
name: 'ColumnList',
props: {
list: {
type: Array as PropType<ColumnProps[]>,
required: true,
},
},
setup(props) {
// 这里使用到了props
const ColumnList = computed(() => {
return props.list.map((item) => {
if (!item.avatar) {
item.avatar = require('@/assets/logo.png'); // 默认图片
}
return item;
});
});
return {
ColumnList,
};
},
});
</script>
<template>
<div id="container">
<column-list :list="list"></column-list>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import ColumnList, { ColumnProps } from '@/components/column-list.vue';
const testDate: ColumnProps[] = [
{
id: 1,
title: 'test1',
des: 'test1',
avatar: 'https://picsum.photos/id/239/200/200',
},
{
id: 2,
title: 'test1',
des: 'test1',
avatar: 'https://picsum.photos/id/239/200/200',
},
{
id: 3,
title: 'test1',
des: 'test1',
avatar: 'https://picsum.photos/id/239/200/200',
},
{
id: 4,
title: 'test1',
des: 'test1',
avatar: 'https://picsum.photos/id/239/200/200',
},
];
export default defineComponent({
name: 'App',
components: {
ColumnList,
},
setup() {
return {
list: testDate,
};
},
});
</script>