「Vue3.X」新特性快速学习,带你迅速上手Vue3!

505 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

一、Vue3 快速上手 

1、vue3 带来了什么

1.1、性能的提升

  • 打包大小减少41%
  • 初次渲染快55%,更新渲染快133%
  • 内存减少54%

2.2、源码的升级

  • 受用Proxy代替defineProperty实现响应式
  • 重写虚拟DOM的实现和Tree-Shaking(去除未使用的JS代码)

2.3更好的支持TypeScript

2.4新的特性

Composition API (组合API)

  • setup 配置
  • ref 和 reactive
  • watch 和 watchEffect
  • provide 和 inject

新的内置组件

  • Fragment
  • Teleport
  • Suspense

其他改变

  • 新的声明周期钩子
  • data选项应始终被声明为一个函数
  • 移除keyCode 支持作为v-on 的修饰符

2、使用vite 创建

  • 什么是vite --- 新一代的前端构建工具
  • 优势如下:
    • 开发环境钟,无需打包,可以快速的冷启动(动态的根据路由来加载对应模块)
    • 轻量快速的热重载(HMR)
    • 真正的按需编译,不再等待整个应用编译完成

使用vite 创建项目

npm init vite-app vue3_test_vite
cd .\vue3_test_vite\
npm install
npm run dev

3、使用vue-cli 创建

vue create 项目名称

主页挂载根vue2的区别

// 引入的不再是Vue构造函数,引入的是createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
// 创建应用实例对象-app (类似于之前Vue2中的vm,但app比vn更轻
const app = createApp(App)
// 挂载
app.mount('#app')


// Vue2 中挂载
// const vm = new Vue({
//   render:h=>h(App)
// })
// vm.$mount("#app")

Vue 3 组件中不需要根标签

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

二、常用的 Composition API

1、setup函数

  • 理解:Vue3 里的新配置,作为一个函数

  • 组件中所用到的:数据、方法等等,均要配置在setup中

  • setup 函数的两种返回值:

    1、一个返回一个对象,对象中的属性、方法,在模板中可以直接使用

    2、返回一个渲染函数,则可以自定义渲染内容

  • 不考虑响应式的初次写法

<template>
  <h1>{{name}}</h1>
  <h2>{{age}}</h2>
  <button @click="sayName">点击查看名字</button>
</template>

<script>
import {h} from 'vue'
export default {
  name: 'App',
  setup() {
    // 数据
    let name = 'zs'
    let age = 18
    // 方法
    function sayName(){
      alert(`这个人的名字${name},年龄是${age}`)
    }
    // 第一种返回一个对象(常用)
      return{
      name,
      age,
      sayName
    }

  // 第二种放回一个(渲染函数) 在使用 h 前需要从vue中导入
    return ()=>h('h1',name)
  }
}
</script>

==注意点:==

  • 尽量不要与Vue2混用
  • setup不能是一个async 函数,因为返回值不再是return的对象,而是promise,模板看不到return 对象中的属性,(但是后期可以搭配异步组件使用)

2、ref 函数

作用:定义一个响应式的数据

在vue3中若是想实现响应式的数据变化,需要从vue中引入一个ref函数

并且在取值的时候需要在后面加上.value,但是在模板上不用加,因为vue帮我们自动加了

<template>
  <h1>{{name}}</h1>
  <h2>{{age}}</h2>
  <button @click="changeName">点我改变名字</button>
</template>

import {ref} from 'vue'
export default {
  name: 'App',
  setup() {
    // 数据
    let name = ref('zs')
    let age = ref(18)

    // 方法
    function changeName(){
      name.value = '李四',
      age.value = 20
    }
    // 第一种返回一个对象(常用)
      return{
      name,
      age,
      changeName
    }

我们打印一下ref获得的属性:ref("name") 获得一个RefImpl ==引用实例==

image-20220125171021571

可以发现实际上vue3实现响应式还是通过definePropety的set,get方法实现的

对于对象的数据 实现的响应式

setup() {
    // 数据
    let name = ref('zs')
    let age = ref(18)
    let obj = ref({
      type:"aaa",
      salay:19999
    })
    
    console.log(obj.value);
    // 打印出的结果为
    //Proxy {type: "aaa", salay: 19999}

==对于对象==它的响应式是ref获得实例后,再用proxy 来代理属性的变化,实际上还是借助了Vue3里的 reactive 函数去实现

3、reactive 函数

作用: 定义一个对象类型的响应式数据

语法: const 代理对象 = reactive (源对象) 接受一个(对象 / 数组) 返回一个代理对象*(Proxy的实例对象)*

import {ref ,reactive} from 'vue'

let name = ref('zs')
let age = ref(18)
let obj = reactive({
      type:"aaa",
      salay:19999
    })

// 取值的时候就不用在后面加.value 了
// 方法
    function changeName(){
      name.value = '李四',
      age.value = 20,
      obj.type = "五六"
    }

4、Vue3.x 的响应式

  • 实现原理:

    • 用过Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写,属性的添加、属性的删除等

    • 通过Reflect (反射) :对被代理对象的属性进行操作

    Proxy(代理)

    let Person = {
        name:'张三',
        age:19
      }
      
      const p = new Proxy(Person,{
        //target 代表源数据Person 的对象
        // propValue  代表要操作的对应属性
        //  newValue 代表要操作的新的值 
    
        get(target,propValue){  // 读取属性时调用
          console.log(`读取Person的属性返回对应值`);
          return target[propValue]
        },
        set(target,propValue,newValue){  //  修改跟增加时掉用
          console.log("Person属性发生了修改跟增加,去页面做对应操作");
          target[propValue] = newValue
        },
        deleteProperty(target,propValue){  //  删除时调用
          console.log("Person属性要被删除");
          return delete target[propValue]
        }
      })
    

    Reflect (反射)

    window.Reflect 是ES6上新增的方法,上面包含了许多Object上的方法

    // 使用object.defineProperty()  一旦报错就容易使js运行不下去
    //使用Reflect  使用对应的方法,它会将defineProperty  的运行结果以布尔值返回,不会影响后面代码执行
    // 相对使用Reflect 反射可以减少try  catch 的使用
    const p = new Proxy(Person,{
        //target 代表源数据Person 的对象
        // propValue  代表要操作的对应属性
        //  newValue 代表要操作的新的值 
    
        get(target,propValue){  // 读取属性时调用
          console.log(`读取Person的属性返回对应值`);
          return Reflect.get(target,propName)
        },
        set(target,propValue,newValue){  //  修改跟增加时掉用
          console.log("Person属性发生了修改跟增加,去页面做对应操作");
          Reflect.set(target,propName,newValue)
        },
        deleteProperty(target,propValue){  //  删除时调用
          console.log("Person属性要被删除");
          return Reflect.deleteProperty(target,propName)
        }
      })
    

5、reactive 和 ref 的对比

  • 从定义数据的角度
    • ref 用来定义:基本类型数据
    • reactive 用来定义:对象(或数组)类型数据
    • 其中:ref 也可以用来定义 对象(或数组)类型数据,它内部会自动通过 ==reactive== 转为代理对象
  • 从原理角度:
    • ref通过 Object.defineProperty() 的 get 与 set 来实现响应式 (数据劫持)
    • reactive 通过使用 Proxy 来实现响应式(数据劫持),并通过 Reflect 操作源对象内部的数据
  • 从使用角度对比:
    • ref 定义的数据: 操作数据都需要 .value
    • reactive 定义的数据 : 操作数据与读取数据 , 均不需要 .value

6、setup的两个注意点

  • setup 执行的时机
    • beforeCreate 之前执行一次,this 是 undefined
  • setup 的参数
    • props:值为对象,包含 : 组件外部传递过来,且组件内部声明接收了的属性。
    • context :上下文对象
      • attrs: 值为对象,包含:组件外部传过来,但没有在 props 配置声明的属性,相当于 ==this.$attrs==
      • slots: 收到的插槽的内容,相当于 ==this.$slots.==
      • emit : 分发自定义事件的函数, 相当于 ==this.$emit==

7、computed 函数与 watch 函数

1、computed 函数

  • 与Vue2.x 中computed配置功能一致

  • 写法

    import {computed} from 'vue'
    
    setup(){
        ...
    	//计算属性——简写
        let fullName = computed(()=>{
            return person.firstName + '-' + person.lastName
        })
        //计算属性——完整 // 能捕获到计算属性的修改
        let fullName = computed({
            get(){
                return person.firstName + '-' + person.lastName
            },
            set(value){
                const nameArr = value.split('-')
                person.firstName = nameArr[0]
                person.lastName = nameArr[1]
            }
        })
    }
    

2、watch 函数

  • 与Vue2.x 中watch 配置功能一致
  • 两个小
    • 监视 reactive 定义的响应式数据时(对象 / 数组),oldValue 无法获取正确的值,强制开启了深度监视 (deep配置有无都一样)都可以获取到深处值
    • 监视 reactive 定义的数据中的某个对象 / 数组时,deep 配置有效
    • 监视 reactive 定义的数据中的具体属性时,oldVaule 能获取正确的值
import { watch } from 'vue'

// 情况一: 监视 ref 定义的响应式数据
// 第一个参数,需要监视的对象,
// 第二个参数,回调的函数里面有新值,跟旧值,两个值
// 第三个参数,watch的配置项。可以配置immediate(页面加载时第一次是否执行),deep(深层次属性是否监听)
watch( sum , (newValue,oldValue)=>{
    console.log('sum变化了',newValue,oldValue)
},{immediate:true})

//情况二:监视多个ref定义的响应式数据
watch([sum,msg],(newValue,oldValue)=>{
	console.log('sum或msg变化了',newValue,oldValue)
}) 

/* 情况三:监视reactive定义的响应式数据
			若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
			若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 
*/
watch(person,(newValue,oldValue)=>{
	console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep配置不再奏效

//情况四:监视reactive定义的响应式数据中的某个属性
//  oldVaulue  可以正确获取
watch(()=>person.job,(newValue,oldValue)=>{
	console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true}) 

//情况五:监视reactive定义的响应式数据中的某些属性 
// oldValue 可以正常获取
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
	console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})

//特殊情况  监视对象类型的需要开启deep,此时的obj 没有被Proxy代理所以需要开启
watch(()=>person.obj,(newValue,oldValue)=>{
    console.log('person的job变化了',newValue,oldValue)
},{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效

7.1 watchEffect 函数

  • watch的套路是:既要指明监视的属性,也要指明监视的回调。
  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
  • watchEffect有点像computed:
    • 刚加载初期就会回调一次
    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
//页面加载初期会回调一次函数
watchEffect(()=>{
    const x1 = sum.value
    const x2 = person.age
    console.log('watchEffect配置的回调执行了')
})

8、Vue 3 生命周期

lifecycle_2

  • Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:

    • beforeDestroy改名为 beforeUnmount
    • destroyed改名为 unmounted
  • Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:

  • 这些可以写进setup函数里(使用前需引用)

    setup(){
     `beforeCreate`===>`setup()`
     `created`=======>`setup()`
     `beforeMount` ===>`onBeforeMount`
     `mounted`=======>`onMounted`
    `beforeUpdate`===>`onBeforeUpdate`
     `updated` =======>`onUpdated`
    `beforeUnmount` ==>`onBeforeUnmount`
     `unmounted` =====>`onUnmounted`
    }
    

9、自定义hook函数

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。
  • 类似于vue2.x中的mixin。
  • 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂

一个简单的获取鼠标坐标hook

//  在hooks 问价夹下建立一个 usePoint.js 将鼠标点击获取到的值返回出去,给外界调这个方法时就会得到鼠标当前的值
import {reactive,onMounted,onUnmounted} from 'vue'

export default function usePoint(){
  let point = reactive({
    x:0,
    y:0
  })
  function getPoint(event){
    point.x = event.pageX
    point.y = event.pageY
    console.log(event.pageX,event.pageY);
  }
  
  onMounted(()=>{
    window.addEventListener('click',getPoint)
  })

  onUnmounted(()=>{
    window.removeEventListener('click',getPoint)
  })
  return point
}

// 外界直接引入就可以获得里面返回的值了
<template>
  <h1>我是test</h1>
  <h1>鼠标当前坐标:x:{{ point.x }}y:{{ point.y }}</h1>
</template>
<script>
import usePoint from "../hooks/usePoint";
export default {
  setup() {
    let point = usePoint(); //  在这边直接使用函数即可获得其返回值
    return {
      point,
    };
  },
};
</script>

10、toRef 和 toRefs

  • 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
    • 使return 出去的属性可以更简洁
  • 语法:const name = toRef(person,'name') 把person 对象里的name引用回原数据里的name
  • 应用: 要将响应式对象中的某个属性单独提供给外部使用时。
  • 扩展:toRefstoRef功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person)
<template>
	<h4>{{person}}</h4>
	<h2>姓名:{{name}}</h2>
	<h2>年龄:{{age}}</h2>
	<h2>薪资:{{job.j1.salary}}K</h2>
	<button @click="name+='~'">修改姓名</button>
	<button @click="age++">增长年龄</button>
	<button @click="job.j1.salary++">涨薪</button>
</template>
<script>
	import {ref,reactive,toRef,toRefs} from 'vue'
	export default {
		name: 'Demo',
		setup(){
			//数据
			let person = reactive({
				name:'张三',
				age:18,
				job:{
					j1:{
						salary:20
					}
				}
			})
			// const name1 = person.name
			// console.log('%%%',name1)

			// const name2 = toRef(person,'name')
			// console.log('####',name2)

			const x = toRefs(person)
			console.log('******',x)

			//返回一个对象(常用)
			return {
				person,
				// name:toRef(person,'name'),
				// age:toRef(person,'age'),
				// salary:toRef(person.job.j1,'salary'),
				...toRefs(person)
			}
		}
	}
</script>

三、其它 Composition API

1、shallowReactive 和 shallowRef

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)。
  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。
  • 什么时候使用?(性能有所优化)
    • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换(x={y:newValue}) ===> shallowRef。

2.readonly 与 shallowReadonly

  • readonly: 让一个响应式数据变为只读的(深只读)。
  • shallowReadonly:让一个响应式数据变为只读的(浅只读)。(只有第一层不能改)
  • 应用场景: 不希望数据被修改时。

3.toRaw 与 markRaw

  • toRaw:
    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
  • markRaw:
    • 作用:标记一个对象,使其永远不会再成为响应式对象。
    • 应用场景:
      1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。
      2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

4.customRef

  • 作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
  • 实现防抖效果:
<template>
	<input type="text" v-model="keyword">
	<h3>{{keyword}}</h3>
</template>

<script>
	import {ref,customRef} from 'vue'
	export default {
		name:'Demo',
		setup(){
			// let keyword = ref('hello') //使用Vue准备好的内置ref
			//自定义一个myRef
			function myRef(value,delay){
				let timer
				//通过customRef去实现自定义
				return customRef((track,trigger)=>{
					return{
						get(){
							track() //告诉Vue这个value值是需要被“追踪”的
							return value
						},
						set(newValue){
							clearTimeout(timer)
							timer = setTimeout(()=>{
								value = newValue
								trigger() //告诉Vue去更新界面
							},delay)
						}
					}
				})
			}
			let keyword = myRef('hello',500) //使用程序员自定义的ref
			return {
				keyword
			}
		}
	}
</script>

5.provide 与 inject

img

  • 作用:实现祖与后代组件间通信

  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据

  • 具体写法:

    1. 祖组件中:

      setup(){
          ......
          let car = reactive({name:'奔驰',price:'40万'})
          provide('car',car)
          ......
      }
      
    2. 后代组件中:

      setup(props,context){
          ......
          const car = inject('car')
          return {car}
          ......
      }
      

6.响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

四、Composition API 的优势

1.Options API 存在的问题

使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。

2.Composition API 的优势

我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。

五、新的组件

1.Fragment

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级, 减小内存占用

2.Teleport

  • 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。

    <!--可以更好的相对页面布局 -->
    <teleport to="移动位置">
    	<div v-if="isShow" class="mask">
    		<div class="dialog">
    			<h3>我是一个弹窗</h3>
    			<button @click="isShow = false">关闭弹窗</button>
    		</div>
    	</div>
    </teleport>
    

3.Suspense

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验

  • 使用步骤:

    • 异步引入组件

      import {defineAsyncComponent} from 'vue'
      const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
      
    • 使用Suspense包裹组件,并配置好defaultfallback

      <template>
          <div class="app">
              <h3>我是App组件</h3>
              <Suspense>
                  <template v-slot:default>
                      <Child/>
                  </template>
                  <template v-slot:fallback>
                      <h3>加载中.....</h3>
                  </template>
              </Suspense>
          </div>
      </template>
      

六、其他

1.全局API的转移

  • Vue 2.x 有许多全局 API 和配置。

    • 例如:注册全局组件、注册全局指令等。

      //注册全局组件
      Vue.component('MyButton', {
        data: () => ({
          count: 0
        }),
        template: '<button @click="count++">Clicked {{ count }} times.</button>'
      })
      
      //注册全局指令
      Vue.directive('focus', {
        inserted: el => el.focus()
      }
      
  • Vue3.0中对这些API做出了调整:

    • 将全局的API,即:Vue.xxx调整到应用实例(app)上:

      2.x 全局 API(Vue3.x 实例 API (app)
      Vue.config.xxxxapp.config.xxxx
      Vue.config.productionTip移除
      Vue.directiveapp.directive
      Vue.mixinapp.mixin
      Vue.useapp.use
      Vue.prototypeapp.config.globalProperties

2.其他改变

  • data选项应始终被声明为一个函数。

  • 过度类名的更改:

    • Vue2.x写法

      .v-enter,
      .v-leave-to {
        opacity: 0;
      }
      .v-leave,
      .v-enter-to {
        opacity: 1;
      }
      
    • Vue3.x写法

      .v-enter-from,
      .v-leave-to {
        opacity: 0;
      }
      
      .v-leave-from,
      .v-enter-to {
        opacity: 1;
      }
      
  • 移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes

  • 移除v-on.native修饰符

    • 父组件中绑定事件

      <my-component
        v-on:close="handleComponentEvent"
        v-on:click="handleNativeClickEvent"
      />
      
    • 子组件中声明自定义事件

      <script>
        export default {
          emits: ['close']
        }
      </script>
      
  • 移除过滤器(filter)

    过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

  • ......

组件上的 v-model

当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。

父组件传值:

<!-- 父组件传值 -->
<my-counter v-model:number="count"></my-counter>

子组件在 emits 节点声明自定义事件,格式为 update:xxx ,调用 $emit 触发自定义事件:

export default {
  props: ['number'],
  emits: ['update:number'],
  methods: {
    add() {
      this.$emit('update:number', this.number++)
    },
  },
}

注意,在 vue3props 属性同样是只读的,上面 this.number++ 并没有修改 number 的值。

其实通过 v-bind 传值和监听自定义事件的方式能实现和 v-model 相同的效果。

EventBus

借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。

安装 mitt 依赖包:

npm install mitt@2.1.0

创建公共 eventBus 模块:

import mitt from 'mitt'

// 创建 EventBus 实例对象
const bus = mitt()

export default bus

数据接收方调用 bus.on() 监听自定义事件:

import bus from './eventBus.js'

export default {
  data() {
    return { count: 0 }
  },
  created() {
    bus.on('countChange', (count) => {
      this.count = count
    })
  },
}

数据接发送方调用 bus.emit() 触发事件:

import bus from './eventBus.js'

export default {
  data() {
    return { cout: 0 }
  },
  methods: {
    addCount() {
      this.count++
      bus.emit('countChange', this.count)
    },
  },
}

vue 3.x 全局配置 axios

实际项目开发中,几乎每个组件中都会使用 axios 发起数据请求。此时会遇到如下两个问题:

  • 每个组件中都需要导入 axios(代码臃肿)
  • 每次发请求都需要填写完整的请求路径(不利于后期的维护)

main.js 文件中进行配置:

// 配置请求根路径
axios.defaults.baseURL = 'http://api.com'

// 将 axios 挂载为 app 全局自定义属性
// 每个组件可通过 this.$http 访问到 axios
app.config.globalProperties.$http = axios

组件调用:

this.$http.get('/users')