Vue3.0深入浅出系列之Composition API

564 阅读8分钟

前言

最近公司正式将Vue3.0作为前端框架,自己也参与了2个Vue3.0项目的开发,所以准备出一个Vue3.0的学习系列,提到Vue3.0,就不得不说composition API,所以本文就作为Vue3.0系列的开篇之作。

image.png

解决什么痛点?

使用过Vue2.0通常知道,用(data、computed、methods、watch)组件选项来组织逻辑很有效,然而,随着组件变大,逻辑复制时,导致组件难以阅读合理解和后续维护性非常复杂,特别是接手别人代码时,往往需要来回跳转阅读。基于此composition api应用而生。对比图如下:

对比.png

setup

新的setup选项在组件创建之前执行,一旦props被解析,就将作为Composition API的入口。

setup中避免使用this,因为无法获取组件实例;同理,setup的调用发生在data、property、computed property、methods被解析之前,同样在setup中无法获取。

setup接收两个参数:

  • props (props是响应式的,不能使用ES6解构,如需解构使用toRefstoRef
  • context (普通对象,可解构,包含3个属性。context.attrs、context.slots、context.emit
import { toRefs,roRef } from 'vue'
props: {
  name: String,
  age: Number
},
setup(props,context){
  console.log(props.name)
  
  //toRefs 不会创建一个 ref ,而toRef 可以:
  const {name}=toRefs(props)
  const {age}=toRef(props,'age')
  
  
  // Attribute (非响应式对象)
  console.log(context.attrs)

  // 插槽 (非响应式对象)
  console.log(context.slots)

  // 触发事件 (方法)
  console.log(context.emit)
}

结论

执行 setup 时,组件实例尚未被创建。因此,你只能访问以下 property:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

ref 与 reactive

在vue3中,官方提供了ref()reactive()两种方式声明响应式数据,接下来我们将详细介绍各自的用法以及两者的区别。

ref函数

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

import { ref } from 'vue'

const age = ref(0)
console.log(age.value) // 0

age.value++
console.log(age.value) // 1

而为何将值封装到一个对象里面?

官方给出了分析和解释:

在 JavaScript 中,Number 或 String 等基本类型是通过值而非引用传递的。封装成对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。

gif

结论

ref 为我们的值创建了一个响应式引用

reactive函数

返回对象的响应式副本,响应式转换是“深层”的——它影响所有嵌套 property。

import { reactive } from 'vue'

const obj=reactive({
   name:'Vue3',
   age:1
})

reactive 将解包所有深层的 refs,同时维持 ref 的响应性。

const age = ref(1)
const obj = reactive({ age })

// ref 会被解包
console.log(obj.age === age.value) // true

// 它会更新obj.age
age.value++
console.log(age.value) // 2
console.log(obj.age) // 2

// 它也会更新 age  ref
obj.age++
console.log(obj.age) // 3
console.log(age.value) // 3

当将 ref 分配给 reactive property 时,ref 将被自动解包。

const age = ref(1)
const obj = reactive({})

obj.age = age

console.log(obj.age) // 1
console.log(obj.age === age.value) // true

区别

reactiveref 都是用来定义响应式数据的 reactive更推荐去定义复杂的数据类型, ref 更推荐定义基本类型

refreactive 本质我们可以简单的理解为ref是对reactive的二次包装, ref定义的数据访问的时候要多一个.value

使用ref定义基本数据类型,ref也可以定义数组和对象。 reactive 不能代理基本类型,例如string、number、boolean

toRef与toRefs

前面我们提到Props不能使用使用ES6解构,同样,响应式对象也不能使用ES6解构,但是我们想要解构但有不要失去响应怎么办?这个时候toReftoRefs就来了。

toRef 可以用来为源响应式对象上的某个 property 新创建一个 ref

toRefs 用于将一个 reactive 对象转化为属性全部为 ref 对象的普通对象。

import { defineComponent, reactive, toRefs, toRef } from "vue";
export default defineComponent({
  setup () {
    const person = reactive({
      name: 'icey',
      age: 18,
      height: 160
    })

    const ageRef = toRef(person, 'age')
    const { age } = toRefs(person)

    ageRef.value++
    console.log(person.age) // 19
    console.log(age.value)  //19

    person.age++
    console.log(ageRef.value) // 20
    console.log(age.value)  //20
  }
})

区别

  • toRef 会为源对象不存在的 property生成ref
  • toRefs 只会为源对象中包含的 property 生成 ref。 当你要将 prop 的 ref 传递给复合函数时,toRef 很有用: 即使源 property 不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用。

computed 计算属性

  • 接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
import { defineComponent, computed, ref } from "vue";
export default defineComponent({
  setup () {
    const count = ref(1)
    const plusOne = computed(() => count.value + 1)

    console.log(plusOne.value) // 2
    plusOne.value++
    console.log(plusOne.value) // 2
  }
})
  • 使用具有 get 和 set 函数的对象来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

watch 与watchEffect

在官方文档中关于watch与watchEffect讲解中,扑面而来的是一个感觉相当重要,可是有不太理解的词---副作用(side effect)。为了更好的吸收这块知识,首先我们解释下什么是函数副作用?

副作用(side effect)

对于所有初学者,Vue2到Vue3最直观的变化就是Composition API-几乎所有的Vue2 options方法都被放到了setup函数里。这个较大的风格转变通俗的讲,就是就是从基于对象的编程(OOP)转向了函数式编程(FP)。

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。借用网上一张图,图片来源

vue3中响应式数据的变更造成的其他连锁反应,以及后续逻辑,这些连锁反应都叫副作用。副作用不一定是不被需要的。它可以是获取数据、事件监听或订阅、改变应用状态、修改 DOM、输出日志等等。

watchEffect

watchEffect 根据响应式状态自动应用和重新应用副作用。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

watchEffect(effect, [options])

参数说明:

  • effect:侦听副作用传入的函数,该函数可以接收一个 onInvalidate函数作入 参,用以清除副作用。

  • options:包含3个属性的对象,3个属性均为可选项

  • flushpre| post | sync。用来改变副作用的刷新时机。

  • onTrack: 将在响应式 property 或 ref 作为依赖项被追踪时被调用。

  • onTrigger: 将在依赖项变更导致副作用被触发时被调用。 onTrack 和 onTrigger 只能在开发模式下工作。

侦听与停止侦听

import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
  setup () {
    const count = ref(0)
    const stop = watchEffect(() => {    //页面进入时执行一次watchEffect
      console.log(count.value)
    })
    // -> logs 0

    setTimeout(() => {
      count.value++                  //依赖变化时,执行watchEffect
      // -> logs 1
    }, 1000)

    setTimeout(() => {
      stop()                        //停止侦听
      count.value++                 //不在执行watchEffect
      // 控制台无logs输出
    }, 1000)
  }
})

清除副作用(onInvalidate )

它是effect函数中传入的参数,用于清除effect产生的副作用,onInvalidate只作用于异步函数,并且只有在如下两种情况下才会被调用:

  • 副作用即将重新执行时(页面进入不执行)
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时) 换言之,onInvalidate(fn)传入的回调会在 watchEffect 重新运行或者 watchEffect 停止的时候执行。
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
  setup () {
    const count = ref(0)

    const stop = watchEffect((onInvalidate) => { // 首次进入执行副作用,不执行onInvalidate回调
      console.log('watchEffect', count.value)
      
      onInvalidate(() => {
        console.log('onInvalidate', count.value)
      })
    })
      //-->logs: watchEffect 0
   
    setTimeout(() => {
      count.value++   // 执行onInvalidate回调,执行副作用
    }, 1000)
     //-->logs: onInvalidate 1
     //         watchEffect  1
 
    setTimeout(() => {
      stop()     // 执行onInvalidate回调,不执行副作用 
      //-->logs: onInvalidate 1
    }, 1000)
  }

})

控制台输出

image.png

上述例子只是为了验证onInvalidate执行时机,真实的使用需求是: 有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。

副作用刷新时机

Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。

如果有个watchEffect监听了2个变量count和count1,你觉得监听器会调用2次?当然不会,Vue会合并成1次去执行,代码如下,console.log只会执行一次:

import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
  setup () {
    const count = ref(0)
    const count1 = ref(1)
    
    watchEffect(() => {
      console.log('watchEffect', count.value, count1.value)
    })
  }
})
  • count 会在初始运行时同步打印出来
  • 更改 count 时,将在组件更新前执行副作用。

如果需要在组件更新(例如:当与模板引用一起)重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象 (默认为 'pre'):

  • flushpre 在组件更新前执行副作用。

  • flushpost,在组件更新后触发,这样你就可以访问更新的 DOM。 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。

  • flush : sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。

Watch

watch API 完全等同于组件侦听器 property。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

侦听单个数据源

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

侦听多个数据源

const firstName = ref('')
const lastName = ref('')

watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues)
})

firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]

如果你在同一个方法里同时改变这些被侦听的来源,侦听器仍只会执行一次。注意多个同步更改只会触发一次侦听器。

setup() {
  const firstName = ref('')
  const lastName = ref('')

  watch([firstName, lastName], (newValues, prevValues) => {
    console.log(newValues, prevValues)
  })

  const changeValues = () => {
    firstName.value = 'John'
    lastName.value = 'Smith'
    // 打印 ["John", "Smith"] ["", ""]
  }

  return { changeValues }
}

通过更改设置 flush: 'sync',我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行

const changeValues = async () => {
  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
  await nextTick()
  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}