vue3的语法
main.js中写发生变化,并不兼容vue2的写法
//vue3
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'createApp(App).mount('#app')//vue2
import Vue from 'vue'
import './style.css'
import App from './App.vue'const vm = new Vue({render:h=>h(app)
})
vm.$mount("#app")
vue3中template模版可以不使用根标签。
Composition API
setup:组件中所用到的数据,方法,计算属性,生命周期等,均配置在这里,返回有两种值:
- 对象:其属性,方法在模版中可以直接使用。
- 返回一个渲染函数,可自定义渲染内容,会直接代替模版内容,不常用
setup在beforeCreate之前执行一次,this是undefined
setup含有两个参数:
props:值为对象,包含组件外部传递过来的,且组件内部声明接收了的属性,想要获取props的属性,一定需要props接收参数,这点和vue2相同,倘若不使用props接收,那么数据会存储在context参数中的attrs属性中。
context:上下文对象,包括
attrs:包含组件外部传递过来,但没有在props中声明的属性,相当于替换$attrs
slots:收到的插槽内容,相当于this.$slots,
emit:分发自定义时间的函数,相当于挺好$emit(见下方代码)
emits与vue2有区别,在vue3中传递给父组件需要emits来声明已绑定在子组件的事件。
vue3不要和vue2混用,vue2可以访问到setup中的数据,但setup不能访问到vue2的数据,如果有重复数据,setup的优先级更高。setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模版会看不到return中的属性。
<script>
import { h } from 'vue';
import HelloWorld from './components/HelloWorld.vue'
export default{name:"app",props:['name','age'],emits:['test']compponents:{HelloWorld},setup(props,context) {//非响应式数据let name = props.name?props.name:'张三'let age = props.age?props.agge:18function Greeting(){console.log(name,age);}//返回一个对象(常用)return {name,age,Greeting}function test(){context.emit(test,'123')}//返回渲染函数// return ()=> h("h1",'你好')}
}
</script><template><div>app</div><p>{{name }}-{{ age }}</p>
</template><style scoped>
</style>
响应式数据
ref函数 适用于基本类型数据,也可以是对象类型,创建一个reference的引用对象,修改数据需要.value的值,模版中读取数据不需要.value,直接使用即可。
接收的数据可以是基本类型也可以是对象类型,基本类型的书记是靠数据劫持Object.defineProperty()完成的,而对象类型会转为Proxy对象,借助了reactive函数。
reactive函数 定义一个对象类型的响应式数据(基本类型不需要它,使用ref),包括数组类型也支持响应式。
内部基于es6的proxy实现,返回一个代理对象Proxy,通过代理对象内部数据进行操作,reactive定义的响应式数据是深层次的。
<script>
import {ref,reactive} from 'vue'
import HelloWorld from './components/HelloWorld.vue'
export default{name:"app",props:['name','age'],compponents:{HelloWorld},setup(props) {//定义响应式数据let name = ref('张三')let age = ref(19)let obj = reactive({type:"ui",salary:"100k"})function msgChange(){name.value = "李四"age.value = 20obj.type = 'web'obj.salary = '200k'}//返回一个对象(常用)return {name,age,msgChange,obj}}
}
</script><template><div>app</div><p>{{name }}-{{ age }}</p><p>{{obj.type }}-{{ obj.salary }}</p><button @click="msgChange">修改信息</button>
</template><style scoped>
</style>
proxy的响应式实现原理:
通过Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写,属性的添加,属性的删除等,比起vue2多出了增加和删除属性的监听。
通过Reflect(反射):对源对象的属性进行操作。类似于objec的操作方法。
使用Reflect的原因:在封装框架的过程中,Object.defineProperty()如果添加了相同的属性名,那么会直接报错,导致js线程阻塞,如果要解决那么就要写很多的 try{} catch(){} ,Reflect.defineProperty()会有个返回值,可以通过判断决定是否即系往下走,所以Reflect相对友好一点。
<script>
import HelloWorld from './components/HelloWorld.vue'
import {ref,reactive} from 'vue'export default{name:"app",props:['name','age'],compponents:{HelloWorld},setup(props) {let person = {name:"张三",age:18}const p = new Proxy(person,{//target就是person本身//以下两种方式都可以,vue3主要采用Reflect的方式get(target,propName){// return target[propName]return Reflect.get(target,propName)},//读取和新增属性,都会调用这个方法set(target,propName,value){// target[propName] = valueReflect.set(target,propName,value)},deleteProperty(target,propName){// return delete target[propName]return Reflect.defineProperty(target,propName)}})function msgChange(){person.name='李四'}//返回一个对象(常用)return {person}}
}
</script><template><div>app</div><p>{{person.name }}-{{ person.age }}</p><button @click="msgChange">修改信息</button>
</template><style scoped>
</style>
计算属性computed:
需要引入computed函数,配置与vue相似。
<script>
import HelloWorld from './components/HelloWorld.vue'
import {ref,reactive,computed} from 'vue'export default{name:"app",setup(props) {//定义响应式数据let name = ref('张三')let age = ref(19)//简写let fullName = computed(()=>{return name.value})//完整写法let fullName2 = computed({get(){return name.value},set(value){name.value = value}})//返回一个对象(常用)return {name,age,fullName,fullName2}}
}
</script><template><div>app</div><p>{{fullName}}</p><p>{{fullName2}}</p>
</template><style scoped>
</style>
监听watch:
<script>
import HelloWorld from './components/HelloWorld.vue'
import {ref,reactive,computed,watch} from 'vue'export default{name:"app",setup(props) {//定义响应式数据let name = ref('张三')let age = ref(19)let person = reactive({name:"李四",age:19,job:{job1:'20k'}})//监听ref定义的数据,多个使用数组watch([name,age],(newVal,oldVal)=>{console.log(111);},{immediate:true,deep:true})//监听reactive定义的数据,会强制开启深度监听模式,拿不到oldValuewatch(person,(newVal,oldVal)=>{console.log(newVal,oldVal);},{immediate:true,deep:true})//监听reactive定义的数据的一个属性,多个属性用数组watch([()=>person.name,()=>person.age],(newVal,oldVal)=>{console.log(newVal,oldVal);})//特殊情况,监听reactive对象属性,这个属性是个对象,需要开启深度监听,否则无效,同时也拿不到oldValuewatch(()=>person.job,(newVal,oldVal)=>{console.log(newVal,oldVal);},{deep:true})function nameChange(e){name.value = '333'}function nameChange(e){name.value = '333'person.name='222'}//返回一个对象(常用)return {name,age,nameChange,person}}
}
</script><template><div>app</div><p>{{name}}</p><button @click="nameChange">修改</button>
</template><style scoped>
</style>
监听reactive对象时,不能获取到oldvalue,并且强制开启了deep深度监听配置,而且配置无效。
监听reactive对象的某个属性(为对象)时,必须deep配置才有效。
监听ref对象时,需要添加.value,实际上就是监听proxy代理的reactive的对象数据,强制开启了深度监听。
监听ref对象时,也可以直接使用对象名称,但是需要自己开启deep深度监听。
let person = ref({name:"李四",age:19,job:{job1:'20k'}})//监听ref定义的数据,深度监听要自己手动添加deep深度监听watch(person,(newVal,oldVal)=>{console.log(111);},{deep:true})//实际是reactive数据,强制开启了deep深度监听watch(person.value,(newVal,oldVal)=>{console.log(newVal,oldVal);})
watchEffect:同样需要先引入,不用指明监视的属性,监视的回调中用到哪个属性,就监视哪个属性。
<script>
import HelloWorld from './components/HelloWorld.vue'
import {ref,reactive,computed,watch,watchEffect} from 'vue'export default{name:"app",setup(props) {//定义响应式数据let name = ref('张三')let age = ref(19)let person = reactive({name:"李四",age:19,job:{job1:'20k'}})watchEffect((()=>{const x1 = name.valueconst x2 = person.job.job1console.log('watchEffect');}))function nameChange(e){name.value = '333'}function nameChange(e){name.value = '333'person.name='222'}//返回一个对象(常用)return {name,age,nameChange,person}}
}
</script><template><div>app</div><p>{{name}}</p><button @click="nameChange">修改</button>
</template><style scoped>
</style>
生命周期
vue2通过new Vue()的形式创建vue实例,在没有el的情况下需要等$mount函数调用才可以继续往下走,但已经执行了beforeCreate created生命周期,其实只创建了vm,并没有被挂载,会造成资源浪费;vue3通过createApp并且执行了mount函数之后,再执行生命周期函数,并且减少了判断次数,提高执行效率。
beforeDestroy替换为beforeUnmount
destroyed替换为unmounted
组合式生命周期api:
beforeCreate => setup()
created => setup()
beforeMount => onBeforeMount
mounted => onMounted
beforeUpdate => onBeforeUpdate
updated => onUpdated
beforeUnmount => onBeforeUnmount
unmounted => onUnmounted
<script>
import HelloWorld from './components/HelloWorld.vue'
import {ref,reactive,computed,watch,watchEffect,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue'export default{name:"app",setup(props) {onBeforeMount(() => {}),onMounted(() => {}),onBeforeUpdate(() => {}),onUpdated(() => {}),onBeforeUnmount(() => {}),onUnmounted(() => {}),//返回一个对象(常用)return {}}
}
</script><template><div>app</div>
</template><style scoped>
</style>
自定义hook函数:把setup函数中使用的组合式api进行封装。
//useMounted.js
import {onMounted, reactive,} from 'vue'
export default function (){let obj = reactive({name:null,age:null})onMounted(()=>{obj.name = '789'obj.age=19})return obj
}
<script>
import HelloWorld from './components/HelloWorld.vue'
import {ref,reactive,computed,watch,watchEffect} from 'vue'
import useMounted from './hooks/useMounted'
export default{name:"app",setup(props) {let obj = useMounted()//返回一个对象(常用)return {obj}}
}
</script><template><div>app</div><p>{{obj.name}}-{{ obj.age }}</p>
</template><style scoped>
</style>
toRef和toRefs:使对象里所有的属性都变为ref响应式数据。toRef智能操作一个属性,toRefs类似于浅拷贝,可以直接将reactive响应式对象放入,实现多个属性的转变,将数据拆散了return出去。
<script>
import HelloWorld from './components/HelloWorld.vue'
import {toRefs,reactive,computed,watch,watchEffect,toRef} from 'vue'
export default{name:"app",setup(props) {let person = reactive({name:"李四",age:19,job:{job1:'20k'}})let student = reactive({grade:3,class:4})function nameChange(e){person.name = '333'person.age=99}//返回一个对象(常用)return {name:toRef(person,'name'),age:toRef(person,'age'),...toRefs(student),nameChange,}}
}
</script><template><div>app</div><p>{{name}}-{{ age }}</p><p>{{grade}}-{{ classes }}</p><button @click="nameChange">修改</button>
</template><style scoped>
</style>
其他组合API(都需要在顶部引入对应的api)
shallowReactive与shallowRef
用法和ref相同,shallowReactive只会将对象最外层的属性作为响应式数据,内层的不是响应式。
shallowRef 如果传入是个基本类型,ref和shallowRef没有区别,如果传入是个对象,shallowRef不再进行响应式处理,但是ref会转为proxy的响应式。
readOnly和shallowReadOnly
readOnly将传入的对象类型的响应式数据全部变为只读,不可更改值,shallowReadOnly将响应式对象类型的数据第一层处理为readOnly深层次不处理,对于基本类型都处理为只读
obj = readOnly(obj)
obj = shadowReadOnly(obj)
toRaw与markRaw
toRaw 将一个由reactive生成的响应式对象转为普通对象,不会引起页面更新。
markRaw 标记一个对象,使其永远不会再成为响应式对象(有些值不应该被设为响应式,例如第三方的库等等),跳过响应式转换可以提高性能。
let obj = toRaw(person)
obj.car = markRaw(car)
customRef(自定义ref)
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制
示例:
<script>
import { customRef } from 'vue'
export default {name: "app",setup(props) {let keyWord = myRef('hello',1000)function myRef(value,delay) {let timer;return customRef((track, trigger) => {return {get() {track() //通知vue追踪数据变化return value},set(newValue) {clearTimeout(timer)timer = setTimeout(() => {value = newValuetrigger() //通知vue去重新解析模版}, delay)}}})}//返回一个对象(常用)return {keyWord}}
}
</script><template><input type="text" v-model="keyWord"><h3>{{ keyWord }}</h3>
</template><style scoped></style>
provide与inject(需要引入)
实现祖孙组件之间的通信:
祖组件:
//祖组件
setup(){let car = reactive({name:'张三',age:9})provide('car',car)
}//孙组件
setup(){const car = inject('car')return{car}
}
响应式数据的判断
isRef:是否为ref对象
isReactive:是否为reactive创建的响应式
isReadonly:是否是由readonly创建的只读代理
isProxy:是否由reactive或者readonly方法创建的代理
一些新的组件
Fragment
vue2中组件必须有一个根标签,vue3中可以没有根标签,内部会将多个标签包含在一个Fragment虚拟元素中,减少了标签层级,减少内存占用。
Teleport
是一种能将我们组件html结构移动到指定位置的技术
to:指定所要传送的位置(body / html /css选择器),会改变页面的html的布局。
<teleport to="body"><Demo />
<teleport/>
Suspense组件
等待异步组件时渲染一些额外内容,用户体验变好。
//父组件
<Suspense><template v-slot:default><Child /><template /><template v-slot:fallback> //有个加载中的画面提示<h3>加载中。。。<h3/><template />
<Suspanse/>//异步引入
<script>import {defineAsyncCompponent} from 'vue'const Child = defineAsyncCompponent(()=>import('./Child')) //动态引入export default{components:{Child}}
</script>
//子组件
<template><h3>子组件<h3/>
<template/>//异步引入
<script>import {defineAsyncCompponent,ref} from 'vue'const Child = defineAsyncCompponent(()=>import('./Child')) //动态引入export default{setup(){let sum = ref(0)let p = new Promise((resolve,reject)=>{setTimeout(()=>{resolve(sum)},3000)}) return await p}}
</script>
全局api的转移
全部调整带应用实例app上了
2.x全局API(Vue) | 3.x全局API(app) |
Vue.config.xxx | app.config.xxx |
Vue.config.productionTip | 移除 |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vie.use | app.use |
Vue.prototype | app.config.globalProperties |
其他改变:
data选项应始终被声明为一个函数,防止组件在被复用的时候产生数据的关联关系,造成干扰。
过渡类名的更改:
<style>//vue2.v-enter,.v-leave-to{opacity:0;};.v-leave,.v-enter-to{opacity:1;}//vue3.v-enter-from,.v-leave-to{opacity:0;};.v-leave-from,.v-enter-to{opacity:1;}
<style/>
移除keyCode作为v-on的修饰符,会存在一定的兼容性问题,所以移除。
已不被支持keyUp.13
不支持config.keyCodes自定义别名按键(Vue.config.keyCodes.huiche=13)
移除v-on.native修饰符:子组件中emits不接受的click事件为原生事件,否则为自定义事件。
父组件:
<div>
<Child @click = "handleAdd" @close="handleClose" />
</div>
子组件:export default{
emits:['close']
}
移除过滤器filter
过滤器虽然看起来很方便,但需要一个自定义语法,打破了大括号内表达式是‘只是javascript’的假设,不仅有学习成本,而且还有实现成本,建议使用方法调用或者计算属性替换过滤器。
setup语法糖
语法糖里面的代码会被编译成组件setup()
函数的内容,不需要通过return
暴露声明的变量、函数以及import
引入的内容,即可在<template/>
使用,并且不需要些export default{}
这意味着与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行
引入组件将自动注册
<script setup>
import HelloWorld from './components/HelloWorld.vue'
let msg = '你好';
function name(){return '123'
}
</script><template><div><a href="https://vitejs.dev" target="_blank"><img src="/vite.svg" class="logo" alt="Vite logo" /></a><a href="https://vuejs.org/" target="_blank"><img src="./assets/vue.svg" class="logo vue" alt="Vue logo" /></a><p>{{ msg }}</p><p>{{ name() }}</p></div><HelloWorld msg="Vite + Vue" />
</template><style scoped>
.logo {height: 6em;padding: 1.5em;will-change: filter;transition: filter 300ms;
}
.logo:hover {filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
组件通信必须要使用defineProps
和defineEmits
API来代替props
和emits
Props:
<template><div>父组件</div><Child :title="msg" />
</template><script setup>
import { ref } from 'vue' // 引入ref
import Child from './child.vue'
const msg = ref('父的值') //自动返回,在template直接解套使用
</script>
<template><div>子组件</div><div>父组件传递的值:{{title}}</div>
</template><script setup>
//import {defineProps} from 'vue' 不需要引入//语法糖必须使用defineProps替代props
const props = defineProps({title: {type: String}
});
//script-setup 需要通过props.xx获取父组件传递过来的props
console.log(props.title) //父的值
</script>
Emits:
<template><div>子组件</div><button @click="toEmits">子组件向外暴露数据</button>
</template><script setup>
import {ref} from 'vue'
const name = ref('我是子组件')
//1、暴露内部数据
const emits = defineEmits(['childFn']);const toEmits = () => {//2、触发父组件中暴露的childFn方法并携带数据emits('childFn',name)
}
</script>
<template><div>父组件</div><Child @childFn='childFn' /><p>接收子组件传递的数据{{childData}} </p>
</template><script setup>
import {ref} from 'vue'
import Child from './child.vue'const childData = ref(null)
const childFn=(e)=>{consloe.log('子组件触发了父组件childFn,并传递了参数e')childData=e.value
} </script>
需要主动暴露组件属性:defineExpose
使用 <script setup>
的组件是默认关闭的,即通过模板 ref
或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。
<script setup>
import { ref } from 'vue'const a = 1
const b = ref(2)
//主动暴露组件属性
defineExpose({a,b
})
</script>
<template><div>父组件</div><Child ref='childRef' /><button @click='getChildData'>通过ref获取子组件的属性 </button>
</template><script setup>
import {ref} from 'vue'
import Child from './child.vue'
const childRef= ref() //注册响应数据
const getChildData =()=>{//子组件接收暴露出来得值console.log(childRef.value.a) //1console.log(childRef.value.b) //2 响应式数据
}
</script>
语法糖其他功能
useSlots
和 useAttrs
(少用,由于大部分人是SFC模式开发,在<template/>
通过<slot/>
标签就可以渲染插槽)
如果需要在script-setup
中使用 slots
和 attrs
需要用useSlots
和 useAttrs
替代
需要引入:import { useSlots ,useAttrs } form 'vue'
在<template/>
中通过 $slots
和$attrs
来访问更方便(attrs
用来获取父组件中非props
的传递到子组件的参数/方法,attrs
用来获取父组件中非props
的传递到子组件的参数/方法,attrs
用来获取父组件中非props
的传递到子组件的参数/方法,slots
可以获取父组件中插槽传递的虚拟dom
对象,在SFC模式应该用处不大,在JSX /TSX使用比较多)
//父组件
<template><Child msg="非porps传值子组件用attrs接收" ><!-- 匿名插槽 --><span >默认插槽</span><!-- 具名插槽 --><template #title><h1>具名插槽</h1></template><!-- 作用域插槽 --><template #footer="{ scope }"><footer>作用域插槽——姓名:{{ scope.name }},年龄{{ scope.age }}</footer></template></Child>
</template><script setup>
// 引入子组件
import Child from './child.vue'
</script>
//子组件
<template><!-- 匿名插槽 --><slot /><!-- 具名插槽 --><slot name="title" /><!-- 作用域插槽 --><slot name="footer" :scope="state" /><!-- $attrs 用来获取父组件中非props的传递到子组件的参数 --><p>{{ attrs.msg == $attrs.msg }}</p><!--true 没想到有啥作用... --><p>{{ slots == $slots }}</p>
</template><script setup>
import { useSlots, useAttrs, reactive, toRef } from 'vue'
const state = reactive({name: '张三',age: '18'
})const slots = useSlots()
console.log(slots.default()); //获取到默认插槽的虚拟dom对象
console.log(slots.title()); //获取到具名title插槽的虚拟dom对象
// console.log(slots.footer()); //报错 不知道为啥有插槽作用域的无法获取
//useAttrs() 用来获取父组件传递的过来的属性数据的(也就是非 props 的属性值)。
const attrs = useAttrs()
</script>
useSlots
或许在JSX/TSX下更实用
<script lang='jsx'>
import { defineComponent, useSlots } from "vue";
export default defineComponent({setup() {// 获取插槽数据const slots = useSlots();// 渲染组件return () => (<div>{slots.default?slots.default():''}{slots.title?slots.title():''}</div>);},
});
</script>
其他的可以参考官网:快速上手 | Vue.js