Vue3(看完学会Vue3)

628 阅读9分钟

vue2和vue3的比较

Image.png

vue3更快

  1. diff方法优化:

Vue2中的虚拟dom是进行全量的对比,Vue3新增了静态标记(PatchFlag)在与上次虚拟节点进行对比时候,只对比带有patch flag的节点并且可以通过flag的信息得知当前节点要对比的具体内容

Image.png 2. hoistStatic静态提升

  • Vue2中无论元素是否参与更新,每次都会重新创建
  • Vue3中对于不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停的复用

Image.png 3. cacheHandlers事件侦听器缓存

  • 默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可。

Image.png

Options API 和 Composition API 的区别

Options API

又叫选项 API,以vue为后缀的文件,通过定义methods,computed,watch,data等属性与方法;

优缺点

条例清晰,相同的放在相同的地方;但随着组件功能的增大,关联性会大大降低,组件的阅读和理解难度会增加;

调用使用this,但逻辑过多时this会出现问题,比如指向不明等;

其本身并不是有效的js代码 我们在使用options API 的时候,需要确切了解我们具体可以访问到哪些属性,以及我们访问到的当前属性的行为在后台,VUE需要将此属性转换为工作代码,因为 我们无法从自动建议和类型检查中受益,因此给我们在使用相关属性时,造成了一定弊端

Composition API

   又叫组合式API,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)

   即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API;

优势 :

其代码更易读,更易理解和学习,没有任何幕后操作

Composition API的好处不仅仅是以不同的方式进行编码,更重要的是对于代码的重用

不受模板和组件范围的限制,也可以准确的知道我们可以使用哪些属性

由于幕后没有什么操作,所以编辑器可以帮助我们进行类型检查和建议

vue3结构

入口文件

//引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)

//挂载
app.mount('#app')

初识setup

定义:setup函数是所有的组合API的入口函数,组件中用到的数据、方法等,均要配置到setup函数中。

  1. 普通数据: 定义数据直接用JS的方式定义(但不是响应式)。
  2. 方法: 方法直接定义
  3. 响应式数据: 引入使用ref,通过ref函数定义数据,使数据变为响应式
  4. domscript想要使用setup中的数据和方法必须在setup中通过return以对象的方式暴露出去。

Image.png

setup的返回值:

  1. 若返回一个对象,则对象中的属性、方法,在模板中均可使用。
  2. 若返回一个渲染函数,则可自定义渲染内容(render h 函数)。

setup的两个注意点

  • setup执行的时机
    • 在beforeCreate之前执行一次,this是undefined。
  • setup的参数
    • props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性(和vue2一样声明接收)。
    • context:上下文对象
      • attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 this.$attrs(在父组件中传了但是在子组件中没有用props接收,就会挂载到子组件的vm上的attrs属性上,如果同props接收了,则挂载到vm上,但attrs属性上,如果同props接收了,则挂载到vm上,但attrs上就没了,捡漏的)。
      • slots: 收到的插槽内容, 相当于 this.$slots(父组件的插槽,以数组的形式存储)。
      • emit: 分发自定义事件的函数, 相当于 this.$emit,在vue3中需要用context.emit(),而且需要在子组件中注册在父组件中需要触发的事件,emits,否则出现警告。
export default {
		name: 'Demo',
		props:['msg','school'],
		emits:['hello'],
		setup(props,context){
			// console.log('---setup---',props)
			// console.log('---setup---',context)
			// console.log('---setup---',context.attrs) //相当与Vue2中的$attrs
			// console.log('---setup---',context.emit) //触发自定义事件的。
			console.log('---setup---',context.slots) //插槽
			//数据
			let person = reactive({
				name:'张三',
				age:18
			})

			//方法
			function test(){
				context.emit('hello',666)
			}

			//返回一个对象(常用)
			return {
				person,
				test
			}
		}
	}

注:

  1. 不能用 async 修饰setup,否则setup的返回值为 promise,导致数据和方法暴露不出去。
  2. setup函数中 的 this 默认为 undefined。

vue2方式和vue3方式的混合

  1. vue2 可以用this的方式拿到 vue3中setup暴露出来的数据和方法,但是 vue3 的setup拿不到 vue2 的数据和方法,为 undefined
  2. 当 vue2 和 vue3 的数据重名时, 以 vue3 为准。

响应式

ref函数(与vue2的ref不是一回事)

定义: 通过ref函数把数据变为引用实现的实例对象RefImpl, 通过getter 和 setter 进行数据劫持,实现响应式。

语法: const xxx = ref(initvalue)

image.png

使用:

  1. 通过数据名.value的方法可以获取和修改数据值。
  2. 在模板中通过{{}}使用数据并不需要加value, vue自动给加上了。
  3. 修改对象中的数据,只有在第一层用.value,下面直接.就可以获取和修改job.value.type = 'UI设计师'
<script>
	import {ref} from 'vue'
	export default {
		name: 'App',
		setup(){
			//数据
			let name = ref('张三')
			let age = ref(18)
			let job = ref({
				type:'前端工程师',
				salary:'30K'
			})

			//方法
			function changeInfo(){
				name.value = '李四'  // 修改普通数据
                                job.value.type = 'UI设计师'  // 修改对象数据
			}

			//返回一个对象(常用)
			return {
				name,
				age,
				job,
				changeInfo
			}
		}
	}
</script>

手写:

Image.png

基础类型和复杂类型

  • 基础类型:响应式依旧是靠 Object.defineObject() 的 set 和 get
  • 复杂类型:第一层仍旧是ref创建的RefImpl对象,但是属性的响应式是求助了vue3的新函数——reactive实现了proxy代理

reactive函数

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

语法:const xxx = reactive({ initvalue }),得到一个proxy对象

image.png

  1. 不用.value获取值。
  2. 数组可以直接用索引修改,在vue2中只能用加工过的(vue)JS原生的方法修改数组。
<script>
	import {reactive} from 'vue'
	export default {
		name: 'App',
		setup(){
			//数据
			let person = reactive({
				name:'张三',
				age:18,
				job:{
					type:'前端工程师',
					salary:'30K',
					a:{
						b:{
							c:666
						}
					}
				},
				hobby:['抽烟','喝酒','烫头']
			})

			//方法
			function changeInfo(){
				person.name = '李四'
				person.age = 48
				person.job.type = 'UI设计师'
				person.job.salary = '60K'
				person.job.a.b.c = 999
				person.hobby[0] = '学习'
			}

			//返回一个对象(常用)
			return {
				person,
				changeInfo
			}
		}
	}
</script>

手写:

Image.png

reactive对比ref

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

vue3响应式原理

实现原理:

  • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
  • 通过Reflect(反射): 对源对象的属性进行操作。 proxy:对proxy对象的操作都能反射到源对象上,修改时也会触发set
			const p = new Proxy(person,{
				//有人读取p的某个属性时调用
				get(target,propName){
					console.log(`有人读取了p身上的${propName}属性`)
					return Reflect.get(target,propName)
				},
				//有人修改p的某个属性、或给p追加某个属性时调用
				set(target,propName,value){
					console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`)
					Reflect.set(target,propName,value)
				},
				//有人删除p的某个属性时调用
				deleteProperty(target,propName){
					console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`)
					return Reflect.deleteProperty(target,propName)
				}
			})

Reflect:通过Object.defineProperty去操作属性时,如果是重复的操作,浏览器会抛出错误,而通过Reflect则不会,避免了try catch的问题

	//通过Object.defineProperty去操作
			//#region 
			/* try {
				Object.defineProperty(obj,'c',{
					get(){
						return 3
					}
				})
				Object.defineProperty(obj,'c',{
					get(){
						return 4
					}
				})
			} catch (error) {
				console.log(error)
			} */
			//#endregion

			//通过Reflect.defineProperty去操作
			//#region 
			/* const x1 = Reflect.defineProperty(obj,'c',{
				get(){
					return 3
				}
			})
			console.log(x1)
			
			const x2 = Reflect.defineProperty(obj,'c',{
				get(){
					return 4
				}
			}) 
			if(x2){
				console.log('某某某操作成功了!')
			}else{
				console.log('某某某操作失败了!')
			} */
			//#endregion

			// console.log('@@@')

computed计算属性

  1. 导入 computed 函数
  2. 向computed 中加入回调。
  3. 格式: 计算后的值 = computed( () => { return 值 } )
import {reactive,computed} from 'vue'
	export default {
		name: 'Demo',
		setup(){
			//数据
			let person = reactive({
				firstName:'张',
				lastName:'三'
			})
			//计算属性——简写(没有考虑计算属性被修改的情况)
			 person.fullName = computed(()=>{
				return person.firstName + '-' + person.lastName
			}) 

			//计算属性——完整写法(考虑读和写)
			person.fullName = computed({
				get(){
					return person.firstName + '-' + person.lastName
				},
				set(value){
					const nameArr = value.split('-')
					person.firstName = nameArr[0]
					person.lastName = nameArr[1]
				}
			})

			//返回一个对象(常用)
			return {
				person
			}
		}
	}

watch数据监听(监听ref或reactive或数组)

监听ref定义的基本类型数据

  1. 从vue中 导入 watch 函数;
  2. 语法: watch('要监听的数据', (new, old) => {要执行的操作}, {配置项});
  3. 同时监听多个: 监听的数据写成数组 ['数据1', '数据2'];

			//情况一:监视ref所定义的一个响应式数据(不用.value)
			 watch(sum,(newValue,oldValue)=>{
				console.log('sum变了',newValue,oldValue)
			},{immediate:true})

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

监听 reactive 定义的数据

必须是reactive定义的

  • 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
  • 监视reactive定义的响应式数据中某个属性时:deep配置有效。 监听reactive中的属性时必须用函数返回值的形式,否则监听不到(只能监听ref对象或reactive对象或是数组)。
  /* 情况三:监视reactive定义的响应式数据
  			
                        若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
  			若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 
                        对象中的属性对象中的属性变化也能监听到(全部都能监听到)
  */
   // 旧value和新value相同,deep全能监听
  watch(person,(newValue,oldValue)=>{
  	console.log('person变化了',newValue,oldValue)
  },{immediate:true,deep:false}) //此处的deep配置不再奏效
  
  //情况四:监视reactive定义的响应式数据中的某个属性(必须写成函数返回值的形式),旧value和新value不同,不配置deep不能监听
  watch(()=>person.job,(newValue,oldValue)=>{
  	console.log('person的job变化了',newValue,oldValue)
  },{immediate:true,deep:true}) 
  
  //情况五:监视reactive定义的响应式数据中的某些属性
  watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
  	console.log('person的job变化了',newValue,oldValue)
  },{immediate:true,deep:true})
  
  //只监听reactive中的属性的对象,需要配置deep,否则不触发watch
  watch(()=>person.job,(newValue,oldValue)=>{
      console.log('person的job变化了',newValue,oldValue)
  },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效

watchEffect函数

  //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
  watchEffect(()=>{
      const x1 = sum.value
      const x2 = person.age
      console.log('watchEffect配置的回调执行了')
  })

vue3生命周期

配置项形式及顺序

		beforeCreate() {
			console.log('---beforeCreate---')
		},
		created() {
			console.log('---created---')
		},
		beforeMount() {
			console.log('---beforeMount---')
		},
		mounted() {
			console.log('---mounted---')
		},
		beforeUpdate(){
			console.log('---beforeUpdate---')
		},
		updated() {
			console.log('---updated---')
		},
		beforeUnmount() {
			console.log('---beforeUnmount---')
		},
		unmounted() {
			console.log('---unmounted---')
		},

组合式API(把生命周期放在setup中)应用及顺序

生命周期beforeCreate和created相当于setup,没有专门的API方法。

import {ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue'
	setup(){
			console.log('---setup---')
			//数据
			let sum = ref(0)

			//通过组合式API的形式去使用生命周期钩子
			onBeforeMount(()=>{
				console.log('---onBeforeMount---')
			})
			onMounted(()=>{
				console.log('---onMounted---')
			})
			onBeforeUpdate(()=>{
				console.log('---onBeforeUpdate---')
			})
			onUpdated(()=>{
				console.log('---onUpdated---')
			})
			onBeforeUnmount(()=>{
				console.log('---onBeforeUnmount---')
			})
			onUnmounted(()=>{
				console.log('---onUnmounted---')
			})

			//返回一个对象(常用)
			return {sum}
		},

注:当配置项生命周期和组合式API同时使用时,先执行的是setup中的生命周期,然后执行配置项中的同一级的声明周期,然后执行setup中的下一生命周期

自定义hook

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。

  • 类似于vue2.x中的mixin。

  • 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。 使用:

  1. 定义一个方法,然后暴露。
import {reactive,onMounted,onBeforeUnmount} from 'vue'
export default function (){
	//实现鼠标“打点”相关的数据
	let point = reactive({
		x:0,
		y:0
	})

	//实现鼠标“打点”相关的方法
	function savePoint(event){
		point.x = event.pageX
		point.y = event.pageY
		console.log(event.pageX,event.pageY)
	}

	//实现鼠标“打点”相关的生命周期钩子
	onMounted(()=>{
		window.addEventListener('click',savePoint)
	})

	onBeforeUnmount(()=>{
		window.removeEventListener('click',savePoint)
	})

	return point
}

  1. 引入想要使用的组件中
<script>
	import usePoint from '../hooks/usePoint'
	export default {
		name:'Test',
		setup(){
			const point = usePoint()
			return {point}
		}
	}
</script>

toRef 和 toRefs

toRef

定义:toRef是为了使数据在模板中使用更加简介,而不是一直使用对象.属性的形式,如果在setup暴露时直接将对象.属性赋值给key(模板中使用的数据为setup暴露时对象的key),会导致数据不是响应式。而toRef解决了这个问题。

  1. ref和toRef的区别
  • ref根据传入的数据,返回一个新的ref实例对象,地址不指向传入的变量地址,是深拷贝,虽然页面会变,但是原本的对象就不会改变了;
  • toRef根据传入的数据,将数据转化为ref形式,但是返回的ref实例对象仍旧指向旧的地址,仍旧响应源对象的get和set; 所以要用toRef转换而不是ref转换。

Image.png

Image.png

  1. 语法: const xxx = toRef(对象名, ‘属性’); 如果属性为深层,则对象.属性到深层转换
			return {
				person,
				name:toRef(person,'name'),
				age:toRef(person,'age'),
				salary:toRef(person.job.j1,'salary'),
				
			}

toRefs

定义:当对象中的属性过多时,不能使用toRef一个个转换,而需要使用toRefs整体转换,但是只能转换对象的第一层,输出为对象

			return {
				person,
				...toRefs(person)
			}

缺陷: 当后续向对象中新添加属性时,toRefs拆解不出来,因为toRefs只会在setup()执行的时候执行,而setup只执行一次。所以当需要添加新属性时:

  1. 在原本的对象中开始就注册一个空的属性占位;
  2. 将整个person对象暴露出去,使用时就用对象.属性的方式。

其他的component API

shallowReactive 和 shallowRef

和reactive的区别: shallowReactive只考虑第一层数据的响应式

和ref的区别: 当传入基本数据时,shallowRef和ref没有区别,当传入对象时,ref会自动调用reactive使数据变为响应式,而shallowRef不会把对象改为响应式

readonly 和 shallowReadonly

readonly: 使数据不允许改变

readonly和 const 的区别: const是变量保护,readonly是属性保护。

shallowReadonly: 只保护对象的第一层数据不被改变,深层可以。

person = readonly(person)

toRaw 和 markRaw

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

customRef


 setup() {
    //自定义一个ref——名为:myRef
    function myRef(value, delay) {
      let timer;
      return customRef((track, trigger) => {
        return {
        // 当模板需要数据时就来找get返回
          get() {
            console.log(`有人从myRef这个容器中读取数据了,我把${value}给他了`);
            track(); //通知Vue追踪value的变化(提前和get商量一下,让他认为这个value是有用的)
            return value;
          },
          set(newValue) {
            console.log(`有人把myRef这个容器中数据改为了:${newValue}`);
            clearTimeout(timer);
            timer = setTimeout(() => {
            // 修改数据会触发,然后把新值付给变量
              value = newValue;
              trigger(); //通知Vue去重新解析模板
            }, delay);
          },
        };
      });
    }

    // let keyWord = ref('hello') //使用Vue提供的ref
    let keyWord = myRef("hello", 500); //使用程序员自定义的ref

    return { keyWord };
  },

provide 与 inject

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

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

  • 具体写法:

    1. 祖组件中:

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

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

响应式数据的判断

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

新的组件

Fragment

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

Teleport

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

    <teleport to="移动位置HTML标签">
    	<div v-if="isShow" class="mask">
    		<div class="dialog">
    			<h3>我是一个弹窗</h3>
    			<button @click="isShow = false">关闭弹窗</button>
    		</div>
    	</div>
    </teleport>
    

Suspense

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

  • 这样其实setup() 可以是async,可以返回异步了。

  • 使用步骤:

    • 异步引入组件

      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>
      

      其他

      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.componentapp.component
      Vue.directiveapp.directive
      Vue.mixinapp.mixin
      Vue.useapp.use
      Vue.prototypeapp.config.globalProperties

其他改变

其他改变

  • 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” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

  • ......