vue3 setup组合api


本次记录vue3相关setup和响应/组合api的使用方法

1 setup

组合api全部都只能在setup中使用,相当于入口
setup中不能this,因为该函数调用早于data、computed、methods、DOM refs。只能访问:props、attrs、slots、emit

接收参数setup(props,context){}
其中普通js对象context包含字段:const {emit, attrs, slots, expose} = context,可解构,该context不是当前实例对象(getCurrentInstance()可以获取当前实例对象)

  • attrs和slots为有状态对象,不要解构
  • expose用于外部组件实例使用ref时访问本组件的方法或属性。expose({})

注意:props不能解构,否则失去响应性,可以使用toRefs转化整个props,或者toRef转化单独属性

返回值

  • setup返回一个对象,对象属性可以在模板中使用。(返回的对象所有属性会被合并到render函数的context中,所以模板才可以使用)
  • setup返回一个函数,该函数必须为渲染函数
<!-- 示例MyBook.vue -->
<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>

返回渲染函数:

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

2 reactive

包裹一个对象并返回该对象的响应式副本,基于ES2015的Proxy,与原对象不相等
注意:包裹的对象会进行深度代理

// 包裹普通对象
const obj = reactive({ count: 0 })
// 包裹混合对象,混合对象中的ref对象会被自动解包,此时二者都有响应式
const count = ref(1)
const obj = reactive({ count })
// 也可以后赋值
const obj = reactive({})
obj.count = count

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

被包裹对象值变化时(指通过回调函数进行变化,如果在setup中直接改变则两者都会导致UI变化),如果模板引用了该对象,则会进行UI更新
如果是普通对象,则UI不会改变

<template>
  <h1>{{ state1 }}</h1>
  <button @click="change1">1</button>
  <h1>{{ state2 }}</h1>
  <button @click="change2">1</button>
</template>

<script lang="ts">
import { defineComponent, reactive, shallowReactive } from "vue";

export default defineComponent({
  abc: 1,
  setup() {
    const s1 = { name: "pp", a: { b: { c: 1 } } };
    const s2 = { name: "pp", a: { b: { c: 1 } } };
    const state1 = reactive(s1);
    const state2 = shallowReactive(s2);

    const change1 = () => {
      state1.a.b.c = 22;
    };
    const change2 = () => {
      state2.a.b.c = 999;
    };

    // 如果通过此种方式进行改变,则UI会刷新
    // setTimeout(() => {
    //   state1.a.b.c = 2222;
    //   state2.a.b.c = 333
    // }, 2000);

    return {
      state1,
      state2,
      change1,
      change2,
    };
  },
});
</script>

2.1 shallowReactive

只转换自身为响应式对象,对于自身包含的属性的值不做转换!即响应一层
与 reactive 不同,任何使用 ref 的 property 都不会被代理自动解包。

3 readonly

包裹一个对象(普通、响应式、ref),返回其只读代理,也是深度的,同样ref会自动解包

const raw = {
  count: ref(123)
}

const copy = readonly(raw)

console.log(raw.count.value) // 123
console.log(copy.count) // 123

3.1 shallowReadonly

只转换自生为只读,不嵌套执行转换属性的值
与 readonly 不同,任何使用 ref 的 property 都不会被代理自动解包。

4 isProxy isReactive isReadonly

isProxy:检查对象是否为Proxy代理对象(reactive和readonly包裹的返回都是proxy代理对象)

isReactive:检查对象是否最底层是响应式的(无论之后是否包裹了readonly等)

isReadonly:检查对象是否被readonly包裹过(包过即true)

区分这三者很简单:可以设想reactive包裹后在目标对象上添加了一个隐藏属性A,readonly包裹后会在目标对象上添加隐藏属性B,而这几种是用来判断是否包含对应隐藏属性的。
普通对象->只读->响应:由于被只读包裹后无法添加响应标记,故响应判定false,只读true
普通对象->响应->只读:由于在响应对象上添加新的只读标记,故响应和只读都可检测true

5 toRaw

该api包裹的对象会被解包到创建时的普通js对象,并获取其引用,操作该引用会改变所有对象,但不触发响应的代理副作用。

const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true

注意:只解包到是一个普通对象即停止!如果该普通对象的某些属性为代理对象,该属性不会被处理

6 markRow

该api用于标记一个普通js对象,表明其不接受转换成proxy对象,返回普通对象本身,标记只存在于第一层!!即无嵌套标记!!
即:被该api标记了的对象通过响应式或只读包裹无效!

跳过无需代理的大对象可以提高性能

不进行嵌套标记意味着:第一层标记为无嵌套对象,但是可以把第二层及更深层包装成响应式,容易混淆,所以尽量避免这种情况发生。

7 Refs相关

7.1 ref

使用ref包裹的基础数据类型会变成响应式,ref 对象仅有一个 .value property,指向该内部值。

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

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

注意:如果ref接收一个基础类型值,则value返回该基础类型值,如果接收的是一个对象,则value返回的是该对象的reactive包裹后的值

7.1.1 模板$refs

属于模板的一个属性:<div ref="a">123</div>
可以定义在普通DOM标签也可以定义在组件标签上

// 模板:<Father ref="father"/>
// 使用:this.$refs.father,可以获取组件实例的一个代理对象

注意:$refs不能在模板中和计算属性中使用

7.1.2 模板refs

在组合式API中即setup中使用模板ref和普通响应式ref使用方式一样

<template>
  <div ref="root">root</div>
</template>

<script>
...
setup(){
  const root = ref(null);
  onMounted(()=>console.log(root.value))
  return{root}
}
...
</script>

原因是:返回的ref如果和虚拟节点或组件的ref同名,则该节点会被绑定到ref值中,只有在挂载完和更新完才能拿到最新值。

结合v-for使用ref

<template>
  <div v-for="(item,index) of list"
    :key="index"
    :ref="el=>{if(el) rlist[index]=el}"
  >{{item.name}}</div>
</template>

<script>
...
setup(){
  const rlist= ref([]);
  return{rlist,list:[1,2,3]}
}
...
</script>

7.2 unref

语法糖:val = isRef(val) ? val.value : val

7.3 toRef

从某个响应式对象中创建一个响应式属性

一般应用场景为:响应式的某个属性为基本数据类型,所以按值传递,在处理过程中,无法影响原对象,所以使用toRef创建一个对象,这样该属性就可以按照引用进行传递了

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 由于传递的是引用,所以改变一个另一个也会变

7.4 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

7.5 isRef

判断是否为ref对象

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

7.6 shallowRef

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的,只跟踪value引用变化。浅层ref。

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

此时更改foo的值会触发更新

  setup() {
    const info = shallowRef({name:1})
    watchEffect(()=>{
      console.log(info.value.name)
    })
    info.value={name:2} // 会触发effect
  },

此时更改foo值的嵌套属性值不会触发更新

  setup() {
    const info = shallowRef({name:1})
    watchEffect(()=>{
      console.log(info.value.name)
    })
    info.value.name=2 // 不会触发effect 可以使用trigger(info)手动触发
  },

7.7 triggerRef

当ref对象作为watchEffect的依赖时,其value的变化会触发effect,当想要手动触发依赖于该ref的effect,可以使用该函数

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

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

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

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

7.8 customRef

接收两个参数,track和trigger,返回一个对象包括:get和set属性
在取值前调用track,在设置值后调用trigger,以让响应系统工作

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() // 赋值结束调用trigger通知系统
        }, delay)
      }
    }
  })
}

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

8 computed

接受一个getter函数,并返回一个不可变的响应式ref对象

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

console.log(plusOne.value) // 2

plusOne.value++ // 错误

或者接收一个包含get和set属性的对象。

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

9 watchEffect

第一次执行setup函数,就会执行一次
每次依赖被改变也会执行一次

特别注意:对于数组或者对象这种引用数据类型,其中的改变无法被观察到!!!除非显式使用数组的长度或对象的某个变化属性,否则只能使用深拷贝!(可以使用lodash的_.cloneDeep(state))或者解构!

9.1 简单使用

自动收集依赖,如果依赖改变则会执行effect,该api返回一个stop函数,执行该函数可以停止该effect的触发

const count = ref(0)

const stop = watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)
setTimeout(() => {
  stop()
}, 1000)

9.2 清理函数

如需清理effect:在【依赖改变时】或者【组件卸载时】清理副作用,需要传入一个清理注册函数onInvalidate
该函数传入的回调函数执行时机为:

  1. 依赖改变后,先调用清理在重新执行effect
  2. 组件卸载前,先执行清理,再卸载
  3. stop回调前,先执行清理,再停止

使用场景有很多:如input输入框的debounce功能

const rid = ref(id)

watchEffect(async(onInvalidate) => {
  // 依赖id改变时需要获取新的数据
  const token = await getUserInfo(rid.value)
  onInvalidate(() => {
    // 在获取新数据时,取消上一次的请求,即清理上一次的作用
    token.cancel()
  })
})

watchEffect可以直接接收一个异步函数作为回调,而react不可以(需要在内部创建一个异步函数,然后同步调用)

9.3 effect在生命周期中的执行时机

默认行为:watchEffect中的effect会在组件update【之前】被调用!

可选参数

watchEffect(()=>{},{flush:'pre/post/sync'})
// pre 默认模式,effect在组件更新前执行
// post effect在组件更新后执行
// sync 同步模式,即所有effect强制同步,效率低下

同步模式即:

  setup() {
    const info = shallowRef({name:'pp'})
    console.log('1')
    watchEffect(()=>{
      console.log(info.value.name)
    },{flush:'sync'})
    console.log('2')
    info.value={name:'qq'}
    console.log('3')
  },

打印输出:
1
pp
2
qq
3

9.4 watchEffect中访问DOM

处于setup中的watchEffect无法访问DOM,因为此时组件还未挂载,如需访问需要放在onMounted生命周期中
原因:在setup中第一次effect执行时,组件尚未挂载,无法获取有关DOM的数据,如ref。组件挂载完毕,DOM上的ref被赋值,重新触发effect,此时可以获取数据。

9.5 watch debugger

watchEffect接收的第二个参数是一个options对象,包含onTrackonTrigger两个函数,用于追踪依赖和回调触发的调试

watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrack(e) { // 跟踪依赖和依赖改变后重新跟踪
      debugger
    },
    onTrigger(e) { // 依赖改变,回调触发
      debugger
    }
  }
)

10 watch

同watchEffect区别:
1 第一次setup不会执行,依赖改变才会执行
2 明确指定依赖
3 可以获取依赖前后变化

第一个参数:接收一个getter函数,或一个ref
第二个参数:副作用回调
第三个参数:调试options,可以深度观察:deep:true,解决数组和对象无法被观察到内部改变的问题(也可以深拷贝等)。可以立即执行:immediate:true,解决首次渲染不执行的问题。
其余行为同watchEffect一样

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

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

// 侦听多个ref
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", ""]

注意多个同步更改只会触发【一次】侦听器。

如果需要同步多次触发更改,有两个方法

  1. flush:'sync',效率低
  2. 在两次同步更改中间添加异步调用:await nextTick()
const changeValues = async () => {
  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
  await nextTick()
  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}

11 provide/inject

对于嵌套多层的组件,想要传递参数,可以使用该api
provide:上层组件提供数据
inject:下层组件使用数据

11.1 选项API用法

// provide 上层组件提供选项
   // 静态数据-非响应式
   provide: {
     user: 'John Doe'
   }
   // 动态数据-非响应式
  provide() {
    return {
      todoLength: this.todos.length
    }
  }
  // 动态数据-响应式
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }

// inject 下层组件使用数据
  inject: ['user']

注意:如果provide使用的是【选项式API】,即使传递ref,值依然不是响应式!即接收方数据不会改变!

11.2 组合API用法

组合API每次provide一个值:provide(key,value)
每次inject一个值:inject(key,default)

// 上层组件
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }

// 下层组件
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }

// 响应式
  setup(){
    const color = ref("red");
    provide('color',color)
    return {
      color
    }
  },

注意:使用组合api的provide,只需要传递的数据是响应式的即可

在更改传递值时,【建议】在上层组件定义该值的同时,定义一个更改该值的方法,并传递出去,不建议在子组件中直接更改该值!可以在传递时限定readonly

  setup(){
    const color = ref("red");
    const changeColor = (newColor)=>color=newColor;
    provide('color',readonly(color))
    provide('changeColor',changeColor)
  },
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值