Vue3学习笔记

102 阅读10分钟

前言:Vue3.0在2022 年 2 月 7 日成为新的默认版本,那么我们也不能停下学习的脚步。本文内容记录了个人在Vue3知识点以及学习时的个人的看法,如有错误的地方,欢迎指出。

常用的Composition API

Composition API

概念:

Composition API(组合式a pi),他是一系列api的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。

Options API 存在的问题

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

Composition API 的优势

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

1. Setup

  1. 理解:setup是 Vue3中的一个新的配置项,是一个函数

  2. setup是所有Compositon API(组合式API)表演的舞台,所有组合式api均要在setup里使用

  3. 组件里的数据、方法等,都要配置在setup里。

  4. setup的返回值的两种形式:

    • 若返回值是一个对象,则对象里的方法、属性可以直接在模板中使用。(重要)
    • 若返回值是一个渲染函数,则可自定义渲染内容。(了解)
  5. 注意点:

    1. 尽量不要与vue2的配置项混用。

      • vue2的配置(data,methods,computed...)可以访问到setup中的属性和方法。
      • 但是setup不能访问vue2的配置中的数据。
      • 如果数据有重名,已setup的优先。
    2. setup在 beforeCreate 之前执行,this是undefined

    3. setup接收的参数:

      • props:值为对象,包含组件外部传过来,且组件内部声明接收了的属性。

      • context(上下文对象):

        • attrs:值为对象,包含组件外部传过来,但是没有在props中声明接收的属性,相当于this.$attrs.
        • slots:收到的插槽内容,想当于this.$slots。现在的vue中具名插槽和作用域插槽在父组件调用时需要使用v-slot,以前具名插槽的slot和作用域插槽的slot-scopescope写法已弃用
        • emit:触发自定义事件,相当于this.$emit.

2. ref函数

  • 作用:定义一个基本类型的响应式数据,创建一个包含响应式数据的引用对象(reference对象,简称ref对象)

  • 用法:

    // 需要通过 `import {ref} from 'vue'`进行引入才能使用。
    let name=ref('张三') 
    
    • 在JS中操作数据:name.value
    • 在模板中操作数据:不需要.value,直接使用名字就行,例如:<div>{{name}}</div>
  • 注意:

    • 传入的数据可以是基本数据类型,也可以是复杂数据类型。

      1. 基本数据类型:底层依然是依靠Object.defineProperty()get()和set()实现的响应式。
      2. 复杂数据类型:底层帮你调用了vue3的一个新的函数——reactive函数。

3.reactive函数

  • 作用:定义一个对象类型的响应式数据。可以接收一个对象或者数组类型的数据,返回值是一个Proxy实例对象

  • 用法:

    //需要通过 `import {reactive} from 'vue'`进行引入才能使用。
    let parson = reactive({
        name:'张三',
        age:18
    })
    
  • 注意:

    • reactive函数底层使用的是ES6中的Proxy函数实现的响应式。
    • reactive函数定义的响应式数据是深层次的。

4. reactive与ref的区别

  • 从定义数据角度:

    • ref定义的是基本数据类型
    • reactive定义的是复杂数据类型(对象和数组)
    • 注意:ref也可以定义复杂数据类型的数据,内部会自动调用reactive函数。
  • 从原理角度:

    • ref使用Object.defineProperty()get()和set()实现的响应式(数据劫持)。
    • reactive使用的是ES6中的Proxy函数实现的响应式(数据劫持),并通过Reflect函数来对源代码的属性进行操作。
  • 从使用角度:

    • ref:在JS中操作数据需要.value,在模板中使用不需要
    • reactive: 可以直接使用,不需要.value

5. Vue3的响应式原理

实现原理:

  • 使用Proxy(代理)对数据的变化(添加、删除,修改、读取)进行监听。

  • 使用Reflect(反射)对源对象进行操作。

    • 好处:对比原生的对属性的操作,可以避免频繁报错,在封装框架时比较好,个人开发中不太需要。
let person = {
            name: '李四',
            age: 28
        };
        let p = new Proxy(person, {
                get(target, prop) {
                    console.log(`读取了person的${prop}属性`)
                    return Reflect.get(target, prop)
                },
                set(target, prop, value) {
                    console.log(`修改了person的${prop}属性,我去更新页面了`)
                    Reflect.set(target, prop, value)
                },
                deleteProperty(target, prop) {
                    console.log(`我删除了person的${prop}属性`)
                    return Reflect.deleteProperty(target, prop)
                }
            })

6. 计算属性与监视属性

1. computed函数

  1. 作用:与 Vue2中一致.

  2. 用法:

    import {computed} from 'vue'
    //计算属性简便写法
     let name = computed(() => {
         return parson.xing + '-' + parson.ming
     })
    //计算属性完整写法,挂载到parson上就不需要setup多返回一个数据了
    parson.name = computed({
        get() {
            return parson.xing + '-' + parson.ming
        },
        set(value) {
            const arr = value.split('-')
            parson.xing = arr[0]
            parson.ming = arr[1]
        }
    })
    

2. Watch函数

  1. 作用:与Vue2中一致

  2. 注意点:

    • 在监视ref定义的数据时:

      • 基本数据类型时:不能使用.value,因为加了过后是具体的基本类型值,是不能被监视的。
      • 复杂数据类型时:需要使用.value,因为底层求助了reactive函数,不加的话,监听的是对象的地址,加了过后监听的才是数据。(或者加deep也能解决
    • 在监视reactive定义的数据时,oldValue(修改前的数据)无法正常获取

      • 原因:因为reactive定义的是复杂数据类型,所以修改前和修改后指针指向的是同一个对象。
    • 在监视reactive定义的数据时,强制开启深度监视(deep配置失效,暂无解决方法)

3.用法:

  • 情况1: 监视ref定义的响应式数据

    let name = ref('张三')
    watch(name, (newValue, oldValue) => {
        console.log('name属性被修改了', newValue, oldValue)
    }, { immediate: true })
    
    • 输出:

image-20221011175531849.png

  • 情况2: 监视多个ref定义的响应式数据

    let name = ref('张三')
    let age = ref(18)
    //监视多个数据时,传入的监视数据是一个数组
    watch([name, age], (newValue, oldValue) => {
        console.log('name或者age属性被修改了', newValue, oldValue)
    }, { immediate: true })
    
    • 输出:

img2.png

  • 情况3: 监视reactive定义的数据

    let parson = reactive({
        name: '张三',
        frame: {
            frameName: 'Vue'
        }
    })
    //注意点:
    //      1. deep配置无效
    //      2.监听的是reactive定义的函数,oldValue无法正常获取,始终会显示为更新后的数据。
    watch(parson, (newValue, oldValue) => {
        console.log('parson发生变化了', newValue, oldValue)
    }, {
        immediate: true,
        deep: false   //已经强制开启了深度监视,此时的deep配置无效。
    })
    
    • 输出:

image-20221011181732053.png

  • 情况4: 监视reactive定义的数据中的某一个属性

    let parson = reactive({
        name: '张三',
        frame: {
            frameName: 'Vue'
        }
    })
    //注意点:
    //      1. 被监听的属性需要写成函数形式
    //      2.因为此时监听的是复杂数据类型中的一个基本数据类型,所以oldValue可以正常获取
    watch(() => parson.frame.frameName, (newValue, oldValue) => {
        console.log('parson的值发生变化了', newValue, oldValue)
    }, {
        immediate: true   //因为是基本数据类型,所以不需要deep
    })
    
    • 输出:

image-20221011182405767.png

  • 情况5: 监视reactive定义的数据中的某些数据

    let parson = reactive({
        name: '张三',
        frame: {
            frameName: 'Vue'
        }
    })
    //注意点:
    //      1. 监听多个数据时,需要写成数组形式,并且被监听的属性需要写成函数形式,
    //      2.因为此时监听的是复杂数据类型中的一个基本数据类型,所以oldValue可以正常获取
    watch([() => parson.name, () => parson.frame.frameName], 
          (newValue, oldValue) => {
        console.log('parson的值发生变化了', newValue, oldValue)
    }, {
        immediate: true
    })
    
    • 输出:

image-20221011183048318.png

  • 特殊情况:

    • 监听reactive定义的响应式数据的子属性对象时,deep配置是有效的,不会强制开启深度监视。(只有监视的数据为 reactive声明的对象时才会强制开启深度监视,监听子属性不会。)
    let parson = reactive({
        name: '张三',
        frame: {
            frameName: 'Vue',
            firends: {
                name: '李四'
            }
        }
    })
    watch(() => parson.frame, (newValue, oldValue) => {
        console.log('parson的值发生变化了', newValue, oldValue)
    }, {
        immediate: true, deep: true   //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效
    })
    
    • 输出:

image-20221011184811117.png

3. watchEffect函数

  1. 作用:不需要指明监视哪个属性,只需要指定监视的回调函数,回调函数中用到了哪些属性,就会监视哪些属性。默认已经开启了immediate.

  2. watchEffect与computed:

    • watchEffect与computed比较类似,都是回调里用到过谁,就会监视谁。但是:
    • computed注重结果,所以需要返回值。
    • watchEffect注重过程,所以不需要返回值。
let x = ref(1)
let y = ref(2)
//只要回调函数里所用到的属性发生了变化,就会调用这个回调函数
watchEffect(() => {
    let b = x.value + y.value;       
    console.log('watchEffect的回调函数调用了')
})

7. 生命周期

生命周期图(vue2和vue3)

  1. 注意点:

    • 在Vue3中,有两个生命周期改名了:

      • beforeDestroy===>beforeUnmount 销毁前改为解除绑定前
      • destroyed===>unmount 销毁改为解除绑定
    • 在Vue3中,可以使用配置项的形式使用生命周期钩子,也可以使用Composition API 形式:

      • 配置项形式

        mounted(){
            console.log('页面挂载了')
        }
        
      • Composition API形式

        import {onMounted} from 'vue'
        setup(){
            onMounted(()=>{
                console.log('页面挂载了')
            })
        }
        
    • 使用Composition API 形式时:

      • 需要在原来的名字前加on,并且需要引入。
      • beforeCreate()Createed()在组合式API里不存在,直接使用setup即可

8. 自定义hook函数

  1. 概念:本质是一个函数,把setup中使用的Composition API进行封装,类似于vue2中的mixin。

  2. 优势:代码复用,让setup中代码更少、更清晰。

  3. 用法:

    • usePoint.js中:

      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
          }
          //在页面挂载完毕时给window绑定点击事件
          onMounted(() => {
              window.addEventListener('click', savePoint)
          })
          //在页面解除挂载完毕时给window解除绑定点击事件
          onBeforeUnmount(() => {
              window.removeEventListener('click', savePoint)
          })
          //需要把point返回出去,否则组件中用不到。
          return point
      }
      
    • 在组件中:

      <template>
          {{point.x}},{{point.y}}
      </template><script>
      import usePoint from './hooks/usePoint'
      export default {
          setup() {
              //调用hook函数并赋值。
              let point = usePoint()
      ​
              return {
                  point
              }
          }
      }
      </script>
      

toRef与toRefs

  1. toRef概念toRef可以创建一个ref对象,使其value值指向另一个对象的某个属性。

  2. 作用:可以使在模板中使用数据时少写一个前缀的变量名。

  3. 缺点:在setup返回数据时需要多次调用toRef函数,效率不高(使用toRefs可以解决)

  4. 用法:

    <template>
        <!-- 原本调用数据的方式
            {{obj.name}}
            {{obj.age}}
        -->
        <!-- 使用toRef后调用数据的方法 -->
        {{ name }}
        {{ age }}
    </template><script>
    import { reactive, toRef } from '@vue/reactivity'
    export default {
        setup() {
            let obj = reactive({
                name: '张三', age: 18
            })
            return {
                //原本返回数据的方法
                //obj,
                //使用toRef后返回数据的方法
                name: toRef(obj, 'name'),
                age: toRef(obj, 'age')
            }
        }
    }
    </script>
    
  5. toRefs概念toRefs作用与toRef相同,但是可以一次性创建多个ref对象,提高效率。(只会返回对象的第一层数据

  6. 用法:

    <template>
        {{ name }}
        {{ age }}
    </template><script>
    import { reactive, toRefs } from '@vue/reactivity'
    export default {
        setup() {
            let obj = reactive({
                name: '张三', age: 18
            })
            return {
                //使用toRef返回数据的方法
                //name: toRef(obj, 'name'),
                //age: toRef(obj, 'age'),
                //使用toRefs返回数据的方法,与拓展运算符结合可以解决toRef需要多次调用的痛点
                ...toRefs(obj)
            }
        }
    }
    </script>
    

不常用的 Composition API

shallowReactive与shallowRef(浅响应式)

  1. 特点 :

    • shallowReactive与reactive用法基本相同,但是shallowReactive只处理对象第一层的属性(浅响应式,类似于没开深度监视)
    • shallowRef只处理基本数据类型的数据(与Ref效果相同),不会处理复杂数据类型的数据(不会把数据改成响应式)
  1. 运用场景:

    • shallowReactive:一个对象只有外层数据需要响应式,深层次的数据不需要响应式时。
    • shallowRef:当一个对象后期不需要修改,只需要一个新的对象替换时。

readonly与shallowReadonly(改为只读)

  1. 特点:

    • readonly:使一个响应式数据变为只读的(深只读)。
    • shallowReadonly:使一个响应式数据变为只读的(浅只读)。
  2. 优点:如果不把数据变为响应式,那么就会遇到当数据会被修改时,vue检测不到数据修改。这两个函数可以直接让数据不可修改,从根源上解决问题。

  3. 应用场景:不希望数据被修改时。

    注意:这两个函数必须传入响应式数据。

toRaw和markRaw(转为普通对象)

  • toRaw:

    • 作用:将一个由reactive生成的响应式对象变为普通对象

    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象进行修改操作时,不会引起页面更新。

    • 用法:

      setup() {
      ​
          let obj = reactive({
              name: '张三',
              age: 18,
              friend: {
                  name: '李四'
              }
          })
          let a = toRaw(obj)
          //此时的a是一个obj对应的普通对象,对其进行修改时不会引起页面更新。
          console.log(a)
          return {
              obj,
              ...toRefs(obj)
          }
      }
      
  • markRaw:

    • 作用:标记一个对象,使其永远不会再变为响应式对象。

    • 应用场景:

      1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。
      1. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
    • 用法:

      setup() {
      ​
              let obj = reactive({
                  name: '张三',
                  age: 18
              })
              //此时对添加到obj上的friend对象进行修改时,不会引起页面的刷新,因为friend对象不是响应式的
              obj.friend = markRaw({
                  name: '李四'
              })
              return {
                  obj,
                  ...toRefs(obj)
              }
          }
      

customRef(自定义Ref)

  1. 作用:customRef的作用是自定义一个Ref函数,可以使响应式数据的读取和修改时完成一些其他的操作。

    官方理解为:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

    customRef本质是一个函数,需要接受一个回调函数,回调函数需要返回一个包含set和get方法的对象,回调函数接受参数为track, trigger。

  1. 使用方法:

    function myRef(value) {
        return customRef((track, trigger) => {
            return {
                get() {
                    track()            //告诉vue这个value值时需要被追踪的
                    return value
                },
                set(newValue) {
                    value = newValue
                    trigger()        //告诉vue去更新页面
                }
            }
        })
    }
    
  2. 定义一个Ref防抖函数

    <template>
        <input type="text" v-model="str">
        <h2>{{ str }}</h2>
    </template><script>
    import { customRef } from '@vue/reactivity'
    export default {
        setup() {
            //定义一个自己的Ref函数
            function myRef(str, times) {
                let timer;
                return customRef((track, trigger) => {
                    return {
                        get() {
                            track()            //告诉vue这个str值时需要被追踪的,否则返回的一直是初始化的值,不会更新DOM
                            return str
                        },
                        set(a) {
                            clearTimeout(timer)
                            timer = setTimeout(() => {
                                str = a
                                trigger()        //告诉vue去更新页面
                            }, times)
    ​
                        }
                    }
                })
            }
            //使用自定义Ref函数生成响应式数据
            let str = myRef('hello', 500)
            return {
                str
            }
        }
    }
    </script>
    

provide与inject(祖孙通信)

  1. 作用:实现祖孙组件之间通信

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

    只能父组件提供数据给后代组件使用。

    提供的数据是响应式的,后代组件是可以修改这些数据的。

  3. 具体写法:

    1. 祖组件中:

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

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

响应式数据的判断

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

新组件

Teleport

  1. 作用:Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。

    移动到的位置可以写html、body或者是CSS选择器

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

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>