Vue3的学习记录
关注公众号 前端开发博客,回复“加群”
加入我们一起学习,天天进步
作者:Bug_Lucas
链接:https://juejin.cn/post/6903797748106330126
这篇文章是我在 vue官网 以及一些其他地方学习v3的一些学习记录与心得,如果有理解不到地方欢迎大家指正。
data的变化
在v3版本中将data的返回值进行了标准化,只接受返回Object的Function, 而在v2版本中同时支持返回Object和返回Object的Function。
mixin合并的变化
v3版本中关于Object的合并是覆盖,而不是合并。
const mixinObject = {
data(){
return {
user:{
id:1,
name:'jack'
}
}
}
}
const CompA = {
mixins: [mixinObject],
data() {
return {
user: {
id: 2
}
}
}
}
// 结果是
user: {
id: 2
}
移除了 off , $once方法
在v2版本中,通常使用off来实现全局事件总线,使用$once绑定监听一个自定义事件,其实这些在实际项目中用到的也不会特别多,在v3版本移除后,可以通过一些外部库来实现相关需求。例如 mitt
删除了过滤器
v3版本中删除了过滤器, {{ calcNum | filter }}
将不在支持,官方建议使用computed
属性替换过滤器(well done ~)。
片段
在v3版本中支持了可以有多个根节点的组件,可以减少节点层级深度。但也希望开发者能够明确语义。
<template>
<header></header>
<main></main>
<footer></footer>
</template>
函数式组件
在将v3版的变化之前我们先来回顾一下v2版本的函数式组件,它有两种创建方式:functional
attribute 和 { functional : true }
选项,代码分别如下
// attribute 方式
<template functional>
<img :src="src" />
</template>
<script>
...
</script>
// 选项方式
export default = {
functional:true,
props:{
...
},
render:function(createElement,context){
...
/**
context传递了一些参数:
props,slots,parent,listeners,injections
**/
}
}
v2版本中组件有两种组件类型:有状态组件和函数式组件(无状态组件),相对于有状态组件,函数式组件不需要被实例化,无状态,没有生命周期钩子,它们的渲染速度远高于有状态组件。往往通常被适用于功能单一的静态展示组件从而优化性能。除此之外,函数式组件还有返回多个根节点的能力。
在v3版本中,有状态组件和函数式组件之间的性能差异已经大大减少,在大部分场景下几乎可以忽略不计。所以函数式组件唯一的优势就在一返回多节点的能力,但这种通常运用比较少,且组件往往比较简单,具体语法糖如下:
// 函数创建
import { h } from 'vue'
const DynamicHeading = (props, context) => {
// context是一个包含 attrs,slots,emit的对象
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
// 单文件组件(SFC)
<template>
<component
v-bind:is="`h${$props.level}`"
v-bind="$attrs"
/>
</template>
<script>
export default {
props: ['level']
}
</script>
全局API调整
v3版本中新增了一个新的全局APIcreateApp
,通过ES模块的方式引入。调用createApp
返回一个应用实例,该应用实例暴露出来全局API,这是Vue3的新概念,主要解决了不同"app"之间能够共享资源(配置,全局组件,指令等等)。我们来对比一下v3和v2分别创建应用的改变。
// v2 创建
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
// v3 创建
import { createApp } from 'vue'
import App from './App.vue'
var app = createApp(App);
app.mount('#app')
在"app"之间共享配置的一种方式是工厂模式:
import { createApp } from 'vue'
import App1 from './App1.vue'
import App2 from './App2.vue'
const createMyApp = options => {
const app = createApp(options)
app.directive('focus' /* ... */)
return app
}
createMyApp(App1).mount('#foo')
createMyApp(App2).mount('#bar')
v3将可以全局改变Vue行为的API从原来的Vue构造函数上转移到了实例上了。列表如下
除此之外,还有一些全局API和内部组件都做了重构,考虑到tree-shaking,只能通过ES模块的方式导入,意味着当你的应用程序中没用到的组件和接口都不会被打包。受影响的API列表如下:
Vue.nextTick Vue.observable (用 Vue.reactive 替换) Vue.version Vue.compile (仅全构建) Vue.set (仅兼容构建) Vue.delete (仅兼容构建)
// 错误使用
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和DOM有关的东西
})
// 正确使用
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
组合式API
重头戏!!!
这是v3中的新概念,vue给我们提供了一些新的api,这些新的api在一个地方使用,而这个地方就是vue给我们新提供了一个从组件初始化到销毁的过程中我们能够做一些事情的地方,我将之理解为钩子函数——Setup
,在Setup里,你可以做原本你能在vue组件里面能做的所有事情,比如你在created
,mounted
,computed
,watch
,methods
里面做的所有事情,都能在Setup
里面完成,那要怎么才能做到呢,就是要通过vue提供的那些新的API。那这个东西主要解决什么事情呢?我们都知道组件的作用就是对功能的提取以及代码的复用。这使得我们的程序在灵活性和可维护性上能走的更远,但是这还不够,当一个组件内部的逻辑很复杂的时候,我们将逻辑分别散落在 created
,mounted
,methods
,computed
,watch
里面,然后整个文件代码长达好几百行。这对于没有编写这个组件的人来说,尝试去理解这些逻辑代码无疑是最头疼的事情。而Setup
正是解决了这个事情,将所有的逻辑都集中起来在Setup
中处理。从今以后,当你想制造一辆汽车的时候,你再也不用去全世界进口各种零件,你在Setup
工厂中就能完成。让我们来见识一下Setup
和那些新的API
的使用以及作用(听说这种东西才被称之为干货???):
Setup
如果你听懂了我上面所说的,那我开局这么写你应该也能理解了:
<template>
<div class="demo">
</div>
</template>
<script>
export default {
name:"Demo",
data(){
return {}
},
setup () {
console.log('run');
}
}
</script>
// 当运行起来打印了run
Setup
可以返回一个对象,你可以在组件的其他地方访问这个对象中的属性。
“注意:在执行
”Setup
的时候尚未创建组件实例,所以在Setup
中没有this
。不过它提供了两个接收参数——props
和context
。在Setup
中无法访问组件中其他的任何属性。
// 调用Demo组件
<Demo :name="'jac" a="1" b="2"></Demo>
// Demo 组件
<template>
<div class="demo">
</div>
</template>
<script>
export default {
name:"Demo",
props:{
name:String,
},
data(){
return {}
},
setup (props,context) {
console.log('props',props); // {name:jac}
console.log('attrs', context.attrs); // {a:1,b:2}
console.log('slots', context.slots); // {}
console.log('emit', context.emit); // function
let shareProp = 'hallo,v3';
// 返回两个属性 shareProp, changeProp
return {
shareProp
}
},
mounted() {
this.shareProp = 'changed';
},
}
</script>
我们会发现试图并没有更新,我们发现setup
返回的对象不是响应式的,响应式我们应该不陌生,在data()选项中的property都是响应式的,那是应为Vue在组件初始化的过程中就已经对data()中的property创建了依赖关系。所以当property发生变化时,视图即会自动更新,这就是响应式。那怎么让它变成响应式的呢?
ref & reactive
我们可以通过ref
,reactive
创建响应式状态,
我们使用ref
可以创建基础数据类型和复杂数据类型的响应式状态,使用reactive
只能创建复杂数据类型的响应式状态,如果使用创建数据类型不正确,控制台会给出对应的警告value cannot be made reactive: **
。那ref
和reactive
的区别到底在哪里呢?
const refNum = ref(10);
const refObj = ref({
name:'jac',
age:20
});
const reactiveNum = reactive(10);
const reactiveObj = reactive({
name: 'jac',
age: 20
})
console.log('refNum',refNum);
console.log('refObj',refObj);
console.log('reactiveNum', reactiveNum);
console.log('reactiveObj', reactiveObj);
结果如下:
当ref
创建的是复杂数据类型的时候内部其实也是用reactive创建的。所以ref
也是可以创建复杂数据类型的响应状态的,只是在setup
中写法会有所不同。
setup(){
const refObj = ref({
name:'jac',
age:20
});
const reactiveObj = reactive({
name: 'jac',
age: 20
})
// ref 方式的更新
const changeRefObj = () => {
refObj.value.name="mac"
}
// reactive 方式的更新
const changeReactiveObj = () => {
reactiveObj.name = 'mac'
}
return {
...
}
}
“注意:通过
”ref
对值进行了包裹,在Setup
中你需要使用变量.value的方式进行访问和设置值,从Setup
中暴露出去的对象你可以直接通过this.变量访问。
<template>
...
</template>
<script>
import { ref,reactive } from 'vue';
export default {
...
setup (props,context) {
...
let shareProp = ref('hallo,v2');
let info = reactive({name:'jac'});
const changeProp = ()=>{
shareProp.value = 'hallow,v3';
}
return {
shareProp,
...
}
},
mounted() {
console.log(this.shareProp)
},
}
</script>
小伙伴可以根据自己的编码习惯选择运用。
toRef & toRefs
有时候我们想通过解构的方式从一个复杂的响应式变量中剥离出一些变量时,我们的代码可能是这样的:
<template>
<button @click="changeObj">my name is {{info.name}}</button>
</template>
<script>
import { ref, provide, reactive } from 'vue';
export default {
name: 'Demo',
setup(){
const info = reactive({name:'jac',sex:'男',age:18});
return {
info
}
},
methods:{
changeObj(){
let { name,sex } = this.info;
name = 'make'
// 视图不会更新
}
}
}
</script>
这样会使我们解构出来的两个property失去响应性,这个时候我们需要使用toRef
和toRefs
从中解构出来,toRef
用于解构出单个property的响应式变量,toRefs
是将源对象中所有property都创建响应式变量,在通过解构的方式创建我们对应的变量。
<template>
<button @click="changeObj">my name is {{info.name}}</button>
</template>
<script>
import { ref, provide, reactive, toRef, toRefs} from 'vue';
export default {
name: 'Demo',
setup(){
const info = reactive({name:'jac',sex:'男',age:18});
return {
info
}
},
methods:{
changeObj(){
// toRef
// let name = toRef(this.info,'name'); // arg1: 源对象, arg2: 属性名称
// name.value = 'make'
// toRefs
// let { name } = toRefs(this.info); // arg1: 源对象
// name.value = 'make'
// 视图成功刷新
}
}
}
</script>
watch & computed & 注册生命周期钩子
在Setup
中我们还能watch
属性,创建独立的computed
,还可以注册各种生命周期钩子,由于Setup
执行的阶段是围绕beforeCreate
和created
和进行的,所以原本在这两个生命周期中做的事情都能够放在Setup
中处理。
<template>
...
</template>
<script>
import { ref, toRefs, watch, computed, onMounted } from 'vue';
export default {
name:"Demo",
props:{
name:String,
},
...
setup (props,context) {
console.log('我首先执行');
// 通过toRefs方法创建props的响应式变量
const { name: listenName } = toRefs(props);
const count = ref(0);
const changCount = ()=>{
count.value++;
}
const totalCount = computed(() => {
return `总计数:${count.value}`
})
// watch 会监听listenName变量,当listenName变量改变的时候会触发回调方法
watch(listenName,(newValue, oldValue)=>{
console.log(listenName.value);
})
onMounted(()=>{
console.log('onMounted也执行了,结果输出如下');
console.log(count.value);
changCount();
console.log(totalCount.value)
})
return {
count,
changCount,
totalCount,
}
},
created() {
console.log('created执行了');
},
mounted() {
console.log('mounted执行了');
},
}
</script>
// 输出结果如下:
我首先执行
created执行了
onMounted也执行了,结果输出如下
0
总计数:1
mounted执行了
看的出来,Setup
中注册的生命周期钩子函数要比外面注册的钩子函数先执行!
provide & inject
provide
和inject
通常情况下我们在业务场景中使用不多,但在写插件或组件库的时候还是有用的,provide
和inject
用于在"祖"组件提供属性,在子组件中注入使用。在setup中怎么使用呢?
// demo组件中提供
<template>
<Test />
</template>
<script>
import { provide } from 'vue';
export default {
name:"Demo",
setup (props,context) {
provide('name','jac')
},
}
</script>
// test组件中注入使用
<template>
...
</template>
<script>
import { inject } from 'vue';
export default {
name:"Test",
setup (props,context) {
console.log(inject('name')); // jac
},
}
</script>
以上是setup
的基本应用,需要好好的感受它还需要在多种场景下去实际运用,才能更好的理解它棒在哪里,比如下面这段代码:
// a.js
import { ref, onMounted } from 'vue'
export default function doSomething(refName,fn){
const a = ref(0);
const b = ref(0);
// ...,
onMounted(() => {
fn();
})
return {
a,
b
}
}
//b.js
import doSomething from './a.js';
setup(){
const {a,b} = doSomething('box',()=>{
console.log('执行');
})
}
我随便举的一个例子,假设你有一段逻辑在多个页面都需要用到,那我们可以将这段逻辑抽离出来,这样让我们的代码更精简,我们不仅可以让组件复用,在也能更大幅度的提高一些业务代码的复用,还能集中处理业务代码,我相信对于我们的开发体验还是代码质量,都大有裨益。
v-model
v3中改变了v-model默认属性值和触发方法,value
=>modelVale
,input
=>update
。在自定义的组件中,允许我们同时设置多个v-model。
// Demo.vue
export default {
name: "Demo",
props: {
title:String,
label:String
},
methods: {
titleEmit(value) {
this.$emit('update:title', value)
},
labelEmit(value) {
this.$emit('update:label', value)
}
},
}
// 调用Demo.vue
<Demo v-model:title="" v-model:label="" />
Typescript的支持
v3推荐使用Typescript
,Typescript
不仅支持ES6
的特性,还具备类型推导,可以帮助我们在开发的过程中就避免很多类型错误,在前端越来越复杂的现在,Typescript
能够支撑应用走的更远,在可维护性和扩展性上都更有优秀,拥抱改变。
怎么创建vue3+typescript的应用呢?
1.用vue-cli
yarn global add @vue/cli@next
# OR
npm install -g @vue/cli@next
// 下面按部就班的创建
2.采用vite(个人推荐,简单省事)
yarn create vite-app my-vue-ts --template vue-ts
啪!没啦
这一节我们就从应用层面简单的了解了一下v3的一些改变,没有深挖原理与实现。
v3 一镜999次 卡!
相关文章
最后
转发文章并关注公众号:前端开发博客,回复 1024,领取前端进阶资料
回复「电子书」领取27本精选电子书 回复「加群」加入前端大神交流群,一起学习进步 回复「Vue」获取 Vue 精选文章