Vue3使用总结,不同于Vue2的特点

193 阅读13分钟

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。也是我们在使用组合式Api书写代码的一个语法糖。

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例,不同于Vue2使用的是:new Vue() 创建示例的

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

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

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

响应式数据:vue3 中控制台打印的时候 带有 Proxy 或者 Ref... 的实例对象都是响应式数据。

常用 Composition API

setup

  • setup是所有Composition API(组合API) 书写的钩子函数 。
  • 组件中所用到的:数据、方法等等,均要配置在setup中。
  • setup函数的两种返回值:
    • 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
    • 若返回一个渲染函数:则可以自定义渲染内容。(了解)
  • 如果组件是同步引入的,setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。
  • 但组件通过 defineAsyncComponent()引入组件,则组件内部 setup 也可以返回一个Promise实例,但需要Suspense和异步组件的配合
  • setup执行的时机,在beforeCreate之前执行一次,this是undefined。
  • setup的参数有两个
    • props:值为对象,包含:组件外部传递过来的数据,但需要在组件内部通过props配置声明接收了属性,在setup函数中才可以被第一个参数中接收到
    • context:上下文对象
      • attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 this.$attrs
      • slots: 收到的插槽内容, 相当于 this.$slots
      • emit: 分发自定义事件的函数, 相当于 this.$emit
props: ['外部传入属性1', '外部传入属性2', ...], // 用法与 vue2 一致

// 这是vue3 新增的组件配置,用于接收外部传入的自定义事件,不使用浏览器会报警告,但不影响使用
emits:['自定义事件名'], 

setup(props,context){
    // 想要获取到外部传入的数据,在setup打印出来,还需要配置props
    console.log('---setup---',props) 
    
    console.log('---setup---',context)
    
    //相当与Vue2中的$attrs,也可以用于接收外部传递的数据。
    //但在vue3中如果使用了 props 配置接收了外部传入的属性,则 context.attrs 不接收,
    console.log('---setup---',context.attrs) 
    
    //context.emit触发自定义事件,使用前还需要 配置 emits 接收自定义事件才可以
    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
    }
}

响应式API-ref函数

定义响应式数据:

  • ref函数,常用于基本数据类型定义为响应式数据(也可以用于定义复杂数据类型,但一般不会这么做)
  • 定义的数据为 RefImpl 的实例对象
  • 修改变量的值,需要:变量.value = 新值
  • 在模板 template 中使用ref申明的响应式数据,可以省略:.value,就是直接访问
  • 在 watch 同时监听多个数据时,第一个参数用数组的方式把监听的数据包裹起来。watch([数据1,数据2,数据3],() => {})
<template>
    <div class="container"> 
        <div>{{name}}</div>
        <div>{{age}}</div>
        <button @click="updateName">修改数据</button>
    </div>
</template>
<script>
import { ref } from 'vue' 
export default { 
    name: 'App', 
    setup () { 
        // 1. name数据 const name = ref('ls') console.log(name) 
        // 调用方法修改的是需要对修改的对象中的 .value 进行修改才能成功 
        const updateName = () => { name.value = 'zs' } 
        // 2. age数据 
        const age = ref(10) 
        // ref常用定义简单数据类型的响应式数据 
        // 其实也可以定义复杂数据类型的响应式数据 
        // 对于数据未知的情况下 ref 是最适用的 
        
        // setup 需要return一个对象才能在模板中使用
        return {name, age, updateName}
    } 
}
</script> 

响应式API- reactive 函数

  • 定义一个引用类型数据(数组,对象),成为响应式数据。不能让基本类型数据变成响应式
  • 定义的数据为 Proxy 类的实例对象
  • 修改 reactive 定义的数据不需要 .value
  • reactive 定义的数据,在被 watch 监听时是默认深度监听的,能够监听到所有的子孙属性。
  • 监听复杂类型数据中的某一个属性时需要把属性以箭头函数的方式返回,watch
<template> 
<div class="container"> 
    <div>{{obj.name}}</div> 
    <div>{{obj.age}}</div> 
    <button @click="updateName">修改数据</button> 
</div> 
</template> 
<script> 
import { reactive } from 'vue' 
export default { 
    name: 'App', 
    setup () { 
        const obj = reactive({ name: 'ls', age: 18 }) 
        // 修改名字 
        const updateName = () => { console.log('updateName') obj.name = 'zs' } 
        
        // setup 需要return一个对象才能在模板中使用
        return { obj ,updateName } 
    } 
} 
</script>

toRef()#

  • 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:
    • 改变源属性的值将更新到用 toRef创建的 ref 的值
    • 反之修改toRef创建的 ref 的值也会更新到源对象的属性。
  • 还可以通过 let obj = toRefs('响应数据') 创建多个 ref,obj是一个对象,可以通过 ...obj 解构出单独的 ref

computed 计算属性函数

  • 与Vue2.x中computed配置功能一致,在vue3中需要在setup里面调用
<script>
import {computed} from 'vue'
export default {
    setup(){
        ...
            //计算属性——简写
        let fullName1 = computed(()=>{
            return person.firstName + '-' + person.lastName
        })
        //计算属性——完整
        let fullName2 = computed({
            get(){
                return person.firstName + '-' + person.lastName
            },
            set(value){
                const nameArr = value.split('-')
                person.firstName = nameArr[0]
                person.lastName = nameArr[1]
            }
        })
        return {fullName1, fullName2}
    }
}
</script>

监听属性 watch

  • 与Vue2.x中watch配置功能一致
  • watch API 第一参数传入要监听的属性,第二个参数传入回调函数,第三传入配置对象
  • watch小“坑”:
    • 监视reactive定义的响应式数据时:oldValue无法正确获取,新值和旧值都是一样的
    • vue3中 reactive定义的响应式数据时默认强制开启了深度监视(deep配置失效)。
    • 监视reactive定义的响应式数据中某个属性时,watch的第一个参数要传入一个函数,将要监听的属性做返回值返回
    • 监视reactive定义的响应式数据中多个属性时,可以像监听ref数据一样定义数组传入到watch的第一个参数
    • 监视reactive定义的响应式数据中某个属性时:deep配置有效。
<script>
import {computed} from 'vue'
export default {
    setup(){
        //情况一:监视ref定义的响应式数据
        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定义的响应式数据中的 某个 属性
        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})

        //特殊情况
        watch(()=>person.job,(newValue,oldValue)=>{
            console.log('person的job变化了',newValue,oldValue)
        },{deep:true}) //此处由于监视的是reactive定义的对象中的某个属性,所以deep配置有效
    }
}
</script>

watchEffect函数

  • watch的套路是:既要指明监视的属性,也要指明监视的回调。
  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
  • watchEffect有点像computed:
    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。

组合式 API:依赖注入

  • 作用:实现嵌套组件祖与后代组件间通信
  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据

provide()

  • 提供一个值,可以被后代组件注入。
  • provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值(可以是任意数据类型)。
  • 与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。
<script setup> 
    import { ref, provide } from 'vue' 
    import { fooSymbol } from './injectionSymbols' 
    // 提供静态值 
    provide('foo', 'bar') 
    // 提供响应式的值 
    const count = ref(0) 
    provide('count', count) 
    // 提供时将 Symbol 作为 key 
    provide(fooSymbol, count) 
</script>

inject()

  • 注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。
  • 第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。
  • 第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。
  • 与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。
  • 需要定义一个变量来接收注入的返回值
<script setup>
    import { inject } from 'vue'
    import { fooSymbol } from './injectionSymbols'

    // 注入值的默认方式
    const foo = inject('foo')

    // 注入响应式的值
    const count = inject('count')

    // 通过 Symbol 类型的 key 注入
    const foo2 = inject(fooSymbol)

    // 注入一个值,若为空则使用提供的默认值,第二个参数在没有找到依赖时才会使用
    const bar = inject('foo', 'default value')

    // 注入一个值,若为空则使用提供的工厂函数
    const baz = inject('foo', () => new Map())

    // 注入时为了表明提供的默认值是个函数,需要传入第三个参数
    const fn = inject('function', () => {}, false)
</script>

工具类 API

isRef()

  • 检查某个值是否为 ref。返回值为布尔值:isRef('要检测的数据')

unref()

  • 如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

isProxy()

  • 检查一个对象是否是由 reactive() readonly() shallowReactive() shallowReadonly() 创建的代理。

isReactive()

  • 检查一个对象是否是由 reactive() shallowReactive()创建的代理。

isReadonly()

  • 检查传入的值是否为只读对象。只读对象的属性可以更改,但他们不能通过传入的对象直接赋值。
  • 通过 readonly() shallowReadonly()创建的代理都是只读的,因为他们是没有 set 函数的 computed() ref。

不常用、进阶类 API

shallowReactive()

  • 只处理对象最外层属性的响应式(浅响应式)。也就是处理一个对象,只有第一层的属性是响应式的,而在深层次的属性则不是响应式
  • let obj = shallowReactive({name: '张三', job: {jobName: '前端', salary: 12000}})
  • 上面的代码中 第一层属性 name、job 都是响应式,而第二层 jobName... 等都不是响应式的

shallowRef()

  • 只处理基本数据类型的响应式, 不对引用类型数据进行响应式处理
  • 跟API ref 不同,ref是任何数据都可以处理,但不建议用来处理引用类型数据

customRef()

  • 创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
  • customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。
  • 一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
  • 也就是可以自己创建一个符合自己预期的 ref 响应式数据
  • 创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:
// 定义一个hook的 js 文件
import { customRef } from 'vue' 
export function useDebouncedRef(value, delay = 200) { 
    let timeout 
    return customRef((track, trigger) => { 
        return { 
            get() { track() return value }, 
            set(newValue) { 
                clearTimeout(timeout) 
                timeout = setTimeout(() => { value = newValue trigger() }, delay) 
            } 
        } 
    }) 
}

// 在组件中使用
<script setup>
    import { useDebouncedRef } from './debouncedRef.js'
    const text = useDebouncedRef('hello', 1000)
</script>

<template>
      <p>
        在下方的输入框中,只有输入间隔在 1000 毫秒以上才会触发重新渲染
      </p>
      <p>{{ text }}</p>
      <input v-model="text" />
</template>

readonly()

  • 接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
  • 只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。
  • 也就是说不管是什么类型数据通过 readonly 处理的都是只读的

shallowReadonly()

  • readonly() 的浅层作用形式
  • 只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了
  • 也就是让一个响应式数据变为只读的,且只是第一层属性是只读,而深层次的属性还是响应式

toRaw 与 markRaw

  • toRaw:
    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
    • 这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
  • markRaw:
    • 作用:标记一个对象,使其永远不会再成为响应式对象。
    • 应用场景:
      • 有些值不应被设置为响应式的,例如复杂的第三方类库等。
      • 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

VUE3 新增内置组件

<Teleport>

  • 将其插槽内容渲染到 DOM 中的另一个位置。也就是Teleport组件的属性 to 指定的位置
  • 这个效果就很好的解决,当嵌套很多层的组件,最里层的组件有一个结构想要依据 body 定位,就可以使用 Teleport
<teleport to="#some-id" /> 
<teleport to=".some-class" /> 
<teleport to="[data-teleport]" />
<teleport to="body" >
    ...
</teleport>

<Suspense> #

  • 用于协调对组件树中嵌套的异步依赖的处理。
  • <Suspense> 接受两个插槽:#default 和 #fallback。它将在内存中渲染默认插槽的同时展示后备插槽内容。
  • 如果在渲染时遇到异步依赖项 (异步组件和具有 async setup() 的组件),它将等到所有异步依赖项解析完成时再显示默认插槽。
<template>
    <div class="app">
        <Suspense>
            <template v-slot:default>
                <Child/>
            </template>
            <template v-slot:fallback>
                <h3>加载中.....</h3>
            </template>
        </Suspense>
    </div>
</template>

生命周期钩子函数:

Vue3.0也提供了 Composition API 形式的生命周期钩子,在 setup 中调用,与Vue2.x中钩子对应关系如下:

  • beforeCreate===>setup()
  • created=======>setup()
  • beforeMount ===>onBeforeMount 挂载DOM前
  • mounted=======>onMounted 挂载DOM后
  • beforeUpdate===>onBeforeUpdate 更新组件前
  • updated =======>onUpdated 更新组件后
  • beforeUnmount ==>onBeforeUnmount 卸载销毁前
  • unmounted =====>onUnmounted 卸载销毁后

vue3 对比 vue2 的变化和注意点

  1. vue3 创建Vue 应用实例是通过 createApp ,Vue2创建是通过 new Vue()
  2. vue3 中没有 this 的实例对象使用
  3. 在 template 模板中,vue3 不再需要一个根标签包裹,会自动添加 Fragment 空标签。
  4. vue3 引入文件需要书写全文件名和后缀,vue2可以不用书写后缀会自动查找文件。
  5. vue3 提供了组合式API 同时 支持 vue2 的选项式API, vue2 只支持选项式API。
  6. vue3中调用生命周期钩子函数可以通过组件配置项调用,也可以在setup函数中调用生命周期钩子函数,vue2 只能通过组件配置生命周期
  7. vue3 的生命周期函数 在组件中的 setup 可以多次调用同一个生命周期钩子函数,执行顺序和书写顺序相同;vue2不可以多次调用同一个生命周期钩子函数。
  8. vue3 和 vue2 通过组件配置项配置的生命周期函数命名大致都一样,但组件卸载的生命周期函数不一样
  9. vue3 取消过滤器 不建议使用混入(mixin)
  10. 自定义指令的生命周期不一样
  11. vue3中定义的引用类型的响应式数据是深度监听的,也就是对象属性的新增或删除等都可以监测到,修改数组的每一项值也会被监测。就不会像vue2那样监测不到,需要通过内置的 $set() 去修改引用类型才能被监测
  12. vue3中父组件给子组件传递自定义事件时,子组件需要配置 emits 接收传入的事件,不然浏览器会警告。而vue2可以直接使用
  13. 计算属性(computed),在vue3 中computed是在setup中调用的函数方法,vue2则是组件的配置之一
  14. 监听属性(watch),在vue3 中watch是在setup中调用的函数方法,vue2则是组件的配置之一

vue3 注意点

  1. 一般不推荐在组件中使用配置项的生命周期同时还在 setup 中调用生命周期函数,如果同时使用相同的生命周期钩子函数再 setup 中的会先执行。
  2. 在 vue3 中 v-if 的权重高于 v-for,vue2则相反

vue3框架 可对项目做的优化(官方)

减少大型不可变数据的响应性开销#

Vue 的响应性系统默认是深度的。虽然这让状态管理变得更直观,但在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪。好在这种性能负担通常这只有在处理超大型数组或层级很深的对象时,例如一次渲染需要访问 100,000+ 个属性时,才会变得比较明显。因此,它只会影响少数特定的场景。

Vue 确实也为此提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新。

大型虚拟列表#

所有的前端应用中最常见的性能问题就是渲染大型列表。无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。

但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。

要实现列表虚拟化并不简单,幸运的是,你可以直接使用现有的社区库:

避免不必要的组件抽象#

有些时候我们会去创建无渲染组件或高阶组件 (用来渲染具有额外 props 的其他组件) 来实现更好的抽象或代码组织。虽然这并没有什么问题,但请记住,组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。

需要提醒的是,只减少几个组件实例对于性能不会有明显的改善,所以如果一个用于抽象的组件在应用中只会渲染几次,就不用操心去优化它了。考虑这种优化的最佳场景还是在大型列表中。想象一下一个有 100 项的列表,每项的组件都包含许多子组件。在这里去掉一个不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。

异步组件——实现组件懒加载

defineAsyncComponent()

  • 在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能:
import { defineAsyncComponent } from 'vue' 
const AsyncComp = defineAsyncComponent(() => { 
    return new Promise((resolve, reject) => { 
        // ...从服务器获取组件 
        resolve(/* 获取到的组件 */) 
    }) 
}) 
// ... 像使用其他一般组件一样使用 `AsyncComp`
  • ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))

将不需要重新渲染的组件排除渲染——提升性能

v-once#

  • 仅渲染元素和组件一次,并跳过之后的更新。

v-memo #

  • 期望的绑定值类型: any[]
  • 缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。