Vue3.0学习建议
针对学会vue2.0的同学
Vue3.0新特性
- Composition Api (最核心)
- v-model更改
- v-for的key节点上的使用情况更改
- v-if和v-for对同一元素的优先级更高
- ref内部v-for不再注册引用数组
- 功能组件只能使用普通函数创建
- 异步组件需要使用
defineAsyncComponent创建方法 - 所有插槽都通过
$slots - 在
destroyed生命周期的选项已更名为unmounted - 在
beforeDestroy生命周期的选项已更名为beforeUnmount
Vue3.0优缺点
优点:
- 将Vue内部的绝大部分api对外暴露,使Vue具备开发大型项目的能力,例如compile编译api等
- webpack的treeshaking(tree shaking 是 DCE 的一种方式,它可以在打包时忽略没有用到的代码。)支持度友好
- 使用Proxy进行响应式变量定义,性能提高1.2~2倍
- ssr快了2~3倍
- 可在Vue2.0中单独使用composition-api插件,或者直接用它开发插件
- 对typescript支持更加友好
- 面向未来:对于尤雨溪最近创新的vite开发服务器(舍弃webpack、底层为Koa框架的高性能开发服务器),直接使用的Vue3.0语法
缺点:
- 只支持IE11及以上
- 对于习惯了Vue2.0开发模式的开发者来说,增加了心智负担,对开发者代码组织能力有体验
同时也是能力提升的机会吧,特别喜欢Vue作者的而设计初心:让开发者随着框架一起成长
体验Vue3.0的四种姿势
现在来说,体验Vue3.0有四种姿势
-
通过CDN:
<script src="https://unpkg.com/vue@next"></script> -
通过 Codepen 的浏览器 playground
-
脚手架 Vite:
npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
尤大开发的新工具,目的是以后取代webpack,原来就是利用浏览器现在已经支持ES6的import;遇到import会发送一个http请求去加载对应的文件,vite拦截这些请求,做预编译,就省去了webpack冗长的打包事件,提升开发体验。
-
脚手架 vue-cli
npm install -g @vue/cli # OR yarn global add @vue/cli vue create hello-vue3 # select vue 3 preset
全局API
新的全局api:createApp
调用createApp返回一个应用实例,这是Vue3.0的新概念:
打开src/main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
应用程序实例暴露当前全局 API 的子集,经验法则是,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上app上,以下是当前全局 API 及其相应实例 API 的表:
| 2.x 全局 API | 3.x 实例 API (app) |
|---|---|
| Vue.config | app.config |
| Vue.config.productionTip | removed 已移除 |
| Vue.config.ignoredElements | app.config.isCustomElement |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
composition API学习
setup
setup函数是一个新的组件选项。作为组件内使用Composition API的入口点
创建组件实例,然后初始化props,紧接着调用setup函数。它会在beforeCreate钩子之前调用。
setup返回一个对象。则对象的所有属性(它是响应式的数据)都可以直接在模板中使用。相当于vue2.0中data函数返回的对象。
App.vue
<script>
export default {
setup () {
return {}
}
}
</script>
响应式数据
- ref:可传入任意类型的值并返回一个响应式且可改变的ref对象。ref对象拥有一个指向内部值的单一属性
.value,改变值的时候必须使用其value属性 - reactive:接受一个普通对象然后返回该普通对象的响应式代理。等同于2.x的
Vue.obserable()
简写之:reactive负责复杂数据结构,ref可以把基本的数据结构包装成响应式
reactive
<template>
<div>
<h2>{{state.count}}</h2>
<button @click="add">计算</button>
</div>
</template>
<script>
import { reactive } from "vue";
export default {
setup(){
// 响应式变量声明 reactive负责复杂数据结构,
const state = reactive({
count: 1
});
function add() {
state.count++;
}
return { state, add};
}
};
</script>
ref
<template>
<div>
<h2>{{state.count}}</h2>
<h3>{{num}}</h3>
<button @click="add">计算</button>
</div>
</template>
<script>
import { reactive, ref } from "vue";
export default {
setup(){
const state = reactive({
count: 1
});
const num = ref(0);
function add() {
state.count++;
num.value+=2
}
return { state, add, num };
}
};
</script>
ref包装的num,模板里可以直接用,但js中修改的时候操作.value属性。
toRefs
将响应式对象转换为普通对象,其中结果对象的每个property都是指向原始对象相应的property
从合成函数返回响应式对象时,toRefs非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解/扩散:
useFeatureX.js
import {reactive} from 'vue';
export function userFeatureX(){
const state = reactive({
foo: 1,
bar: 2
})
// 逻辑运行状态
// 返回时转换为ref
return state;
}
App.vue
import {toRefs} from 'vue'
export default {
setup(){
const state = useFeatureX();
return {
...toRefs(state)
}
}
}
computed
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
import { reactive, ref, computed } from "vue";
export default {
setup() {
// 1.响应式变量声明 reactive负责复杂数据结构,
const state = reactive({
count: 1
});
// 2.ref可以把基本的数据结构包装成响应式
const num = ref(0);
// 3.创建只读的计算属性
const computedEven1 = computed(() => state.count % 2);
// 4.创建可读可写的计算属性
const computedEven2 = computed({
get:()=>{
return state.count % 2;
},
set: newVal=>{
state.count = newVal;
}
})
// 事件的声明
function add() {
state.count++;
num.value += 2;
}
function handleClick() {
computedEven2.value = 10;
}
return { state, add, num, computedEven1,computedEven2,handleClick };
}
};
watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const num = ref(0)
watchEffect(() => console.log(count.value))
// -> 打印出 0
setTimeout(() => {
count.value++
// -> 打印出 1
}, 100)
-
停止监听
隐式停止
当
watchEffect在组件的setup()函数或生命周期钩子被调用时, 侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止显示停止
在一些情况下,也可以显示调用返回值来停止侦听
const stop = watchEffect(()=>{ /*...*/ }) //停止侦听 stop() -
清除副作用
有时候副作用函数会执行一些异步的副作用,这些响应需要在其失效时来清除(即完成之前状态已改变了)。可以在侦听副作用传入的函数中接受一个
onInvalidate函数作为参数,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:- 副作用即将重新执行时
- 侦听器被停止(如果在
setup()或生命周期钩子函数中使用了watchEffect,则在卸载组件时)
官网的例子:
watchEffect((onInvalidate) => { const token = performAsyncOperation(id.value) onInvalidate(() => { // id 改变时 或 停止侦听时 // 取消之前的异步操作 token.cancel() }) })
案例:实现对用户输入“防抖”效果
<template>
<div>
<input type="text"
v-model="keyword">
</div>
</template>
<script>
import { ref, watchEffect } from '@vue/composition-api'
export default {
setup() {
const keyword = ref('')
const asyncPrint = val => {
return setTimeout(() => {
console.log('user input: ', val)
}, 1000)
}
watchEffect(
onInvalidate => {
//用户输入的时间间隔小于1秒,都会立刻清除掉定时,不输入结果。正因为这个,实现了用户防抖的功能,只在用户输入时间间隔大于1秒,才做打印
const timer = asyncPrint(keyword.value)
onInvalidate(() => clearTimeout(timer))
console.log('keyword change: ', keyword.value)
},
{
flush: 'post' // 默认'post',同步'sync','pre'组件更新之前
}
)
return {
keyword
}
}
}
// 实现对用户输入“防抖”效果
</script>
watch
watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
watch()接收的第一个参数被称作"数据源",它可以是:
- 一个返回任意值的getter函数
- 一个包装对象(可以是ref也可以是reactive包装的对象)
- 一个包含上述两种数据源的数组
第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发:
-
侦听单个数据源
const state = reactive({count: 1}); //侦听一个getter watch(()=>state.count,(newCount,oldCount)=>{ console.log('newCount:',newCount); console.log('oldCount:',oldCount); }) //侦听一个ref const num = ref(0); watch(num,(newNum,oldNum)=>{ console.log('newNum:',newNum); console.log('oldNum:',oldNum); }) -
侦听多个数据源(数组)
const state = reactive({count: 1}); const num = ref(0); // 监听一个数组 watch([()=>state.count,num],([newCount,newNum],[oldCount,oldNum])=>{ console.log('new:',newCount,newNum); console.log('old:',oldCount,oldNum); })同时,watch和watchEffect在停止侦听,清除副作用(相应地onInvalidate会作为回调的第三个参数传入)等方面行为一致。
<template> <div> <input type="text" v-model="keyword"> </div> </template> <script> import { ref, watch } from 'vue' export default { setup() { const keyword = ref('') const asyncPrint = val => { return setTimeout(() => { console.log('user input: ', val) }) } watch( keyword, (newVal, oldVal, onCleanUp) => { const timer = asyncPrint(keyword) onCleanUp(() => clearTimeout(timer)) }, { lazy: true // 默认未false,即初始监听回调函数执行了 } ) return { keyword } } } </script>
生命周期钩子
与2.x版本生命周期相对应的组合式API
beforeCreate-> 使用setup()created-> 使用setup()beforeMount->onBeforeMountmounted->onMountedbeforeUpdate->onBeforeUpdateupdated->onUpdatedbeforeDestroy->onBeforeUnmountdestroyed->onUnmountederrorCaptured->onErrorCaptured
新建测试组件/components/Test.vue
<template>
<div id="test">
<h3>{{a}}</h3>
<button @click="handleClick">更改</button>
</div>
</template>
<script>
import {
ref,
onMounted,
onBeforeMount,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from "vue";
export default {
// 初始化数据阶段的生命周期,介于beforeCreate和created之间
setup() {
const a = ref(0);
console.log("👌");
function handleClick() {
a.value += 1;
}
onBeforeMount(() => {
console.log("组件挂载之前");
});
onMounted(() => {
console.log("DOM挂载完成");
});
onBeforeUpdate(() => {
console.log("DOM更新之前", document.getElementById("test").innerHTML);
});
onUpdated(() => {
console.log("DOM更新完成", document.getElementById("test").innerHTML);
});
onBeforeUnmount(() => {
console.log("实例卸载之前");
});
onUnmounted(() => {
console.log("实例卸载之后");
});
return { a, handleClick };
}
};
</script>
按照官方上说的那样,你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
依赖注入
provide和inject提供依赖注入,功能类似2.x的provide/inject。两者都只能在当前组件的setup()中调用
App.vueprovide数据源
<template>
<div>
<Article></Article>
</div>
</template>
<script>
import {
ref,
provide
} from "vue";
import Article from "./components/Article";
export default {
setup() {
const articleList = ref([
{ id: 1, title: "Vue3.0学习", author: "小马哥" },
{ id: 2, title: "componsition api", author: "尤大大" },
{ id: 3, title: "Vue-router最新", author: "vue官方" }
]);
/*
provide 函数允许你通过两个参数定义 property:
property 的 name (<String> 类型)
property 的 value
*/
provide("list",articleList);
return {
articleList
};
},
components: {
Article
}
};
</script>
Article.vue注入数据
<template>
<div>
{{articleList[0].title}}
</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
const articleList = inject('list',[]);
return {articleList};
},
};
</script>
模板引用refs
当使用组合式API时,reactive refs和template refs的概念已经是统一了。为了获得对模板内元素或者组件实例的引用,可以直接在setup()中声明一个ref并返回它
<template>
<div>
<div ref='wrap'>hello vue3.0</div>
<Article ref='articleComp'></Article>
</div>
</template>
<script>
import {
ref,
onMounted,
provide
} from "vue";
import Article from "./components/Article";
export default {
setup() {
const isShow = ref(true);
const wrap = ref(null);
const articleComp = ref(null);
const articleList = ref([
{ id: 1, title: "Vue3.0学习", author: "小马哥" },
{ id: 2, title: "componsition api", author: "尤大大" },
{ id: 3, title: "Vue-router最新", author: "vue官方" }
]);
/*
provide 函数允许你通过两个参数定义 property:
property 的 name (<String> 类型)
property 的 value
*/
provide("list", articleList);
onMounted(() => {
console.log(wrap.value); //获取div元素
console.log(articleComp.value); //获取的article组件实例对象
});
return {
articleList,
wrap,
articleComp
};
},
components: {
Article
}
};
</script>
<style scoped>
</style>
效果图:
重大改变
Fragments
Vue3.0组件中可以允许有多个根组件,避免了多个没必要的div渲染
<template>
<div>头部</div>
<div>内容</div>
</template>
这样子做的好处:
- 少了很多没有意义的div
- 可以实现平级递归,对实现tree组件有很大帮助
emits
- emits 可以是数组或对象
- 触发自定义事件
- 如果emits是数组,则允许我们配置和事件验证。验证函数应返回布尔值,以表示事件参数是否有效。
Emits.vue
<template>
<div>
<button @click="$emit('submit',{username:'xiaomage',password:'123'})">自定义事件</button>
</div>
</template>
<script>
export default {
// emits:['submit'],//可以是数组
emits: {
submit: payload => {
if(payload.username && payload.password){
return true;
}else{
console.warn('无效的payload,请检查submit事件');
return false
}
}
},
setup() {
return {};
}
};
</script>
<style scoped>
</style>
App.vue
<Emits @submit="submitHandle"></Emits>
<script>
import Emits from "./components/Emits";
export default{
components:{
Emits
},
setup(){
function submitHandle(payload) {
console.warn("自定义事件触发",payload);
}
return {
}
}
}
</script>
效果展示:
全局Vue API更改为应用程序实例
上面已经讲过了,不做一一赘述了。
API可做Tree shakable优化
在vue2.0有不少的全局api是作为静态函数直接挂在在Vue构造函数上的,你应该手动操作过DOM,会遇到如下模式。如果我们未是在代码中用过它们,就会形成我们所谓的"死代码",这类全局api造成的"死代码"无法使用webapck的tree-shaking进行'死代码消除'。
import Vue from 'vue'
Vue.nextTick(()=>{
//一些和DOM相关的东西
})
因此,vue3.0做了相应的改变,将它们抽离成为独立的函数,这样打包工具的摇树优化可以将这些"死代码"排除掉。全局 API 现在只能作为 ES 模块构建的命名导出进行访问。例如,我们之前的片段现在应该如下所示
import {nextTick} from 'vue'
nextTick(()=>{
//一些和DOM相关的东西
})
受影响的API
Vue2.x中这些全局API受此更改的影响:
- Vue.nextTick
- Vue.observable(用Vue.reactive替换)
- Vue.version
- Vue.compile(仅完全构建时)
- Vue.set(仅兼容版本)
- Vue.delete(仅兼容版本)
TreeShaking.vue
<template>
<div >
<hr />摇树优化,把没引入的不必要的代码进行优化
<div id='name'>小马哥</div>
<h3 ref='myMsg'>{{msg}}</h3>
<button @click="changeMsg('hai!')">改变</button>
</div>
</template>
<script>
import { ref, nextTick } from "vue";
export default {
setup() {
const msg = ref("hello!");
const myMsg = ref(null);
async function changeMsg(newV) {
msg.value = newV;
// console.log(myMsg.value.innerText); //直接获取DOM还是以前的
// nextTick返回了promise对象
await nextTick();
console.log(myMsg.value.innerText);
}
return {
msg,
myMsg,
changeMsg
};
}
};
</script>
组件上v-model用法
在 Vue 2.0 发布后,开发者使用 v-model 指令必须使用为 value 的 prop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind.sync。此外,由于v-model 和 value 之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。
在 Vue 2.2 中,我们引入了 model 组件选项,允许组件自定义用于 v-model 的 prop 和事件。但是,这仍然只允许在组件上使用一个 model。
在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model 指令时的混淆并且在使用 v-model 指令时可以更加灵活。
2.x语法
在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 和 input 事件:
<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
如果要将属性或事件名称更改为其他名称,则需要在 ChildComponent 组件中添加 model 选项:
<!-- ParentComponent.vue -->
<ChildComponent v-model="pageTitle" />
// ChildComponent.vue
export default {
model: {
prop: 'title',
event: 'change'
},
props: {
// 这将允许 `value` 属性用于其他用途
value: String,
// 使用 `title` 代替 `value` 作为 model 的 prop
title: {
type: String,
default: 'Default title'
}
}
}
所以,在这个例子中 v-model 的简写如下:
<ChildComponent :title="pageTitle" @change="pageTitle = $event" />
使用 v-bind.sync
在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 v-model 绑定 prop 的情况)。为此,我们建议使用 update:myPropName 抛出事件。例如,对于在上一个示例中带有 title prop 的 ChildComponent,我们可以通过下面的方式将分配新 value 的意图传达给父级:
this.$emit('update:title', newValue)
如果需要的话,父级可以监听该事件并更新本地 data property。例如:
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
为了方便起见,我们可以使用 .sync 修饰符来缩写,如下所示:
<ChildComponent :title.sync="pageTitle" />
3.x语法
在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:
<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
渲染函数API改变
h现在全局导入,而不是作为参数传递给渲染函数- 渲染函数参数更为在有状态组件和函数组件之间更加一致
- vnode现在是一个有扁平的prop结构
render函数将自动接收h函数(它是createElement的别名)作为参数
//vue2.x
export default{
render(h){
return h('div')
}
}
//vue3 渲染
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
举个例子:
<template>
<div>
<RenderComp v-model='title'>
<template v-slot:default>
<!-- 默认插槽 -->
头部
</template>
<template v-slot:content>
<!-- 具名插槽 -->
内容
</template>
</RenderComp>
</div>
</template>
<script>
import {
ref,
h
} from "vue";
export default {
components: {
RenderComp: {
props: {
modelValue: {
type: String,
default: ''
},
},
setup(props,{attrs,slots,emit}) {
// 以前得通过$scopedSlots获取对应的插槽
console.log(slots.default()); //获取默认插槽
console.log(slots.content()); //获取名字为content的插槽
function changeTitle(newV) {
emit('update:modelValue','哈哈哈');
}
return () => h("div", {}, [h("div", {
onClick:changeTitle,
},[
`渲染函数api:${props.modelValue}`,
slots.default(),
slots.content()
])]);
}
}
},
setup(props) {
const title = ref("双向数据绑定");
return {
title
};
}
};
</script>
同时,演示了$scopedSlotsproperty已删除,所有插槽都通过$slots作为函数暴露
使用普通函数创建功能组件
- 在 3.x 中,功能性组件 2.x 的性能提升可以忽略不计,因此我们建议只使用有状态的组件
- 功能组件只能使用接收
props和context的普通函数创建 (即:slots,attrs,emit)。 - 重大变更:
functionalattribute 在单文件组件 (SFC)<template>已被移除 - 重大变更:
{ functional: true }选项在通过函数创建组件已被移除
在vue2.0中,功能组件有两个主要用途:
- 性能优化提高,因为它们的初始化速度比有状态组件快
- 可以返回多个根节点
然而,在 Vue 3 中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组件现在还包括返回多个根节点的能力。
因此,功能组件剩下的唯一用例就是简单组件,比如创建动态标题的组件。否则,建议你像平常一样使用有状态组件。
总结:非特殊情况下,官网还是建议我们使用有状态的组件
Functional.vue
import { h } from 'vue'
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
<Functional level='3'>动态标题</Functional>
可以传入不同的level 定制不同的h系列标题。
异步组件的更改
- 新
defineAsyncComponent助手方法,它显示定义异步组件 componnet选项命名为loader- 加载程序函数被本身不接受
resolve和reject参数,必须返回一个Promise
2.x
以前,异步组件是通过将组件定义为返回 promise 的函数来创建的,例如:
const asyncPage = () => import('./NextPage.vue')
对于带有选项的更高阶组件语法:
const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
3.x
在vue3中,由于功能组件被定义为纯函数,因为需要通过将异步组件定义包装在新的defineAsyncComponent助手来显式定义组件
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 不带选项的异步组件
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))
// 带选项的异步组件
const asyncPageWithOptions = defineAsyncComponent({
loader: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
自定义指令
API 已重命名,以便更好地与组件生命周期保持一致
- bind → beforeMount
- inserted → mounted
- beforeUpdate:新的!这是在元素本身更新之前调用的,很像组件生命周期钩子
- update → 移除!有太多的相似之处要更新,所以这是多余的,请改用
updated - componentUpdated → updated
- **beforeUnmount **
新的与组件生命周期钩子类似,它将在卸载元素之前调用。 - unbind -> unmounted
举个例子:
main.js
const app = createApp(App);
// 创建自定义指令
app.directive('highlight',{
// 指令 也拥有一组生命周期钩子
// 1.在绑定元素的父组件挂载之前调用
beforeMount(el,binding,vnode){
el.style.background = binding.value;
},
})
App.vue
<p v-highlight="'red'">自定义指令</p>
动画transion改变
- v-enter->v-enter-from
- v-leave->v-leave-from
vue2.x版本中
移除API
2.x,支持keyCodes作为修改v-on方法的方法
<!-- 键码版本 -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
vue3.x
在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称。
<!-- Vue 3 在 v-on 上使用 按键修饰符 -->
<input v-on:keyup.delete="confirmDelete" />
因此,这意味着 config.keyCodes 现在也已弃用,不再受支持。
$on,$off 和 $once 实例方法已被移除,应用实例不再实现事件触发接口。
Filters 已从 Vue 3.0 中删除,不再受支持。相反,我们建议用方法调用或计算属性替换它们。