阅读 137
【前端】一网打尽──Vue3 Composition-api新特性

【前端】一网打尽──Vue3 Composition-api新特性

写在前面

在体验Vue3之前,我们先来了解一下Vu3到底有哪些亮点之处。

总共有6大特点:

  • Performance(性能比vue2的runtime快2倍左右)
  • Tree shaking support(按需打包模块)
  • Better TypeScript support(更好的TS代码支持)
  • Composition API(组合API)
  • Custom Renderer API(自定义渲染API)
  • Fragment, Teleport, Suspense

1. vue3的起源setup

  1. 理解:vue3.0的组合api舞台是setup
  2. setup所有的数据方法等都配置在setup中
  3. setup有两种返回值:
    1. 若返回一个对象,则对象中的属性、方法,在模板中均可直接使用
    2. 若返回一个渲染函数,则可以自定义渲染内容
  4. 注意:
    1. vue3.0尽量不要和vue2的配置混用
    2. vue2配置(data、methods、computed等)是可以访问vue3的setup属性方法
    3. setup中不可访问vue2的配置(data、methods、computed等)
    4. 如果重名,则vue3为准
  5. setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的属性(后期也可以使用suspense和异步组件的配合得到promise)

2. ref函数

  • 作用:用于定义一个响应式数据
  • 语法:const name = ref(initValue)
    • 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)
    • js中操作数据:name.value
    • 模板中读取数据:不需要value,直接<div>{{name}}</div>
  • 注意:
    • 接受的数据:基本类型、对象类型
    • 基本数据类型:响应式是靠Object.defineProperty()getset进行设置和取值操作
    • 对象类型的数据:内部使用了vue3中的新函数--reactive函数(其实就是proxy代理)

3. reactive函数

  • 作用:用于定义一个对象类型的响应式数据(基本类型不要用它,要用ref函数)
  • 语法:const 代理对象(proxy) = reactive(源对象)接收一个对象或数组,返回一个代理对象(proxy的实例对象,简称proxy对象)
  • reactive定义的响应式是『深层次的』,对象中的所有对象都是响应式的
  • 内部基于es6的proxy实现,通过代理对象内部数据进行操作

经过reactive处理的对象是不能使用解构语法的,因为会失去响应式特性,可以使用toRefs将reactive每个属性都转成ref响应式,这样就可以进行响应式处理。

ref和reactive的差异

reactive就是用于将对象数据转为响应式,与vue2的observable类似,而ref用于获得单独或为基础数据类型转为响应式。

为什么在vue3中会有两个将数据转为响应式数据的api,我们来进行详细说明:

  • ref接受一个内部值并返回一个响应式且可变的ref对象,对象具有指向内部值的单个property.value
  • reactive接受一个对象,并返回对象的响应式副本,其将对象中的每个元素都转为ref对象

简而言之,reactive可以将对象数据转为响应式,可以将对象中的每个元素都转为ref响应式数据;而ref不仅可以将对象数据转为响应式,还可以处理基础数据类型(string、boolean等)。

之所以会有此差异,那时因为vue3中的响应式是基于proxy实现的,而对于proxy的target必须是引用数据类型,即存放在堆内存中通过指针进行引用的对象。为什么是代理的引用数据类型,那是因为简单数据类型每次赋值都是全新的数据,根本无法进行代理,因此难以实现简单数据类型的响应式。

如果我们想要获取简单数据类型的响应式,应该如何做呢?

vue3中也考虑到这点,可以通过ref进行简单数据类型的响应式处理。ref通过创建内部状态,将值挂到value上,因此ref生成的对象要通过.value获取使用。而重写get/set获得的监听,同时对对象的处理也依赖了reactive的实现。

正是如此,ref既可以处理简单数据类型,又可以将引用数据类型进行响应式处理。在实践中,我们应该避免将reactive当作vue2的data在顶部将所有变量进行声明,而是应该结合具体的应用和逻辑进行就近声明。

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
...
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
...
复制代码

4. Vue3中的响应式原理

  • vue2的响应式原理:
    • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)
    • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行包裹)
  Object.defineProperty(data,"count",{
    get(){},
    set(){}
  })
复制代码
  • 存在问题:
    • 新增属性、删除属性,界面不会同步更新
    • 直接通过下表修改数组,界面不会自动更新
  • vue3的响应式原理
    • 通过proxy代理:拦截对象中任意属性的变化,包括:属性值的读写、属性值的添加、删除等
    • 通过reflect反射:对被代理对象的属性进行操作

5. reactive和ref的差异

  • 定义数据
    • ref:用于定义基本数据类型
    • reactive:用于定义引用数据类型
    • 注意:ref也可以用于定义引用数据类型,内部会自动通过ractive转为代理对象
  • 原理
    • ref:通过Object.defineProperty()的set和get属性实现响应式(数据劫持)
    • reactive:通过Proxy来实现响应式(数据劫持),并通过Reflect操作愿对象内部的数据
  • 使用
    • ref:ref定义的数据需要通过.value进行操作,模板读取数据时不需要使用.value
    • reactive:reactive定义的数据无需.value,即可进行设置和读取

6. 计算属性和监视

  • 与vue2中的computed配置功能一致
  • 写法:
import {computed} from "vue";
setup(){
  const sum = computed(()=>{
    return num1 + num2;
  })
}
复制代码
import {computed,reactive} from "vue"
setup(){
  const person = reactive({
    firstName:"wen",
    lastName:"bo"
  })
  //简写
  let fullName = computed(()=>{
    return person.firstName + "-" + person.lastName;
  });
  
  //完整
  let fullName = computed(()=>{
    get(){
      return person.firstName + "-" + person.lastName;
    },
    set(value){
      const newArr = value.split("-");
      person.firstName = nameArr[0];
      person.lastName = nameArr[1];
    }
  })
}
复制代码

7. watch函数

  • 与vue2中的watch配置功能一致
  • 注意:
    • 监视reactive定义的响应式数据时,oldValue无法正确获取,强制开启了深度监视(deep配置失败)
    • 监视reactive定义的响应式数据中某个属性时,deep配置有效

let person = reactive({
  name:"wenbo",
  age:18,
  job:{
    job1:{
      salary:20
    }
  }
})

// 情况一:监视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定义的响应式数据,则强制开启了深度监视
  此时的deep不生效
*/
watch(person,(newValue,oldValue)=>{
  console.log("perosn变了",newValue,oldValue);
},{immediate:true,deep:false})

//情况四:监视reactive所定义的一个响应式数据中的某个属性
watch(()=>person.age,(newValue,oldValue)=>{
  console.log("person的age变化了",newValue,oldValue)
})

//情况五:监视reactive所定义的一个响应式数据中的某些属性
watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{
  console.log("person的name或age变化了",newValue,oldValue)
})

//特殊情况-监听的对象的属性还是个对象,此时deep生效,可以进行深度监听
watch(()=>person.job,(newValue,oldValue)=>{
  console.log("person的job变化了",newValue,oldValue)
},{deep:true})
复制代码

注意:

  • watch监听reactive定义的对象有五种情况

  • watch监听ref定义的响应式数据有两种情况:

    • ref定义的是基本类型数据,此时不能对.value进行监听。如const num = ref(0),此时对num.value监听的话相当于直接对num的值(0)进行监听了,想想0有啥变化的。
    • ref定义的是对象数据类型,此时ref处理后的数据是ref实例的。要监听对象的变化,需要对对象的.value进行监听,因为ref对象.value是借助reactive处理的响应式数据proxy。

8. watchEffect函数

  • watch的套路:既要指明监视的属性,又要指明监视的回调
  • watchEffect的套路:不用指明监视的哪个属性,监视的回调中用到哪个属性,就监视哪个属性
  • watchEffect有点类似computed:
    • computed注重的是计算的值,即回调函数的返回值,所以必须要写返回值
    • watchEffect注重的是计算的过程,即回调函数的函数题,所以不用写返回值
let person = reactive({
  name:"wenbo",
  age:18,
  job:{
    job1:{
      salary:20
    }
  }
})

// warchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调
watchEffect(()=>{
  const x = person.name
  const y = person.job.job1.salary
  console.log("watchEffect触发了")
})
复制代码

9. 自定义hooks函数

  • hook函数:本质上是一个函数,把setup函数中是用的组合api进行封装
  • 类似于vue2中mixins
  • 优点:复用代码、使得setup中的逻辑更清晰易懂
<div>pageX:{{point.pageX}},pageY:{{point.pageY}}<div>
  

复制代码

hooks文件 usePoint.js

import {reactive} from "vue"

const handleClickPoint = ()=>{
  //实现鼠标“打点”相关数据
  let point = reactive({
    pageX:0,
    pagey:0
  })
  
  //实现鼠标“打点”相关方法
  const handlePoint = (event)=>{
    point.pageX = event.pageX;
    point.pageY = event.pageY;
    console.log(event.pageX,event.pageY)
  }
  //实现鼠标打点的相关周期函数
  onMounted(()=>{
    window.addEventListener("click",handlePoint)
  })
  
  onBeforeUnmounted(()=>{
    window.removeEventListener("click",handlePoint)
  })
}
复制代码

10. toRef函数

  • 经过ref、reactive处理的对象不能直接使用解构进行处理,否则就会失去响应式特性
  • 作用:创建一个ref对象,其value值指向另一个对象中的某个属性
  • 语法:const name = toRef(person,"name")
  • 应用:要将响应式对象中的某个属性单独提供给外部使用时
  • 扩展:toRefs与toRef功能一致,但是可以批量创建多个ref对象,语法:toRefs(person)

11. VUE3生命周期

其它组合式api

1. shallowReactive与shallowRef

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

2. readonly与shallowReadonly

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

3. toRaw与markRaw

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

4. customRef

  • 作用:创建一个自定义ref,并对其依赖项跟踪和更新触发进行显式控制
  • 示例:

  <template>
    <input type="text" v-model="str"/>
    <h1>当前的值:{{str}}</h1>
  </template>

    <script>
    import {ref, customRef} from "vue";
    export default {
      setup(){
        // 自定义一个ref:myRef
        const myRef = (value)=>{
          return customRef((track,trigger)=>{
            return{
              get(){
                console.log(`有人从myRef中读取了此数据${value}`);
                track();
                return value;
              },
              set(newValue){
                console.log(`有人把myRef的数据改为了新数据${newValue}`);
                value = newValue;
                trigger();//通知vue去重新解析模板
              }
            }
          });
        }
        const str = myRef("hello");
        return{
          str
        }
      }
    }
    </script>

复制代码

5. provide与inject

  • 作用:实现祖孙组件间的通信
  • 应用场景:父组件有一个provide选项提供数据,子组件有一个inject来获取使用数据

6. 响应式数据的判断

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

vue3中使用hook注意点

  1. hook中存在异步问题

其实在使用中可以发现,其实hook本质上就是进行抽取的函数,灵活性比较高,但是在涉及到异步逻辑时考虑不周全就会导致很多问题。hook是可以覆盖异步情况的,但是必HTML须在setup当中执行时返回有效对象不能被阻塞。

通常异步具有两种风格:

  • 外部没有其它依赖,只是交付渲染的响应变量。对于此种情况,可以通过声明、对外暴露响应变量,在hook中使用异步修改的方式。

  • 外部具有依赖,需要在使用侧进行加工。对于此种情形,可以通过对外暴露Promise的方式,使得外部获取到同步操作的能力。

  1. this的问题

由于setup处于生命周期的beforeCreate和created阶段之间,因此不能获取到this。当然我们可以通过setup的第二个参数context获取到类似与this的部分能力,但是对于想操作路由、vuex等能力则受到了限制,为此最新的router@4、vuex@4都提供了组合式的api。

由于vue2底层限制使得无法使用这些hook,为此可以通过引用实例的方式获取一定的操作能力,也可以通过getCurrentInstance获得组件实例上挂载的对象。

虽然组合式api的响应式原理是通过Object.defineProperty改写属性的,与vue的响应式原理相同,但是在具体实现方式上存在差异,因此在setup中与vue原生的响应式并不互通。正因此,导致即使拿到相应的实例也没有办法监听它们的响应式,只能通过在选项配置进行使用。

vue3中常见的组件

1. Fragment组件

在vue2中:组件必须有一个根标签

在vue3中:组件可以没有根标签,内部会将多个标签包裹在Fragment虚拟元素中

好处:减少标签层级,减少内存占用

2. Teleport

Teleport时倚重能够将组件html结构移动到指定位置的技术

语法:<teleport to="目标位置"></teleport>

main.vue

<div name="modal"></div>
<Modal :isOpen="isOpen" @closeModal="closeModal"/>
<button @click="openModal" >open modal</button>
复制代码

modal.vue

 <teleport to='#modal'>
    <div class="modal-box" v-if="isOpen">
      <slot>this is a modal</slot>
      <button @click="handleCloseModal">close</button>
    </div>
  </teleport>
复制代码

3. suspense

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

使用步骤:

  1. 异步引入组件

  2. 使用suspense包裹组件,并配置好defaultfallback


import {defineAsyncComponent} from "vue"
const child =defineAsyncComponent(()=>import("./components/child.vue"))
复制代码

参考文章

写在最后

我是前端小菜鸡,感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。

更多最新文章敬请关注笔者掘金账号一川萤火和公众号前端万有引力

文章分类
前端
文章标签