初始化vue3项目
- 方法一:通过vue-cli直接创建,要求脚手架的版本最新
- 方法二:在一个初始化的vue2项目中,在终端中使用命令
vue add vue-next
来升级为vue3项目。(注意是在一个初始化的vue2项目,不要瞎搞
)
支持多个根节点
- 我们都知道在vue2中只允许在template中定义一个根节点,否则会出现The template root requires exactly one element的提示报错并导致编译错误。
- 在vue3中我们可以在template里写上多个根节点,这个特性在vue3中得以展现。
<template>
<div>hello</div>
<div>world</div>
</template>
- 实现原理(DocumentFragment)可参考官网
入口文件mian.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render:h=>{App}
}).$mount('#app')
import { createApp } from "vue";
import App from "./App.vue";
// Vue3的使用方式大都以函数式的方式来实现
// App是 vue 的组件对象实例
createApp(App).mount("#app");
/*App是vue的组件对象实例,结构如下:
{
name: 'App',
components: {
HelloWorld
}
}
*/
更好的typescript支持
- vue3因为有着typescript的支持,配合vscode可以在编写代码中有着十分好的类型提示,开发体验十分之好
支持自动的tree-shaking
- 在vue3中自动会对打包构建的代码进行tree-shaking。对用户来说不需要的vue功能不用打包进去了会优化最终的文件体积;对框架开发者来说可以提供更多的功能也不用的担心会导致用户的文件体积变大(在用户不使用该功能的情况下)
传送门的概念
- vue3新增了传送门teleport的概念,我们来看一个业务场景(
在vue2下
),并使用vue3的传送门来解决这个业务场景,官方的演示地址
- 在vue2中因为只允许有一个根节点,代码书写如下:(
注意在下方的代码中根节点#app的style="position: relative;"
)
//这里是在App.vue中的代码:
<template>
<div id="app" style="position: relative;">
<h3>Tooltips with Vue 3 Teleport</h3>
<modal-button></modal-button>
</div>
</template>
<script>
import ModalButton from "./components/ModalButton";
export default {
name: "App",
components: {
ModalButton,
},
};
</script>
- 我们在子组件ModalButton中想要实现一个基于窗口居中的弹框,代码书写如下:(
注意这里的.modal的style="position: absolute;"
)
<template>
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal! (My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
modalOpen: false,
};
},
};
</script>
<style>
.modal {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.modal div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
width: 300px;
height: 300px;
padding: 5px;
}
</style>
- 于是我们得到了基于根节点#app的居中弹框,这不是我们想要的结果,我们是想要基于窗口(
这里可以是body
)的居中弹框啊!!!,效果图如下:
- 紧接着该请出我们vue3的新特性传送门(teleport)了,下面是用vue3重写在子组件ModalButton中的代码,参考如下:
<template>
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal! (My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
</template>
<style>
.modal {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.modal div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
width: 300px;
height: 300px;
padding: 5px;
}
</style>
- 使用了传送门
<teleport to="body"> </teleport>
后的效果图如下,我们实现了基于body的居中弹窗:
- 我们需要注意使用
<teleport to="body"> </teleport>
后在渲染的时候并不是渲染到当前引入这个组件的组件内而是渲染到指定的element内
- 我们还需要注意teleport是可以在一个页面组件中使用多次的,但是我们在使用多个teleport时需要注意它们之间的z-index关系,这是一个添加的操作。
- 我们更需要注意如果我在teleport中加入子组件,此时这个子组件的父组件是谁呢?在传送门内定义的子组件的父组件是属于引入这个组件的组件,在这里我在组件ModalButton中使用了teleport,如果这个teleport中还定义了子组件,那么这个在teleport中定义的子组件的父组件是ModalButton!!!
//这里是在ModalButton的组件中书写的代码
//下方写在teleport中的组件Foo的父组件是ModalButton
<template>
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<Foo></Foo>
<div>
two
</div>
</div>
</teleport>
</template>
<script>
import Foo from "./Foo";
export default {
components: {
Foo,
},
data() {
return {
modalOpen: false,
lth:true
};
},
};
</script>
composition api
- 接着才是vue3的重头戏,那就是composition api。这可以说是完全颠覆了使用vue的逻辑书写习惯,为了不引入全新的概念,采用独立的函数来创建和监听响应式的状态等
- 我们不必担心在vue2的options api会不能在vue3中使用,它和vue3的composition api是兼容共存的,但是建议统一风格的写法。
- 接下来将composition api的每个api单独做为一个大标题来书写:
setup入口
- setup是初始化的入口,代表着当前组件初始化composition api的入口,在setup函数中return的对象中的值可以直接写在template中,但是注意如果不做任何处理,直接用返回的值在template中书写的话,这个值不是响应式数据!!!
- 我们来看一下setup入口的运用,代码参考如下:
<template>
<div id='Setup'>
不是响应式数据:{{count}}
<button @click="handleClick">点击按钮count</button>
<hr/>
</div>
</template>
<script>
export default {
name: 'Setup' ,
setup(){
console.log(this,'这里的this是undefined')
const count =1;
const handleClick=function handleClick(e){
console.log(e,'e');
console.log('不是响应式数据了?');
// 'count' is constant no-const-assign
// 下一行出错
// count++;
};
//必须返回一个对象
//返回的数据就是暴露给template用的
//返回的数据不做任何处理的话在template直接使用不是响应式数据
return {
//返回一个普通值
count,
//返回一个函数
handleClick
}
}
}
</script>
- 在setup函数中return的对象中的值可以直接写在template中,注意这里返回的得是一个对象,如果你返回一个函数的话,实际上是调用render函数返回一个vnode.
//在setup中return的可以是一个函数
return ()=>{
//在这里它相当于render函数
//return vnode
//返回一个虚拟节点
//不推荐的写法
}
- 因为在data内可以获取到setup返回的值,所以可以直接在template中使用这个返回的值。
- 注意这里的在setup函数中的this是获取不到的,因为setup函数的执行比beforeCreate和created调用的时间都要早,它是在options api之前调用的,自然获取不到
- setup的第一个参数是props,(
即setup(props){}
)可以获取父组件传递过来的props,当然这个由父组件传递过来的props是不可以在子组件中修改的,是只读的属性。
ref的使用
- ref是响应式系统api的一员大将,在上方的例子中我们可以发现不做任何处理直接返回的count不是响应式数据,它就是一个普通的值,可是我们的需求是在我点击按钮的时候count可以做为响应式数据被累加
- ref可以用于创建一个响应式数据,代码参考如下:
<template>
<div id='Setup'>
是响应式数据:{{count1}}
<button @click="handleClickCount1">点击按钮count1</button>
这样写反而没有用{{count1.value}}
<hr/>
</div>
</template>
<script>
//这些函数都必须在setup函数里面去调用
import {ref} from 'vue';
export default {
name: 'Setup' ,
setup(){
//创建一个响应式数据
const count1=ref(0)
//RefImpl对象
//console.log(count1,'count1');
const handleClickCount1=function handleClickCount1(){
console.log('是响应式数据了');
console.log(this,'这里竟然可以拿到this');
//一定得记住通过.value来取值
count1.value++;
};
//必须返回一个对象
//返回的数据就是暴露给template用的
//返回的数据不做任何处理的话在template直接使用不是响应式数据
return {
//返回一个普通值
count1,
//返回一个函数
handleClickCount1
}
}
}
</script>
- 注意点:这里通过
const count1=ref(0)
创建了一个值为0的响应式数据,并在handleClickCount1
该方法中实现了对该响应式数据累加的方法。
- 我们需要注意这里的是count1现在是一个RefImpl对象,0这个值被保存在这个对象的
value
属性下,所以我们在setup函数中需要通过count1.value
来获取这个值0,这令我们徒增了许多心智负担。
- 虽然我们在setup函数中需要通过
count1.value
来获取这个值0,但是我们可以直接返回这个count1对象,并在template中直接使用{{count1}}
来获取这个值
reactive的使用
- ref是响应式系统api的一员大将,在上方的例子中我们通过ref创建一个响应式数据,我们还可以通过reactive来创建一个一个响应式数据,代码参考如下:
<template>
<div id='Setup'>
<button @click="handleAge">点击按钮变化age</button>
user:{{user}}user name:{{user.name}}user age{{user.age}}
<hr/>
</div>
</template>
<script>
//这些函数都必须在setup函数里面去调用
//必须得引入才能使用
import {reactive} from 'vue';
export default {
name: 'Setup' ,
setup(){
//引用类型 对象/数组/......
let user=reactive({name:'lth',age:20})
// Proxy {name: "lth", age: 20}
// console.log(user,'user');
const handleAge=()=>{
console.log('handleAge');
console.log(user.age,'user.age before');
user.age++;
console.log(user.age,'user.age after');
}
//必须返回一个对象
//返回的数据就是暴露给template用的
//返回的数据不做任何处理的话在template直接使用不是响应式数据
return {
//返回一个引用类型
user,
//返回一个函数
handleAge
}
}
}
</script>
- 注意点:这里通过
let user=reactive({name:'lth',age:20})
创建一个引用类型的响应式数据,并在handleAge
中实现了对user.age
的累加.
- 我们需要注意这里的user是Proxy对象,可以直接通过
user.age
和user.name
来直接访问属性值,这一点得和使用ref区别开来.
readonly的使用
- readonly是响应式系统api的一员大将,在上面的例子中我们使用了reactive创建一个响应式数据,我们可以看到在点击按钮时,
user.age
的值会被累加,也就是说该user.age
的值是可以被修改的,如果我们想要让一个值即是响应式数据又是不可修改的只读的值呢?我们可以使用readonly
,代码参考如下:
<template>
<div id='Setup'>
<button @click="handleReadOnly">只读age</button>
{{readonlyUser}}
</div>
</template>
<script>
//这些函数都必须在setup函数里面去调用
import {readonly} from 'vue';
export default {
name: 'Setup' ,
setup(){
const readonlyUser=readonly({user1:'secret',age:20})
// console.log(readonlyUser,'read');
const handleReadOnly=()=>{
//下面两行完全没反应
readonlyUser.age++
console.log(readonlyUser.age=23,'ss');
}
//必须返回一个对象
//返回的数据就是暴露给template用的
//返回的数据不做任何处理的话在template直接使用不是响应式数据
return {
//返回一个只读值
readonlyUser,
//返回一个函数
handleReadOnly
}
}
}
</script>
- 我们可以回顾一下在vue2中父组件传递给子组件的props属性也是响应式的,但是我们在子组件中是不能直接修改props数据的,它也是一个只读属性.
- 在vue3中直接对在子组件中收到的props自动设置了readonly,如果你企图在子组件中直接修改props属性,是会报错的,代码如下:
<script>
//这些函数都必须在setup函数里面去调用
import {readonly} from 'vue';
export default {
name: 'Setup' ,
props:{
msg:String
},
setup(props){
//proxy对象 readonly
// console.log(props,'props');
//Set operation on key "msg" failed: target is readonly.
// console.log(props.msg='12456');
return {
//todo...
}
}
}
</script>
computed计算属性的使用
- computed是响应式系统api的一员大将,这里的计算属性接收一个对应的函数,我们回顾一个计算属性,它是一个依赖别的属性的属性,通过返回一个属性供模板调用
- 通过计算属性创建的是也响应式数据,直接上代码:
<template>
<div id='Setup'>
是响应式数据:{{count1}}
doublecount:{{double}}
<button @click="handleClickCount1">点击按钮count1</button>
</div>
</template>
<script>
//这些函数都必须在setup函数里面去调用
import {computed,ref} from 'vue';
export default {
name: 'Setup' ,
setup(){
const count1=ref(0)
//计算属性接收一个对应的函数
const double=computed(()=>{
return count1.value*2
})
//ComputedRefImpl
//计算属性创建的也是响应式数据
//和ref类型一致基本一致
//需要通过double.value来获取具体值
// console.log(double,'double');
const handleClickCount1=function handleClickCount1(){
console.log('是响应式数据了');
console.log(this,'这里竟然可以拿到this');
//一定得记住通过.value来取值
count1.value++;
};
return {
//返回一个值
count1,
double,
//返回一个函数
handleClickCount1,
}
}
}
</script>
watch侦听器的使用
- watch是响应式系统api的一员大将,watch的第一个参数必须是响应式数据或者是一个返回一个响应式数据的函数.直接上代码:
<script>
//这些函数都必须在setup函数里面去调用
import {watch,reactive,ref} from 'vue';
export default {
name: 'Setup' ,
setup(){
const count1=ref(0)
//注意书写的顺序
//还没写出count1就watch是不行的
watch(count1,(newValue,oldValue)=>{
console.log(newValue,oldValue,'newValue,oldValue');
},{
immediate:true
})
let user=reactive({name:'lth',age:20})
//user是响应式数据了
watch(user,(newValue,oldValue)=>{
console.log(newValue,oldValue,'newValue,oldValue');
},{
immediate:true
})
//返回一个响应式数据的函数
watch(()=>user.age,(newValue,oldValue)=>{
console.log('fuck anyone');
console.log(newValue,oldValue,'newValue,oldValue');
})
return {
user,
count1
}
}
}
</script>
watchEffect的使用
- watchEffect是响应式系统api的一员大将,它不需要一开始设置去观察谁,始终获取书写在改函数中的值的最新值,直接接收一个函数,并在一开始一上来就立即执行了一遍.代码如下:
<script>
//这些函数都必须在setup函数里面去调用
import {watchEffect,reactive} from 'vue';
export default {
name: 'Setup' ,
setup(){
let user=reactive({name:'lth',age:20})
//直接接收一个函数
//不需要一开始设置去观察谁
watchEffect(()=>{
console.log('fun k');
//当user.age改变时会触发
//并且一上来就立即执行了一遍
console.log(user.age);
})
return {
user
}
}
}
</script>
生命周期钩子的改变
- setup函数的执行比beforeCreate和created调用的时间都要早,它是在options api之前调用的,如下表就是生命周期钩子的改变:
beforeCreate |
使用 setup() |
created |
使用 setup() |
beforeMount |
使用 onBeforeMount |
mounted |
使用onMounted |
beforeUpdate |
使用onBeforeUpdate |
updated |
使用onUpdated |
beforeDestroy |
使用onBeforeUnmount |
destroyed |
使用onUnmounted |
errorCaptured |
使用onErrorCaptured |
依赖注入的使用
- 依赖注入(
provide/inject
)可以用于组件通信,我们先来看在APP.vue中的代码:
- provide的第一个参数是key,第二个参数是value
<template>
<div>
<Bar></Bar>
</div>
</template>
<script>
import Bar from "./components/Bar";
import { provide } from "vue";
export default {
name: "App",
components: {
Bar,
},
setup() {
provide("app", "app-component");
},
};
</script>
import {inject} from 'vue';
setup() {
// inject的第二个参数可以指定一个默认值
// 在provide('app','app-component')没传第二个参数时
//const app=inject('app','没传时使用默认值')
const app=inject('app');
console.log(app,'app');
}
refs的使用
- 使用refs可以获取真实dom元素或者组件实例,代码参考如下:
<template>
<div id="App">
hello world
<button ref="btn">点击refs</button>
</div>
</template>
<script>
import {onMounted,provide} from 'vue';
import Setup from './components/Setup';
export default {
name: 'App',
setup() {
onMounted(()=>{
console.log(btn);
})
const btn=ref(null);
return {
//返回的btn一定要与
//<button ref="btn">点击refs</button>
//中的btn一致
btn
}
}
}
</script>
代码组织
- 为什么要去使用
composition api
呢,因为可以更好的实现代码组织,我们来看以往在vue2中书写的常规代码:
export default {
name: 'Foo' ,
components: {},
data() {
return {
one:1,
tow:2
}
},
computed: {
oneCom() {
return this.one
},
twoCom() {
return this.two
}
},
methods: {
oneF() {
console.log('11');
},
twoF() {
console.log('22');
}
},
watch: {
one(newValue, oldValue) {
console.log(newValue, oldValue);
},
two(newValue, oldValue) {
console.log(newValue, oldValue);
}
},
};
- 上面的代码组织看起来比较冗余,不同的逻辑点放在不同的地方,逻辑少的话比较清晰,但是逻辑多的话找起来麻烦,我们在来看换成composition api后的写法(
下面还不是最佳的写法,最佳写法在下下面
)
setup() {
//功能一
const one =ref('one')
const oneF=()=>{console.log('one')};
const oneCom=computed(()=>{return one.value*2});
watch(one,()=>{return });
//功能二
const two =ref('two')
const twoF=()=>{console.log('two')};
const twoCom=computed(()=>{return two.value*2});
watch(two,()=>{return });
}
- 上面的写法中我们已经可以清晰的观察到一个功能块的使用,我们在来做些改变,按照功能把功能块单独抽离为一个函数,利用到了组合的思想,代码如下:
<template>
<div id='Foo'>
foo
<hr/>
<button @click="oneF">点击改变one</button>
this is one:{{one}}
</div>
</template>
<script>
import {computed,watch,ref} from 'vue';
export default {
name: 'Foo' ,
components: {},
setup() {
//功能一
const {one,oneF} =oneFeature()
//功能二
const {two,twoF} =twoFeature()
return {
one,
oneF
}
}
};
function oneFeature(){
const one =ref(1)
const oneF=()=>{console.log(one);one.value++};
// const oneCom=computed(()=>{return one.value*2});
// watch(one,()=>{return });
return {
one,
oneF
}
}
function twoFeature(){
const two =ref(2)
const twoF=()=>{console.log(two)};
// const twoCom=computed(()=>{return two.value*2});
// watch(two,()=>{return });
return {
two,
twoF
}
}
</script>
<style lang="scss" scoped>
</style>
- 我们可以清晰的看到上面的写法带来的简便性,并且我们可以将上方的功能块函数单独放在一个js文件内导出,在由主入口来引入这个导出的函数,这是最佳解,代码如下:
//因为所有的composition api都可以函数式的被导出使用
import {computed,watch,ref} from 'vue';
//在该js文件中单独封装这个功能块方法并导出
export oneFeature(){
const one =ref(1)
const oneF=()=>{console.log(one);one.value++};
// const oneCom=computed(()=>{return one.value*2});
// watch(one,()=>{return });
return {
one,
oneF
}
}
逻辑复用
- 我们知道一个函数其实就是代表着可以复用的,在vue2中我们想要复用组件往往会使用
mixins
来实现,代码参考如下:
//使用mixins导致来源不清晰容易造成命名冲突
//下面是
<template>
<div>{{ x }} -- {{ y }}</div>
</template>
<script>
//下面是在MoveMixin.js中封装的需要被mixins的复用组件
import { MoveMixin } from "./MoveMixin";
export default {
mixins: [MoveMixin],
//如果有多个复用组件呢?
//问题:来源不清晰和容易引起命名冲突
//mixins: [MoveMixin, fooMixin, barMixin],
},
};
</script>
- 下面是在
./MoveMixin.js
的复用组件的逻辑代码:
export const MoveMixin = {
data() {
return {
x: 0,
y: 0,
};
},
methods: {
useMousePosition(e){
this.x=e.pageX;
this.y=e.pageY;
}
},
mounted() {
window.addEventListener("mousemove", this.useMousePosition);
},
//因为在vue3中测试所以这里写的是 unmounted()
//可以改回vue2的
unmounted() {
window.removeEventListener("mousemove", his.useMousePosition);
},
};
- 如果我们有多个复用组件需要被使用,使用mixins导致来源不清晰和容易造成命名冲突的问题是十分棘手的,我们在来看vue3为我们带来的在逻辑复用上的好处,代码参考如下:
//在这里使用vue3封装复用组件的逻辑
//下面是在useMousePosition.js中的代码
//这里是js文件
import { onMounted, onUnmounted, reactive } from "vue";
export function useMousePosition() {
const position = reactive({
x: 0,
y: 0,
});
const useMousePosition = (e) => {
// console.log(e);
position.x=e.pageX
position.y=e.pageY
};
onMounted(() => {
window.addEventListener("mousemove",useMousePosition);
});
onUnmounted(() => {
window.removeEventListener("mousemove", useMousePosition);
});
return { position };
}
- 使用veu3的新特性封装完复用组件的逻辑后,在组件中使用的代码如下:
- 下面有一个坑,就是返回的响应式数据如果直接解构,解构出来的值不再是响应式数据,可以使用
toRefs
来解决这个问题.
- 我们可以发现我们的逻辑复用是十分的清晰的.
template>
<div id="App">
鼠标位置x:{{x}}鼠标位置y:{{y}}
</div>
</template>
<script>
import {useMousePosition} from './components/useMousePosition';
import { toRefs } from "vue";
export default {
name: 'App',
setup() {
const {position}=useMousePosition();
// 响应式数据对象丢失
// x和y不再是响应式数据
// const { x, y } = position;
// console.log(x,y); //普通值
//const { x, y } = toRefs(position);
//ObjectRefImpl 又变成了响应式数据
//console.log(x,y);
//position是在useMousePosition.js创建的响应式数据
//如果直接通过解构出来的x和y会丧失掉响应式
//可以利用toRefs这样解构出来的x和y依旧是响应式数据
const {x,y}=toRefs(position);
return {
x,
y
}
}
}
</script>
发表评论
还没有评论,快来抢沙发吧!