欢迎点击: 个人官网博客
2020年9月份beta版本出来后就一直追3.0,作为第一批吃螃蟹的人,这里把3.0的一些知识分享一下,良心之作。喜欢就收藏吧!
TypeScript传送门
Vue3支持大多数的Vue2的特性。如果3.0项目中2.0跟3.0混搭写法,方法重名的话3.0优先
Vue3中设计了一套强大的组合APi(Compostion API)代替了Vue2中的option API ,复用性更强了。可自定义hook函数,不需要代码都写在一个template(再也不怕代码多起来,滚轮滚到手发抖了)
按需编译,体积比Vue2.x更小
Vue3更好的支持TypeScript(TS)
Vue3中使用了Proxy配合Reflect 代替了Vue2中Object.defineProperty()方法实现数据的响应式(数据代理)
重写了虚拟DOM,速度更快了
新的组件: Fragment(片段) / Teleport(瞬移) / Suspense(不确定)
设计了一个新的脚手架工具—vite,npm run dev 秒开,热重载也很快。这种开发体验真是很爽,拒绝等待。
全局 API 现在只能作为 ES 模块构建的命名导出进行访问
Vue3可多个根组件
Vue3中v-if优先于v-for,移除过滤器
import {
defineComponent,//目的是定义一个组件,vue3如果用ts,导出时候要用 defineComponent,这俩是配对的,为了类型的审查正确
defineAsyncComponent,//vue3中动态引入组件
provide, //祖孙组件传参
inject, //祖孙组件传参
reactive, //定义多个数据的响应式,可配合torefs使用方便
toRefs, //torefs函数可以将reavtive创建的响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref,html模板中直接使用变量显示数据
ref, //定义一个数据的响应式(一般是基本数据类型),通过.value获取到值,如果是object之类会自动转化为reactive
computed, //计算属性,如果只传入一个回调函数,表示的是get,可get,set
watch, //监听一个或多个属性或对象,默认不执行---可加参数immediate表示默认执行一次,deep表示深度监视
watchEffect, //不需要配置immediate,默认就会执行一次。监视所有回调中使用的数据
onMounted, //生命周期
onUnmounted,
onUpdated,
onBeforeUpdate,
onBeforeMount,
onBeforeUnmount,
nextTick,//整个视图都渲染完毕后
customRef,//追踪数据并告诉Vue去触发界面更新,可用于对页面更新进行一个防抖
readonly,//数据只读
shallowReactive,// 只处理了对象内最外层属性的响应式(也就是浅响应式)
shallowRef,//只处理了value的响应式, 不进行对象的reactive处理
toRaw,//返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。
} from "vue";
setup是组合api的入口函数,函数返回对象,html模板中可以直接使用。
setup细节问题:
1.setup在beforeCreate之前执行。
由此推断setup在执行的时候,当前组件还没有创建出来,也意味着组件实例对象this根本不能用(undefined)。
2.setup中的返回值是一个对象,内部的属性和方法是给html模板使用的。
import {defineComponent} from "vue";
export default defineComponent({
setup(props, {attrs,slots,emit}){
console.log("this=>", this); //undefined
console.log(props);//父组件传过来是值
console.log(slots);//插槽
console.log(attrs); //当前组件标签上的属性,不带冒号的,带冒号是props
//如 text是attrs,mag是props
---------------------
const addCount = () => {
emit("demoHander", "我的子组件传来的");//向父组件传参,这种写法父组件@demoHander='函数'
//或者
props.demoHander('我的子组件传来的')//这种写法父组件:demoHander='函数'
};
return {};//必须为一个对象
}
});
<template>
<div>
{{ count }}
{{state.age}}
<button @click="add">加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive} from "vue";
export default defineComponent({
setup() {
// let count = 0;//这个数据不是响应式数据,(响应式数据:数据变化,页面跟着渲染)
let count = ref(0); //ref一般定义一个数据的响应式(一般是基本数据类型,单独定义某一个常量),方法中要通过count.value获取到值,如果ref是一个对象也可以,会自动转为reactive
---------------------------------------------
let obj = {
name: "tom",
age: 25,
wife: {
name: "marry",
age: 22,
},
};
let state = reactive(obj); //reactive一般定义多个数据(复杂数据类型)的响应式,如果不使用torefs的话,html模板中要使用state.age显示数据
//方法
const add = () => {
count.value++;//操作ref定义的值
state.age++;//操作reactive定义的值
------------------------
// obj.age++;这种方式页面不会更新渲染,需要操作代理对象才有用
// delete state.wife//删除某个属性,页面会更新
};
return {
count,
add,
state //响应式数据
//...state 不是响应式数据
//...toRefs(state) 响应式数据
};
},
});
</script>
reactive不能处理简单数据类型。如reactive(10)是无法响应式的,必须是一个对象reactive({ })
ref 是一个对象也可以,会自动转为reactive。如ref({ id:10 })
注意: 解构state 或者 扩展运算符都会使数据失去响应性
toRefs()函数可以将reactive创建出来的响应式对象,转换为普通对象,只不过这个对象上的每个属性节点,都是ref()类型的响应式数据
(使用…操作符返回的对象,不会失去响应性)
<template>
<div>
<!-- 不使用toRefs:{{state.age}} -->
<!--使用toRefs -->{{ age }}
</div>
</template>
<script lang="ts">
import { defineComponent,reactive,toRefs} from "vue";
export default defineComponent({
setup() {
let obj = {
name: "tom",
age: 25,
wife: {
name: "marry",
age: 22,
},
};
let state = reactive(obj);
return {
//state
...toRefs(state)
};
},
});
</script>
<template>
<div>
<input type="text" ref="inputRef" name="" id="" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref,} from "vue";
export default defineComponent({
setup() {
//通过ref自动获取焦点
const inputRef = ref<HTMLElement | null>(null);
onMounted(() => {
inputRef.value && inputRef.value.focus();
});
return {
inputRef,
};
},
});
</script>
setup() {
let state = reactive({
fristName: "东方",
laetName: "不败",
fallName3:""
});
------------方法一
watch(state,({fristName,laetName})=>{
fallName3.value=fristName+'_'+laetName
},{immediate:true,deep:true})
//immediate表示默认执行一次
//deep表示深度监视
------------方法二
const stop=watch(() => state.fristName, (val,oldval) => {
console.log(val);
},{deep:true});
//直接调用stop(),可以清除监听
return {
...toRefs(state)
};
}
//监听reactive创造出来的数据使用箭头函数,ref直接放
watch([() => state.fristName, () => state.laetName, fallName3], (val) => {
console.log(val);
});
//watchEffect不需要配置immediate,默认就会执行一次
// watchEffect: 监视所有回调中使用的数据
watchEffect(()=>{
fallName3.value= state.fristName+'_'+state.laetName
})
vue2.0写法
watch: {
/**
* 监听路由变化刷新页面
*/
$route(to) {
logger.log("watch router invoke", to.query);
this.initPage();
},
configureList: {
handler(newVale) {
logger.log("watch configureList invoke", newVale);
},
deep: true,
immediate: false,
},
},
注意:计算属性如果只传入一个回调函数,表示的是get。
场景:三个input任意一个值变化,其他两个也会变化
<input type="text" v-model="state.fristName"/>
<input type="text" v-model="state.laetName"/>
<input type="text" v-model="fallName"/>
计算属性如果只传入一个回调函数,表示的是get。即fristName或laetName改变,fallName发生改变。但是fallName发生改变,其他两个值不会变化
setup() {
let state = reactive({
fristName: "东方",
laetName: "不败",
});
//fallName 返回的是一个ref对象
const fallName = computed(() => {
return state.fristName + "_" + state.laetName;
});
}
解决办法:
const fallName = computed({
get() {
return state.fristName + "_" + state.laetName;
},
set(val: any) {
const names = val.split("_");
state.fristName = names[0];
state.laetName = names[1];
},
});
父组件
<template>
<h1>父组件</h1>
<p>当前颜色: {{color}}</p>
<Son />
</template>
<script lang="ts">
import { provide, ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'ProvideInject',
components: {
Son
},
setup() {
const color = ref('red')
provide('color', color)
return {
color
}
}
}
</script>
子组件
<template>
<h2>子组件</h2>
<GrandSon />
</template>
<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
components: {
GrandSon
},
}
</script>
孙组件
<template>
<h3 :style="{color}">孙子组件: {{color}}</h3>
</template>
<script lang="ts">
import { inject } from 'vue'
export default {
setup() {
const color = inject('color')
return {
color
}
}
}
</script>
抽离前:
<template>
<button @click="add">add</button>
<img :src="src" alt="">
</template>
<script>
import {reactive, toRefs} from "vue";
export default {
setup() {
const state = reactive({
index: 1,
src:''
});
const add = () => {
console.log(state.index)
state.index++;
console.log(state.index)
};
return {
...toRefs(state),
add
};
},
};
抽离后:
<template>
<button @click="add">add</button>
<img :src="src" alt="">
</template>
<script>
import hander from './hooks/demo.ts'
import {toRefs} from "vue";
export default {
setup() {
let {state,add}=hander()
return {
...toRefs(state),
add
};
},
};
demo.ts
import {reactive} from "vue";
export default function useMousePosition () {
const state = reactive({
index: 1,
src:''
});
const add = () => {
console.log(state.index)
state.index++;
console.log(state.index)
};
return {state,add}
}
<script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue";
// vue正常引入组件
import SuspensChild from '@/components/Suspense/Son.vue'
// vue2中动态引入组件的写法:(在vue3中这种写法不行)
const AsyncCompotent=()=>import('./Son.vue')
//vue3中动态引入组件
const SuspensChild = defineAsyncComponent(() => import("./Son.vue"));
</script>
注意:由于组件是异步引入的,会有一瞬间的空白,所以可以用Suspense配合loading填充,可以让我们创建一个平滑的用户体验
<template>
<Suspense>
//v-slot:default也可以写成#default
<template v-slot:default><!-- 组件加载完后显示-->
<SuspensChild/> <!-- 异步组件 -->
</template>
<template v-slot:fallback> <!-- 组件加载完前显示-->
<h1>LOADING...</h1> <!-- LOADING的内容 -->
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue";
const SuspensChild = defineAsyncComponent(() => import("./Son.vue"));
export default defineComponent({
name: "Suspense1",
components: {
SuspensChild,
},
async setup(){}
});
</script>
Teleport 让组件的html在父组件界面外的特定标签(可能是body,也可能是其他)下插入显示
<template>
<teleport to="body"> <!-- teleport 直接父级为body -->
<div>
<div>
I'm a teleported modal!
</div>
</div>
</teleport>
</template>
这些api在平常开发很少用到,这里我就不细讲了,有兴趣的朋友可以自行了解
script-setup 的推出是为了让熟悉 3.0 的用户可以更高效率的开发组件,只需要给 script 标签添加一个 setup 属性,那么整个 script 就直接会变成 setup 函数,所有顶级变量、函数,均会自动暴露给模板使用(无需再一个个 return 了)。
ElementUI-plus等框架也是用的setup语法糖
如果你使用的是 TypeScript ,还需要借助 defineComponent 来帮助你对类型的自动推导,使用前:
<!-- 标准组件格式 -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
// ...
return {
// ...
}
}
})
</script>
使用script-setup 后:
<!-- 使用 script-setup 格式 -->
<script setup lang="ts">
// ...
</script>
注意:
在 script-setup 模式下,新增了 4 个全局编译器宏,他们无需 import 就可以直接使用。
但是默认的情况下直接使用,项目的 eslint 会提示你没有导入,
可以配置一下 eslint
// 项目根目录下的 .eslintrc.js
module.exports = {
// 原来的lint规则,补充下面的globals...
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
},
}
使用setup语法糖后:
- 变量无需进行 return
- 子组件无需手动注册
- props 的接收方式变化为defineProps接收
- emits 的接收方式变化为defineEmits
- attrs 的接收方式变化为useAttrs
- 顶级 await 的支持
- 通过ref获取子组件的数据,需要子组件通过defineExpose暴露出来
<!-- 标准组件格式 -->
<template>
<p>{{ msg }}</p>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
const msg: string = 'Hello World!';
// 要给 template 用的数据需要 return 出来才可以
return {
msg
}
}
})
</script>
在 script-setup 模式下
<template>
<div class="home">
<div>加数:<input type="text" v-model="state.addNum1" /></div>
<div>加数:{{ state.addNum2 }}</div>
<div>和:{{ state.sum }}</div>
<button @click="handleAddNum">=</button>
</div>
</template>
<script lang="ts" setup>
import { reactive, provide, computed,ref } from "vue";
const state = reactive({
addNum1: 1,
addNum2: 1,
sum: 0,
nav: computed(() => store.state.home.nav),
});
const hellow=ref()
const msg: string = 'Hello World!';
const handleAddNum = () => {
const { addNum1, addNum2 } = state;
state.sum = Number(addNum1) + addNum2;
};
</script>
<!-- 标准组件格式 -->
<template>
<Child />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
// 导入子组件
import Child from '..//Child.vue'
export default defineComponent({
// 需要启用子组件作为模板
components: {
Child
},
// 组件里的业务代码
setup () {
// ...
}
})
</script>
在 script-setup 模式下,只需要导入组件即可,编译器会自动识别并启用。
<!-- 使用 script-setup 格式 -->
<template>
<Child />
</template>
<script setup lang="ts">
import Child from '@cp/Child.vue'
</script>
由于整个 script 都变成了一个大的 setup function ,没有了组件选项,也没有了 setup 入参,所以没办法和标准写法一样去接收 props 了。
这里需要使用一个全新的 API :defineProps 。
defineProps([
'name',
'userInfo',
'tags'
])
如果 script 里的方法要拿到 props 的值
const props = defineProps([
'name',
'userInfo',
'tags'
])
console.log(props.name);
const { tags} = toRefs(props)
props校验机制:
defineProps({
name: {
type: String,
required: false,
default: 'Petter'
},
userInfo: Object,
tags: Array
});
或者使用ts校验
interface UserInfo {
id: number;
age: number;
}
defineProps<{
nname?: string;
tags: string[];
userInfo: UserInfo;
}>();
defineEmits 的用法和原来的 emits 选项差别不大
// 获取 emit
const emit = defineEmits(['chang-name']);
// 调用 emit
emit('chang-name', 'Tom');
attrs 和 props 很相似,也是基于父子通信的数据,如果父组件绑定下来的数据没有被指定为 props ,那么就会被挂到 attrs 这边来。
// 标准组件的写法
export default defineComponent({
setup (props, { attrs }) {
// attrs 是个对象,每个 Attribute 都是它的 key
console.log(attrs.class);
// 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
console.log(attrs['data-hash']);
}
})
useAttrs 的基础用法
// 导入 useAttrs 组件
import { useAttrs } from 'vue'
// 获取 attrs
const attrs = useAttrs()
// attrs是个对象,和 props 一样,需要通过 key 来得到对应的单个 attr
console.log(attrs.msg);
通过ref获取子组件的数据,需要子组件通过defineExpose暴露出来
标准组件写法里,子组件的数据都是默认隐式暴露给父组件的,也就是父组件可以通过 childComponent.value.foo 这样的方式直接操作子组件的数据
但在 script-setup 模式下,所有数据只是默认隐式 return 给 template 使用,不会暴露到组件外,所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。
在 script-setup 模式下,如果要调用子组件的数据,需要先在子组件显示的暴露出来,才能够正确的拿到,这个操作,就是由 defineExpose 来完成。
子组件:
<script setup lang="ts">
// 定义一个想提供给父组件拿到的数据
const msg: string = 'Hello World!';
// 显示暴露的数据,才可以在父组件拿到
defineExpose({
msg
});
</script>
然后你在父组件就可以通过挂载在子组件上的 ref 变量,去拿到暴露出来的数据了
<template>
<div class="home">
<HelloWorld ref="hellow" />
<button @click="handleAddNum">=</button>
</div>
</template>
<script lang="ts" setup>
import {ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
const hellow=ref()
const handleAddNum = () => {
console.log('通过ref获取子组件defineExpose暴露的数据',hellow.value.msg2);
};
</script>
在 script-setup 模式下,不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成 async setup 。
<script lang="ts">
<!-- 标准组件格式 -->
import { defineComponent, withAsyncContext } from 'vue'
export default defineComponent({
async setup() {
const post = await withAsyncContext(
fetch(`/api/post/1`).then((r) => r.json())
)
return {
post
}
}
})
</script>
<script setup lang="ts">
<!-- script-setup 模式下 -->
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
let obj = {}
// 1.要操作的对象
// 2.要操作的属性
// 3.具体值
//数据劫持
Object.defineProperty(obj, 'username', {
get: function () {
console.log('取值')
},
set: function (val) {
console.log('赋值')
},
})
//目标对象
let obj = {
name: "tom",
age: 25,
wife: {
name: "marry",
age: 22,
},
}
//把目标对象变成代理对象
//参数1:obj-->目标对象;参数2:handler-->处理器对象,用来监视数据,及数据的操作;
const proxyUser = new Proxy(obj, {//
//get获取目标对象的某个属性值
get(target, prop) {
console.log('get')
//这里要通过 Reflect反射返回,否则proxyUser.name是undefiend
return Reflect.get(target, prop)
},
//set方法不仅能修改还能为目标对象增加属性值
set(target, prop, newval) {
console.log('set')
return Reflect.set(target, prop, newval)
},
//deleteProperty删除目标对象的某个属性值
deleteProperty(target, prop) {
console.log('deleteProperty')
return Reflect.deleteProperty(target, prop)
},
})
---
注意:直接操作目标对象,视图是不会跟新的
---
//-----获取属性值测试
console.log('结果=>', proxyUser.name);
//-----更改属性值测试
proxyUser.name = '小明'
//-----增加属性值测试
proxyUser.sax = '男'
//-----删除属性值测试
delete proxyUser.name
//-----更改深层次属性值测试
proxyUser.wife.name='小红'
<template>
<div class="home">
<button @click="alter">路由</button>
</div>
</template>
<script>
import {defineComponent} from "vue";
import { useRouter, useRoute,onBeforeRouteLeave } from "vue-router";
//守卫beforeRouteEnter,在composition api中被移除了,解决办法
//1、可以通过watch监听路由实现
//2、beforeRouteEnter不需要导入,只需要写在setup外即可
import { useStore } from "vuex";
export default {
name: "Home",
beforeRouteEnter(to,from,next){
next(vm=>{
console.log('组件内的守卫===',vm)
})
},
setup() {
let router = useRouter();//路由实例
let route = useRoute()//路由信息,query,params等
let store = useStore();
const alter = () => {
console.log(store.state.num);
};
onBeforeRouteLeave((to,from,next) => {
next()
});
//监听路由变化
watch(route,(val)=>{
console.log('watch',val.matched)
},{immediate:true,deep:true})
const routerto = () => {
router.push({
path: "/about",
query: {
id: 2,
},
});
};
return {
};
},
};
</script>
数据监听方式变成了Proxy,消除了Object.defineProperty现有的限制(例如无法检测新的属性添加),并提供更好的性能。
vue3解决了vue2的一些问题,大型应用的性能问题、ts支持不友好问题,自定义渲染API解决体系架构存在的问题,Compostion API让代码的组织形式更好。vite开发构建工具让开发体验更好,Tree shaking让包更小、性能更优。
总的来说vue3还是非常棒的,带来了很多非常好的新特性。