目录
前言
vue在20年9月发布了正式版本,一转眼一年过去了,终于有时间来学习3.0了,接下来做一些自己的学习笔记。
vue3对比vue2.x改变了什么
- 响应式采用Proxy,不同于2.X的Object.defineProperty
- composition API
- 新的生命周期-LifeCycle Hooks
- 更好的typescript支持
- 明显的性能提升,打包更小、初次渲染更快、内存使用更少
- 增加了新特性,如Teleport组件,全局API的修改和优化等
setup
准备工作:
- 首先使用vuecli搭建一个vue3project项目,选择语言为typescript,其他的vuex、vuerouter等都选上,之后等待创建完成。
- 来到Home.vue,把原有的代码都删除。
要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent 全局方法定义组件
import {defineComponent} from 'vue';
export default defineComponent({
name: 'Home',
setup() {
}
});
setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3 的 Composition API 新特性提供了统一的入口, setup 函数会在 beforeCreate 、created 之前执行, vue3也是取消了这两个钩子,统一用setup代替, 该函数相当于一个生命周期函数,vue中过去的data,methods,watch等全部都用对应的新增api写在setup()函数中
setup()接收两个参数props和context,在接下来的组件中会说到。
需要注意的是在 setup() 内部,无法使用 this。 因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。
响应性API
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。
import {defineComponent, ref} from 'vue';
export default defineComponent({
name: 'Home',
setup() {
// ref,定义一个响应式的数据(一般用来定义一个基本类型)
const message = ref<string>('');
// 打印 message 可以看到是一个Proxy对象
console.log(message)
}
});
reactive
返回对象的响应式副本
import {defineComponent, reactive} from 'vue';
interface UserForm {
name: string,
age: number,
email: string;
}
export default defineComponent({
name: 'Home',
setup() {
// reactive,定义多个数据的响应式,接收一个普通对象然后返回该普通对象的响应式代理器对象(Proxy)
const state = reactive({
men: {
name: '张三',
age: 18,
email: '123456@qq.com'
} as UserForm
});
}
});
把响应式对象展示在页面上
<template>
<div class="home">
Hellow World<br>
<input type="text" v-model="message">
<p>{{ message }}</p>
<ul>
<li>姓名:{{ state.men.name }}</li>
<li>年龄:{{ state.men.age }}</li>
<li>邮箱:{{ state.men.email }}</li>
</ul>
<button @click="ageAdd">点击使张三年龄+1</button>
</div>
</template>
<script lang="ts">
import {defineComponent, ref, reactive} from 'vue';
interface UserForm {
name: string,
age: number,
email: string;
}
export default defineComponent({
name: 'Home',
setup() {
// ref,定义一个响应式的数据(一般用来定义一个基本类型)
const message = ref<string>('');
// reactive,定义多个数据的响应式,接收一个普通对象然后返回该普通对象的响应式代理器对象(Proxy)
const state = reactive({
men: {
name: '张三',
age: 18,
email: '123456@qq.com'
} as UserForm
});
// 打印一下state可以看到是一个Proxy对象
// console.log(state);
// 定义点击事件函数
const ageAdd = (): void => {
state.men.age++;
};
// 这里只要return出去就可以在template中使用了
return {
message,
state,
ageAdd
};
}
});
</script>
效果:可以看到当输入发生改变时会实时改变message的值,同时添加一个点击事件,每次点击都会使年龄+1。
生命周期
和2.x的生命周期作对比
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
示例:
export default defineComponent({
setup() {
// 你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子
onMounted(() => {
console.log('onMounted');
});
}
});
组件和组件传参
假设现在我们在做一个商城项目,需要把商品列表做成一个组件,那么我们需要:
- 创建一个list组件,组件接收goodsList(商品列表)和page(分页)
- list组件需要触发父组件的pageChange事件,类似于vue2.x的this.$emit()
父组件:
<template>
<div className="lifeCycle">
lifeCycle{{ goodsList }}{{ page }}
<List ref="ellist" :goodsList="goodsList" :page="page" @pageChange="pageChange"/>
</div>
</template>
<script lang="ts">
import {defineComponent, onMounted, ref, reactive} from 'vue';
import List from '@/views/study/components/list.vue';
interface Data {
name: string,
age: number,
email: string
}
interface Page {
pageNo: number,
pageSize: number,
total: number
}
export default defineComponent({
name: 'lifeCycle',
components: {List},
setup() {
let goodsList = ref<Data[]>([]);
let page = reactive({
pageNo: 10,
pageSize: 1,
total: 0
});
const pageChange=(page:any)=>{
console.log(page.pageNo)
}
// 获取真实dom
const ellist = ref<null | HTMLElement>(null);
// 你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子
onMounted(() => {
console.log(ellist);
});
// 模拟ajax
setTimeout(() => {
goodsList.value = [
{
name: '张三',
age: 18,
email: '1@qq.com'
},
{
name: '李四',
age: 19,
email: '2@qq.com'
}
];
page.total = 10;
}, 1000);
return {
goodsList, page,pageChange,ellist
};
}
});
</script>
</style>
子组件:
子组件通过props接收父组件的参数, content.emit()传递参数给父组件,这一点和2.x版本是类似的,只是写法不同。
setup(props,content)函数接收两个参数,props:组件传的参数。content:是一个普通的 JavaScript 对象,它暴露组件的三个 property,attrs,slots,emit。
<template>
<div className="list">
list{{ goodsList }}{{ page }}
<table border="1">
<tr>
<th>姓名</th>
<th>年龄</th>
<th>邮箱</th>
</tr>
<tr v-for="item in goodsList" :key="item.email">
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>{{ item.email }}</td>
</tr>
</table>
分页:
<button @click="pageChange('upper')">上一页</button>
<ul class="page">
<li v-for="item in page.total" :key="item">{{ item }}</li>
</ul>
<button @click="pageChange('next')">下一页</button>
</div>
</template>
<script lang="ts">
import {defineComponent, toRefs, ref, reactive, watchEffect, computed, PropType} from 'vue';
interface Page {
pageNo: number,
pageSize: number,
total: number
}
export default defineComponent({
name: 'list',
props: {
goodsList: Array,
page: {
type: Object as PropType<Page>
}
},
setup(props, content) {
const goodsList = computed(() => props.goodsList);
const page = computed(() => props.page);
const pageChange = (e: string) => {
content.emit('pageChange', page.value);
};
return {
goodsList, page, pageChange
};
}
});
</script>
<style lang="scss" scoped>
.page {
display: flex;
}
</style>
Provide / Inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。
爷组件传值给子组件
假设我们的组建层级是这样的:
- grandparent.vue
- parent.vue
- children.vue
- parent.vue
爷组件需要传值给子组件需要这么做:
export default defineComponent({
name: 'grandparent',
setup() {
const message = ref<string>('你好');
// provide共享数据
provide('message', message);
return {
message
};
}
子组件接收数据:
export default defineComponent({
name: 'children',
setup() {
let message=inject('message');
return { message }
}
});
子组件修改爷组件的值
当子组件想要修改爷组件的值时:
<template>
<div>
子组件,接受爷组件传过来的值:{{message}}
<button @click="updateMessage('子组件告诉你hello')">点击子组件改变爷组件的值</button>
</div>
</template>
<script lang="ts">
import {defineComponent, ref, reactive,inject} from 'vue';
export default defineComponent({
name: 'children',
setup() {
let message=inject('message');
const updateMessage = inject('updateMessage')
return { message,updateMessage }
}
});
</script>
爷组件:
<template>
爷组件,值为:{{ message }}
<button @click="message='hello'">点击修改上方的值</button>
<Parent/>
<Children/>
</template>
<script lang="ts">
import {defineComponent, ref, reactive, provide, shallowRef,} from 'vue';
import Parent from '@/views/study/components/parent.vue';
import Children from '@/views/study/components/children.vue';
export default defineComponent({
name: 'ProvideInject',
components: {Parent, Children},
setup() {
const message = ref<string>('你好');
// provide共享数据
provide('message', message);
// 接收子组件,子组件修改值
provide('updateMessage', (val: string) => {
message.value = val;
});
return {
message
};
}
});
</script>
computed / watch
computed
接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
<template>
<div>
<p>computeds</p>
<p>count:{{ count }}</p>
<p>plusOne.value:{{ plusOne }}</p>
<p>plusOne是只读的,当count改变时会自动触发computed改变plusOne.value的值</p>
<p>来试一下:
<button @click="count++">点击使count+1</button>
</p>
</div>
</template>
<script lang="ts">
import {defineComponent, computed, ref, reactive, watch, watchEffect} from 'vue';
interface Obj {
name: string
}
export default defineComponent({
name: 'computedAndWatch',
setup() {
const count = ref<number>(1);
const plusOne = computed(() => count.value + 1);
return {
count, plusOne
};
}
});
</script>
上面的写法plusOne是只读的,无法修改,我们也可以使用具有 get 和 set 函数的对象来创建可写的 ref 对象。
const plusTwo = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1;
},
});
// 修改plusTwo的时候,count也会随之变化
plusTwo = 100
watch
watch和2.x的 this.$watch 完全等效
export default defineComponent({
name: 'computedAndWatch',
setup() {
const obj = reactive({
name: '张三'
} as Obj);
let age = ref<string | number>('不知道');
// 监听 obj 的值
watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue);
if (newValue.name === '李四') {
age.value = '18';
}
});
}
});
当然,你也可以这样监听多个数据:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
})
watchEffect
watchEffect和watch不同的是,watchEffect会立即运行
// watchEffect会立即执行,自动检测内部代码,代码中有依赖 便会执行
watchEffect(() => {
if (obj.name === '李四') {
age.value = '18';
} else {
age.value = '还没有';
}
});
hooks
在vue3之前,我们会采用mixins的方式封装一些公共的方法,但是mixins也有一些缺陷,比如:
- 容易冲突:因为每个特性的属性都被合并到同一个组件中,组件内同名的属性或方法会被mixins里的覆盖掉。
- 可重用性有限:我们不能向 mixins 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。
- 数据来源不清晰:组件里所使用的mixins里的数据或方法在当前组件代码里搜索不到,易造成错误的解读,比如被当成错误代码或冗余代码而误删。
在vue3出现了composition api之后,我们完全可以使用自定义hooks来替代mixins。
我们来感受一下hooks的使用方法,首先新建一个myHooks.ts:
import {reactive, ref,onMounted} from 'vue';
export const myHooks = () => {
// ref,定义一个响应式的数据(一般用来定义一个基本类型)
const message = ref<string>('111');
console.log(message);
// reactive,定义多个数据的响应式,接收一个普通对象然后返回该普通对象的响应式代理器对象(Proxy)
const state = reactive({
men: {
name: '张三',
age: 18,
email: '123456@qq.com'
}
});
onMounted(() => {
console.log('hooks mounted')
})
return {
message, state
};
};
之后再需要的地方使用:
<template>
<p>{{ message }}</p>
<p>{{ state }}</p>
</template>
<script lang="ts">
import {defineComponent, ref, reactive, onMounted} from 'vue';
import {myHooks} from '@/hooks/myHooks';
export default defineComponent({
name: 'hooks',
setup() {
// myHooks是个函数,需要执行一下
const {message, state} = myHooks();
onMounted(() => {
console.log('index mounted');
});
return{
message,state
}
}
});
</script>
获取真实的DOM
在vue2中我们使用 this.ref.xxx 获取组件的DOM,3.0也是用同样的方法,不过写法不一样
<List ref="ellist"/>
setup() {
let goodsList = ref<Data[]>([]);
// 获取真实dom
const ellist = ref<null | HTMLElement>(null);
onMounted(() => {
console.log(ellist);
});
}