11.Vue3组合式Api你真的了解吗?

885 阅读11分钟

1.0 组合式Api详解

上一章以及我们已经初步了解了什么是组合式api,这一章将会详细的介绍各类新的Api与写法

1.1 setup初识

setup将是我们使用组合式API写法风格的起点,将来的组合API的代码,基本上都在这

本质上是一个生命周期钩子函数,并且在beforeCreate钩子前执行,即组件创建前执行

所以:

  • setup中不能使用this,此时还未创建实例,this指向的undefined
  • 模板中需要使用的数据与函数,需要在setup中返回

App.vuelog一下查看

<template>
  <div @click="sing()">{{title}}</div>
</template>

<script>
export default {
  // 组合Api的起点, 将来的组合API的代码,基本上在这
  setup () {
    console.log('setup', this);
    let title = 'hello Vue3'
    const sing = () => {
      console.log('啦啦啦,我是买包的小行家');
    }
    return { title, sing }
  },
  beforeCreate () {
    console.log('beforeCreate', this);
  }
}
</script>

可以观察到

image.png

1.2 组合式api的生命周期

上面的案例中我们使用了beforeCreate,这是vue2的生命周期,现在依旧有用,但是不建议继续在vue3中使用了,我们来看看vue3的变化吧

阶段生命周期钩子生命周期钩子组合式api生命周期钩子组合式api生命周期钩子
初始化beforeCreatecreatedsetup(创建实例前)
挂载beforeMountmountedonBeforeMount(挂载DOM前)onMounted(挂载DOM后)
更新beforeUpdateupdatedonBeforeUpdate(更新组件后)onUpdate(更新组件后)
销毁beforeUnmountunmountedonBeforeUnmount(卸载销毁前)onUnmounted(卸载销毁后)

原本的八个钩子精简为了七个钩子函数,变化比较大的是setup,原有的两个初始化的生命周期函数更换为了在初始化更前面触发的创建实例前setup(),并且后面的六个新的钩子函数函数除了名字改变了以外,需要写在setup函数中

  • 新增setup作为组合式APi的起点,可以在其中写上最新的钩子函数
  • 使用新的钩子函数需要在vue中导入,并使用新钩子的参数为一个箭头函数,并在箭头函数内写上该钩子中执行的代码
  • 以上的生命周期钩子都可以使用,区别是前面的是传统写法,与setup同级,后面是组合式API写法,需要写在setup内
  • 组合式API的生命周期可以重复执行,从上往下依次执行,更利于分离代码

我们看看实际开发中如何使用组合式API的生命周期吧

<template>
  <div class="main">根组件</div>
</template>

<script>
import { onBeforeMount, onMounted } from 'vue'
export default {
  name: 'App',
  setup () {
    onBeforeMount(() => {
      console.log('onBeforeMount', document.querySelector('.main'));
    }),
      onMounted(() => {
        console.log('onMounted', document.querySelector('.main'));
      })
      ,
      // 在vue3中可以定义多个相同的钩子函数,实现不同的逻辑,以分离代码结构
      onMounted(() => {
        console.log('onMounted2', document.querySelector('.main'));
      })
  },
}
</script>

image.png

可以看到,在组合式APi中,生命周期是可以重复触发的,在vue2中的生命周期,则通常是会后面生命周期覆盖前面的同名生命周期函数,这也是为了更便利我们将逻辑代码分离,一个逻辑的代码写在一块,执行顺序与代码顺序有关,从上往下执行

1.3 reactive函数(复杂数据类型响应式)

我们之前在setup中定义了一些数据并成功的展示在了插值表达式中,但是该数据并不是响应式数据,当我们使用方法对其进行修改时,并不会触发视图的响应,在vue2中我们在data中定义的数据直接就是响应式数据,那么我们在vue3的组合式API写法中又应该如何实现响应式数据呢?

要实现响应式数据,我们只需要使用vue3为我们提供的reactive函数即可

  • reactive函数可以定义一个复杂数据类型,成为响应式数据

代码演示 App.vue

<template>
  <div>
    <ul>
      <li>{{obj.name}}</li>
      <li>{{obj.age}}</li>
      <li>{{obj2.name}}</li>
      <li>{{obj2.age}}</li>
      <li>{{num}}</li>
      <h1>{{msg.say}}</h1>
      <button @click="updateName">改名</button>
      <button @click="changeMsg">改msg</button>
    </ul>
  </div>
</template>

<script>
import { reactive } from 'vue'
export default {
  name: 'App',
  setup () {
    // 普通数据
    const obj = {
      name: '王叔叔',
      age: 18
    }
    let num = 0
    // 响应数据 
    const obj2 = reactive({
      name: '王叔叔',
      age: 18
    })
    const msg = {
      say: 'lalala'
    }
    // 修改名字
    const updateName = () => {
      obj2.name = '隔壁老王'
      obj.name = '隔壁老王'
      num++
      console.log(num);
    }
    const changeMsg = () => {
      msg.say = '妙啊'
    }
    return { obj, updateName, obj2, num, changeMsg, msg }
  }
}
</script>

动画.gif 这里我们能看到一个奇怪的现象:

  • obj并没有进行reactive响应式绑定,但是对obj的修改视图却响应了
  • 简单数据num没有进行绑定,我们能发现数值是在变化的,但是当obj引起的视图更新并没有同时更新num的的视图
  • 我们优先修改了obj的name值再去修改msg的值msg的值就不再响应了

我们可以总结为

  • 当Diff算法比对reactive绑定的复杂数据类型无需更新Dom时,那么将不会更新其余未绑定的复杂数据类型视图
  • 当Diff算法比对reactive绑定的复杂数据类型需要更新Dom时,那么将更新所有的复杂类型的视图

这一点当我们将name修改为随机值的时候就能发现

     // 修改名字
    const updateName = () => {
      obj2.name = Math.random() * 10
      obj.name = Math.random() * 10
      num++
      console.log(num);
    }

动画.gif

上面的现象与先后顺序无关,与Dom是否需要更新有关

  • 当然实际开发,出于性能考虑,还是建议大家将与DOM渲染有关的数据统一使用reactive绑定

1.4 Ref函数(简单数据类型响应式)

上个章节的reactive函数,我们已经学会了如何将复杂类型数据对象,数组添加响应式,本章节我们将学习如何将简单类型数据字符串等添加响应式

该章节我们将介绍三个api

  • ref---->定义简单类型响应式数据
  • toRef---->转换响应式对象某个属性为单独响应式数据,并且转换后属性与之前响应式对象属性指向同一地址,即值属关联的
  • toRefs---->转换响应式对象中所有属性为单独响应式数据,对象成为普通对象,并且转换后属性与之前响应式对象属性指向同一地址,即值属关联的

1.4.1 ref函数

因为vue3采用了Proxy来作为响应式的实现,而proxy仅接受对象类型作为入参,这才有了ref解决简单数据类型的响应式,如果传入 ref 的是一个对象,内部也会调用 reactive 方法进行深层响应转换,想要解决可以采用shallowRef进行浅层转换

  • 修改获取值的时候需要使用.value属性
  • template模板中使用ref的值可以省略.value
    // 定义响应式数据
    const name = ref('隔壁老王')
    // 修改响应式数据
    name.value = '法外张三'

我们可以通过一个demo了解一下

<template>
  <div class="container">
    <div>{{name}}</div>
    <button @click="updateName">修改数据</button>
  </div>
</template>
<script>
import { ref } from 'vue'
export default {
  name: 'App',
  setup () {
    const name = ref('隔壁老王')

    const updateName = () => {
      name.value = '法外张三'
    }

    return { name, updateName }
  }
}
</script>
<style scoped lang="less"></style>

动画.gif

1.4.2 toRef函数

基于响应式对象上的一个属性,创建一个对应的ref对象,两者之间保持同步,修改ref属性会带动响应式数据的修改,反之亦然

使用步骤如下

  1. 定义一个响应式对象
  2. 基于其中一个属性创建一个ref响应式简单数据
  3. 通过.value修改数据
// 定义一个响应式对象
const msg = reactive({
      name: '隔壁老王',
      age: 27
    })
    // 基于其中一个属性创建一个ref响应式简单数据
    let name = toRef(msg, 'name')
    // 修改这个数据
    name.value = '老王'

来一个demo试验一下

<template>
  <div>
    <ul>
      <h1>{{name}}</h1>
      <button @click="changeName">修改名字</button>
    </ul>
  </div>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
  name: 'App',
  setup () {
    const msg = reactive({
      name: '隔壁老王',
      age: 27
    })

    // 模板中仅仅需要name
    let name = toRef(msg, 'name')
    // 通过toRef转换城了响应式数据保证的对象, value是存放值的位置,修改必须通过value
    const changeName = () => {
      name.value = '老王'
      console.log('修改了名字', name);
    }

    return { name, changeName }
  }
}
</script>

动画.gif

1.4.3 toRefs函数

转换响应式对象中所有属性为单独响应式数据(每一个属性都是使用toRef转为ref对象),对象成为普通对象,并且转换后属性与之前响应式对象属性指向同一地址,即值属关联的

使用步骤如下

  1. 定义一个响应式对象
  2. 使用toRefs将所有属性转为响应式数据
  3. 通过.value或者原响应式对象修改数据
    // 定义一个响应式对象
    const obj = reactive({
      name: '隔壁老王',
      age: 28
    })
    // 使用toRefs将所有属性转为响应式数据
    const { name, age } = toRefs(obj)
    // 通过.value或者原响应式对象修改数据
    obj.age = 18
    name.value = '法外张三'

做一个demo试验一下吧

<template>
  <div class="container">
    <div>{{name}}</div>
    <div>{{age}}</div>
    <button @click="updateName">修改数据</button>
  </div>
</template>
<script>
import { reactive, toRef, toRefs } from 'vue'
export default {
  name: 'App',
  setup () {
    // 1. 响应式数据对象
    const obj = reactive({
      name: '隔壁老王',
      age: 28
    })
    console.log(obj);
    const { name, age } = toRefs(obj)
    console.log(obj);

    const updateName = () => {
      obj.age = 18
      name.value = '法外张三'
    }

    return { name, age, updateName }
  }
}
</script>
<style scoped lang="less"></style>

动画.gif

1.5 计算属性computed

在之前的文章我们已经详细的了解过计算属性了,在vue3中有一定的变化,但是区别不大

使用场景: 当我们需要依赖现有的响应式数据得到一个新的数据的时候就可以使用计算属性,或者是更新计算属性时影响现有的响应式数据就使用计算属性

计算属性和我们之前学习的vue2是相似的

分为两种写法

  1. 只读写法,即该写法的计算属性时不可修改的
    let num = ref(6)
    let price = computed(() => num.value * 66)
  1. 可修改写法,该写法下可以更新计算属性并带动响应式数据修改
    let sanLian = computed({
      set (val) {
        // forEach方法会直接改变原数组
        arr.forEach(obj => obj.check = val);
      },
      get () {
        // every方法 当数组里面有一个不符合条件 ,直接返回false
        return arr.every(obj => obj.check === true);
      },
    })

同样的我们通过一个demo详细的体验一下计算属性

<template>
  <div>
    <span>一键三连:</span>
    <input type="checkbox" v-model="sanLian" />
    <ul>
      <li v-for="(val, index) in arr" :key="index">
        <input type="checkbox" v-model="val.check" />
        <span>{{ val.name }}</span>
      </li>
    </ul>
    <button @click="addNum">再打赏一个火箭</button>
    <div>打赏{{num}}个火箭 共 {{price}}元</div>
  </div>
</template>
<script>
import { computed, reactive, ref } from 'vue'
export default {
  name: 'App',
  setup () {
    // 打赏 只读的计算属性定义
    let num = ref(6)
    let price = computed(() => num.value * 66)
    const addNum = () => {
      num.value++
    }

    // 一键三连 可以修改的计算属性
    let arr = reactive([
      {
        name: "点赞",
        check: false,
      },
      {
        name: "转发",
        check: false,
      },
      {
        name: "收藏",
        check: false,
      },
    ])
    let sanLian = computed({
      set (val) {
        // forEach方法会直接改变原数组
        arr.forEach(obj => obj.check = val);
      },
      get () {
        // every方法 当数组里面有一个不符合条件 ,直接返回false
        return arr.every(obj => obj.check === true);
      },
    })
    return { arr, sanLian, num, price, addNum }
  }
}
</script>
<style scoped lang="less"></style>

动画.gif

1.6 侦听器watch

watch是用来定义侦听器的,监听数据的变化,并可以在回调函数中做出一些基于业务的操作

watch()默认是懒侦听的,仅在侦听源发生修改时才执行回调函数

使用方式是调用watch函数并传入各个执行侦听

第一个参数是侦听器的。来源可能是如下多种:

  • 监听ref定义的响应式数据
  • 监听reactive定义的响应式数据
  • 监听多个响应式数据数据
  • 监听reactive定义的响应式数据中某个属性的变化
  • 监听深层次的reactive定义的响应式数据

第二个参数是侦听到变化后调用的回调函数。这个回调函数接受三个参数:新值旧值以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,用于清除无效副作用

当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

第三个可选的参数是一个对象,支持以下这些选项:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
  • flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()
  • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
  1. 监听ref定义的响应式数据
    const count = ref(0)
    watch(count, (newVal,oldVal)=>{
      console.log(newVal,oldVal)
    })
  1. 监听reactive定义的响应式数据
    const obj = reactive({
      name: 'zs',
      age: 18
    })
    watch(obj, (newVal,oldVal)=>{
      console.log(newVal,oldVal)
    })
  1. 监听多个响应式数据数据
   watch([count, obj], (newVal, oldVal) => {
      console.log('监听多个数据改变了', newVal, oldVal)
    })
  1. 监听reactive定义的响应式数据中某个属性的变化
    watch(() => obj.name) => {
      console.log('name数据改变了')
    })
  1. 监听深层次的reactive定义的响应式数据,或想要修改监听器默认配置时
   watch(() => obj.name) => {
      console.log('name数据改变了')
    }, {
      // 需要深度监听,只有在修改name时候才触发
      deep: true,
      // 想默认触发
      immediate: true
    })

1.7 ref属性

获取DOM或者组件实例可以使用ref属性,但是写法和vue2.x有区别

  1. 获取单个DOM
    • 定义一个由ref定义得空的响应式数据
    • setup中返回该数据,并为想要获取的Dom元素的ref属性绑定上返回的响应式数据
    • 此时空的响应式数据.value就已经是Dom了
    <div ref="pig">我是个pig</div>
    
    const pig = ref(null)
    return { pig }
  1. 获取v-for遍历的DOM或组件
    • 定义一个空数组,接收所有的li
    • 定义一个函数,向空数组pushDom
    • 将函数返回,动态绑定在Dom的:ref属性上
   <ui>
      <li v-for="i in 4" :key="i" :ref="setLi">{{i}}</li>
    </ui>
     let li = []
    const setLi = (el) => {
      li.push(el)
    }
    return { pig, setLi }

在demo里面看看效果吧

<template>
  <div class="container">
    <div ref="pig">我是个pig</div>

    <ui>
      <li v-for="i in 4" :key="i" :ref="setLi">{{i}}</li>
    </ui>
  </div>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
  name: 'App',
  setup () {
    const pig = ref(null)
    onMounted(() => {
      console.log(pig.value);
      console.log(li);
    })
    // 获取遍历的元素
    let li = []
    const setLi = (el) => {
      li.push(el)
    }
    return { pig, setLi }
  }
}
</script>

image.png

来看看我的其他章节吧,正在长更中

从0到Vue3企业项目实战【01.Vue的基本概念与学习指南】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【02.了解并理解Vue指令以及虚拟Dom】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【03.vue基本api入门】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【04.从vue组件通讯到eventBus以及vuex(附mock接口与axios简单实践)】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【05.vue生命周期】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【06.refref与nextTick使用】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【07.vue动态组件,组件缓存,组件插槽,子组件直接修改props,自定义指令看这一篇就够了】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【08.Vue路由基础】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【09.Vue路由进阶】 - 掘金 (juejin.cn)

10.Vue3.0初识 - 掘金 (juejin.cn)

11.Vue3组合式Api你真的了解吗? - 掘金 (juejin.cn)