vue composition api 一个迷人的新特性

1,274 阅读15分钟

介绍

什么是组合式API?

我们知道,通过vue的组件化思想,我们可以将界面中重复的部分连同功能一起提取成可复用的组件或者代码段。但是,当你的项目非常大的时候,一个组件内就可能包含了各种业务逻辑,而传统的vue2组件模板编写中,业务的开发逻辑是写在script标签之下,而script标签又有很多划分功能的模块,比如data,methods,computer,watch等,如果说一个功能代表一个颜色块,组合起来的业务逻辑视图就像👇一样

640.webp 显然,这对于不是编写改组件的人来说,是很难去阅读和理解,找一个逻辑要上翻下翻多次,无法体现组件化的可维护性和灵活性

然而,尤大大也考虑到这一点,为了拥有更好的可维护性,让开发者更关注于业务功能开发,在更新vue3到时候,给其加了个新特性,也就是vue composition api组合式api,组合式api的逻辑代码统一写在setup(后面会介绍),组合起来的业务逻辑就会像👇

640 (1).webp 这看起来就很有种想去阅读的感觉了!这也是为什么vue3出版后更多人爱上的原因,当然更爱尤大大😳

Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以单独的功能逻辑拆分成单独的文件,接下来将重点认识它。

组合式API基础

setup组件选项

setup是vue3新增的一个选项,是composition api的入口

setup的执行机制

setup的执行机制是在beforecreate之前执行

export default defineComponent ({
    beforeCreate() {
        console.log("----beforeCreate----");
    },
    created() {
        console.log("----created----");
    },
    setup() {
        console.log("----setup----");
    },
})

image.png 由此可见,setup是在beforecreate之前执行,需要注意的是,因为执行setup尚未创建组件实例,因此在setup选项中没有this,也不能使用this

参数

使用setup时,它将接受2个参数

  1. props
  2. context

Props

setup中第一个参数就是props,他是响应式的,所以当传入新的props值时,他将被更新。 由于他是响应式的,所以不能进行ES6的解构赋值,这会消除他的响应式

你可以利用toRefs函数来完成这个操作

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)

  console.log(title.value)
}

如果title是可选的prop,则传入的props中可能没有title。这时候,toRefs将不会为title创建一个ref。你需要使用toRef替代他

import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

Context

context时setup函数的第二个参数,context是一个普通的js对象,他会暴露三个属性:

  1. attrs
  2. slot
  3. emit 三个属性分别对应vue2中的$attrs, $slot, $emit

注意,他不是this,只是有点仿造this那意思而已

context是js对象,所以不是响应式的,可以进行解构

export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}

attrsslots是有状态的对象,它们会随着组件本身更新而更新,所以应该避免对它们进行解构,并始终以attr.xslots.x的方式引用property。

注意,与props不同,attrsslots是非响应式的。如果你打算根据attrs或slots更改应用的副作用,那么应该在onUpdated生命钩子函数中使用

访问组件的property

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

  • props
  • attrs
  • slots
  • emit 换句话说,你无法访问到:
  • data
  • computer
  • methods

组件模板使用

只用setup函数返回到对象或者是setup参数中的props对象才可以在模板里面访问到,包括这些参数的子属性等可以访问到

<template>
  <div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>

<script>
  import { ref, reactive } from 'vue'

  export default {
    props: {
      collectionName: String
    },
    setup(props) {
      const readersNumber = ref(0)
      const book = reactive({ title: 'Vue 3 Guide' })

      // 暴露给 template
      return {
        readersNumber,
        book
      }
    }
  }
</script>

注意,从 setup 返回的refs在模板中访问时是被自动浅解包的,因此不应在模板中使用 .value

使用渲染函数

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:

import { h, ref, reactive } from 'vue'

export default {
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    // **请注意这里我们需要显式调用 ref 的 value**
    return () => h('div', [readersNumber.value, book.title])
  }
}

ref与reactive的响应式数据

很多初学者经常会认为ref用来为基础数据类型添加响应式,而reactive为对象添加响应式,其实不是这样的,ref也可以为对象添加响应式,让我们一起来看看它们的具体用法

ref

ref接受参数并将其包裹成一个带有valueproperty的对象中返回,然后我们就可以通过改property访问或者更改响应式变量的值,当然该响应式变量在任何地方都起作用

import { ref } from 'vue'

const counter = ref(0)

console.log(counter) // { value: 0 }
console.log(counter.value) // 0

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

将值封装成一个对象,看似没有必要,其实可以保持js不同数据类型的一个统一,在js中numberstring类型也都是通过值而非引用传递的。这样讲任何值封装成一个对象,我们就可以在整个应用中安全的传递他,不必担心其响应式的丢失

换句话说,ref为我们的值创建了一个响应式的引用,在整个组合式API中会经常使用引用的概念

reactive

reactive接受一个对象并返回该对象的响应式副本,响应式的转换式深层的,影响着所有嵌套property。

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

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

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

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

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

当将ref分配给reactiveproperty时,ref将被自动解包

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

obj.count = count

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

生命周期钩子函数

同vue2中相比,vue3的生命周期钩子发生了一些小变动: image.png

vue3中对于生命周期钩子函数的调用,必须要先引入import才能使用

watch响应式更改

watch函数用来侦听特定的数据源,并在回调函数中执行副作用,默认时惰性的,只有数据源发生改变才会触发执行函数,它接受三个参数:

  • source:一个响应式引用或者返回值的getter函数
  • callback(new,old)
  • option?: 支持deep,immediate,flush watch可以通过数组侦听多个数据源
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", ""]

watch侦听响应式对象

当响应式对象时深层嵌套对象的时候,需要将watch的第三个参数的deep设置为true,才能监听到深度嵌套对象中property的变化

const state = reactive({ 
  id: 1,
  attributes: { 
    name: '',
  }
})

watch(
  () => state,
  (state, prevState) => {
    console.log(
      'deep',
      state.attributes.name,
      prevState.attributes.name
    )
  },
  { deep: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"

上面这个例子中,deep设置为true能监听到name到变化,然而,保存了上一个状态prevstate的值,由于是复杂数据类型,所以打印出来上一个值还是Alex。(后续会深入研究其运行的原理),为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。这可以通过诸如lodash.cloneDeep这样的实用工具来实现。

import _ from 'lodash'

const state = reactive({
  id: 1,
  attributes: {
    name: '',
  }
})

watch(
  () => _.cloneDeep(state),
  (state, prevState) => {
    console.log(
      state.attributes.name,
      prevState.attributes.name
    )
  }
)

state.attributes.name = 'Alex' // 日志: "Alex" ""

我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想停止某个监听,可以调用watch()的返回值,watch函数默认返回一个停止他监听的函数。

const stopWatchRoom = watch(() => state.room(newType, oldType) => {
    console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
    // 停止监听\
    stopWatchRoom()
}, 3000)

vue3 composition API中引入了一个新的函数watchEffect,具体用法会另行说明

独立的computer计算属性

有时我们需要依赖于其他状态的状态---在vue中,这是用组件的计算属性处理的,composition API也提供了计算computer函数,它接受getter函数并为getter返回的值返回一个不可变的响应式ref对象。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // error

它也可以使用一个带有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

provide 和 inject

温习一下vue2中provideinject的使用,父组件用provide提供数据,不必知道给哪些儿子提供;子组件同inject接收数据,也无须了解数据从哪来。这样一传一接收,形成多级嵌套组件之间的传值,需注意,通过provide和inject的数据不是响应式的,所以我们需要利用composition API给其添加响应式 在composition API中provideinject都是写在setup中并且需要提前导入,这样可以使我们能够调用provide或者inject来定义每一个property

provide

provide函数允许你通过两个参数定义property:

  1. name(string 类型)
  2. value
<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
</script>

inject

inject函数需要2个参数:

  1. 要inject的property的name
  2. 默认值(可选)
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

响应式

添加响应式

我们可以利用refreactive给变量等添加上响应式后,再给provide提供

setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
  }

修改响应式property

当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。 例如,在需要更改用户位置的情况下,我们最好在 MyMap 组件中执行此操作

setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)

    return {
      location
    }
  },
  methods: {
    updateLocation() {
      this.location = 'South Pole'
    }
  }

然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。

//myMap.vue
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation)
  }
myMarket.vue
setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }

最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly

常见API使用说明

除了上面所介绍的一些API,composition API也提供了许多其他的功能的api来供我们对特定的情况 做处理,下面会详细介绍一下这些api的使用

响应式基础api

readonly

接收一个对象或者ref并返回原始对象的只读代理。代理是深层的,也就是任何深层嵌套属性都是只读的

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 用于响应性追踪
  console.log(copy.count)
})

// 变更 original 会触发依赖于副本的侦听器
original.count++

// 变更副本将失败并导致警告
copy.count++ // 警告!

isProxy

检查对象是否是由reactive或者readonly创建的proxy

isReactive

检查对象是否是由reactive创建的代理

如果该代理是由reactive创建的,并且外层还包含了readonly,也返回true

isReadonly

检查对象是否是由readonly创建的代理

toRaw

返回reactivereadonly代理的原始对象。可临时读取数据避免被代理跟踪等的开销。

markRaw

标记一个对象,使其永远不能被代理。返回对象本身

markRaw和后面的shallowXXX API使你可以选择性的退出默认的深度响应式/只读模式,并将原始的,未被代理的对象潜入到状态图中。原始选择退出是会产生在根级别,如果将嵌套在内的,未被标记的的原始对象添加进响应式对象,会得到原始对象被代理后到对象。这样就会导致执行一个依赖,即会使用原始对象又会使用被代理后到对象

const foo = markRaw({
  nested: {}
})

const bar = reactive({
  // 虽然 `foo` 被标记为原始,但 foo.nested 不是。
  nested: foo.nested
})

console.log(foo.nested === bar.nested) // false

shallowReactive | shallowReadonly

创建一个响应式/只读代理,跟踪自身property到响应性,但不执行嵌套对象的深层响应式转换(暴露原始值) 与reactive不同,使用shallowReactive中,任何使用ref的属性不会被自动解包,也就是自己的访问value值

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

Ref

unref

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

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) // unwrapped 现在一定是数字类型
}

toRef

可以用来为源响应式对象上的某个属性新创建一个ref,然后,ref可以被传递,他会保持对其源property的响应式链接

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

当你要讲prop的ref传递给复合函数时,toRef很有用:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

即使源属性不存在,toRef也会返回一个可用的ref,这使得他在使用可选的prop时特别有用,可选prop并不会被toRefs处理。

toRefs

将响应式对象转换为普通对象,其中结果对象的每一个属性都是指向原始对象相应属性的ref。

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

toRefs经常可以用来解构赋值

customRef

创建一个自定义的ref,并对其依赖跟踪和更新触发进行显示控制。该函数接收tracktrigger函数作为参数,并且返回一个带有get和set的参数

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)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}

shallowRef

创建一个跟踪自身.value变化的ref,但不会使其值也变成响应式,也就是创建独立的响应式值

const foo = shallowRef({})
// 改变 ref 的值是响应式的
foo.value = {}
// 但是这个值不会被转换。
isReactive(foo.value) // false

triggerRef

手动执行与shallowRef关联的副作用

const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这不会触发副作用,因为 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 记录 "Hello, universe"
triggerRef(shallow)

watchEffect

其实watch已经能实现大部分功能了,但是vue3推出了一个watchEffect,我个人觉得这个是用来更好的补充watch的其余功能,至于什么作用,还是看你需要吧,个人理解

watchEffect可以监听响应式状态自动执行和重新应用副作用(watch是惰性的),他会立即执行传入的函数并追踪依赖,更新依赖时重新触发

watchEffect在组件setup()函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件销毁时自动停止。 当然我们也可以像watch一样通过他的返回值来停止

清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。

在执行数据请求时,副作用函数往往是一个异步函数:

const data = ref(null)
watchEffect(async onInvalidate => {
   onInvalidate(() => { /* ... */ }) // 我们在Promise解析之前注册清除函数
  data.value = await fetchData(props.id)
})

我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。

副作用刷新时机

Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update 执行:

<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      const count = ref(0)

      watchEffect(() => {
        console.log(count.value)
      })

      return {
        count
      }
    }
  }
</script>

在这个例子中:

  • count 会在初始运行时同步打印出来
  • 更改 count 时,将在组件更新前执行副作用。 如果需要在组件更新(例如:当与模板引用一起)后重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象 (默认为 'pre'):
// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)

侦听器调试

onTrack 和 onTrigger 选项可用于调试侦听器的行为。

  • onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。

这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:

watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrigger(e) {
      debugger
    }
  }
)

onTrack 和 onTrigger 只能在开发模式下工作。

参考文档

vue3中文文档:v3.cn.vuejs.org/guide/compo…