vue3学习 --- Mixin 和 Composition API 介绍

9,741 阅读7分钟

Mixin

组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取

在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成

  • Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能
  • 一个Mixin对象可以包含任何组件选项 --- 其本质就是一个对象
  • 当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项

mixin定义者 --- /mixins/fooMixin.js

export default {
  data() {
    return {
      message: 'something in fooMixn'
    }
  },

  methods: {
    foo() {
      console.log('foo')
    }
  }
}

mixin使用者 --- App.vue

<template>
  <div>
    <h2>{{ message }}</h2>
    <button @click="foo">click me</button>
  </div>
</template>

<script>
import fooMixin from './mixins/fooMixin'

export default {
  name: 'App',

  // mixin本质上是一个对象
  // 一个组件可以混入多个,所以mixins所对应的值是一个对象
  mixins: [fooMixin]
}
</script>

合并规则

  1. 如果是data函数的返回值对象

    • 返回值对象默认情况下会进行合并
    • 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据
  2. 如何生命周期钩子函数

    • 生命周期的钩子函数会被合并到数组中,都会被调用

    • 先执行Mixin中对应的逻辑,在执行组件中对应生命周期钩子的逻辑

  3. 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象

    • 比如都有methods选项,并且都定义了方法,那么它们都会生效
    • 但是如果对象的key相同,那么会取组件对象的键值对

全局混入

如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin:

  • 全局的Mixin可以使用 应用app的方法 mixin 来完成注册
  • 一旦注册,那么全局混入的选项将会影响每一个组件
  • 全局混入的生命周期会优先执行 --- 因为最早被插入到数组中
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mixin({
  created() {
    console.log('全局混入')
  }
})

app.mount('#app')

extends

另一个和Mixins十分相似的属性,就是extends

vue组件只能实现单继承

基础组件

<template>
  <div>
    <!--
      如果使用继承,只能继承script中的内容
      无法继承模板中的内容
    -->
    <h1>Base</h1>
  </div>
</template>

<script>
export default {
  name: 'BaseComponent',

  data() {
    return {
      msg: 'base msg'
    }
  },

  methods: {
    foo() {
      console.log('foo')
    }
  }
}
</script>

使用继承组件

<template>
  <div>
    <h2>{{ msg }}</h2>
    <button @click="foo">click ms</button>
  </div>
</template>

<script>
import BaseComponent from './BaseComponent.vue'

export default {
  name: 'App',

  // 继承自BaseComponent, 对其进行扩展
  // 组件只能进行单继承
  extends: BaseComponent
}
</script>

Composition API

在Vue2中,我们编写组件的方式是Options API:

Options API的一大特点就是在对应的属性中编写对应的功能模块

比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命 周期钩子

但是这么做有一个很大的弊端:

  • 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中

  • 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散

  • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人)

Composition API(Vue Composition API ---> VCA)就是为了解决上述问题而存在的

Composition API能将同一个逻辑关注点相关的代码收集在一起

IbrvEO.gif

总结: option api 编写的代码相对比较分散,不利于后期的维护和抽取(代码复用)

composition api 可以将option api的代码增合到一起,增加了代码的可读性和可复用性

同时 方便代码的抽离,可以将代码抽离成为一个个的hook函数,以提高代码的可复用性

Setup

为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地

在Vue组件中,这个位置就是 setup 函数

参数

setup函数,主要有两个参数:

第一个参数:props:

  • 父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可 以直接通过props参数获取

  • 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义

  • 并且在template中依然是可以正常去使用props中的属性,比如message

  • 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取

  • setup函数中,是没有this关键字的

  • 因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可

第二个参数:context

另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性

  • attrs:所有的非prop的attribute
  • slots:父组件传递过来的插槽
  • emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件)
<template>
  <div>
    <h2>{{ message }}</h2>
  </div>
</template>

<script>
export default {
  name: 'Home',

  props: {
    message: {
      type: String,
      required: true
    }
  },

  setup(props, { emit }) {
    // 获取父组件传递过来的数据
    console.log(props)

    // 类似于vue2中的this.$emit方法 
    emit('homeClick')
  }
}
</script>
返回值

setup既然是一个函数,那么它也可以有返回值

setup的返回值可以在模板template中被使用, 也就是说我们可以通过setup的返回值来替代data选项

<template>
  <div>
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
export default {
  name: 'Home',

  setup() {
    return {
      msg: 'message'
    }
  }
}
</script>

但是setup函数的返回值默认情况下,只是一个普通变量,Vue并不会跟踪它的变化,来引起界面的响应式操作

<template>
  <div>
    <button @click="increment">{{ count }}</button>
  </div>
</template>

<script>
export default {
  name: 'Home',

  setup() {
    let count = 0

    // 此时点击了按钮后,setup中的count发生了改变,但是界面中的count并不会发现相应的改变
    const increment = () => count++

    return  {
      count,
      increment
    }
  }
}
</script>

Reactive

当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集

当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)

事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的

<template>
  <div>
    <button @click="increment">{{ state.count }}</button>
  </div>
</template>

<script>
import { reactive } from 'vue'

export default {
  name: 'Home',

  setup() {
    const state = reactive({
      count: 0
    })

    const increment = () => state.count++

    return  {
      state,
      increment
    }
  }
}
</script>

Ref

reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型

如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告

这个时候Vue3给我们提供了另外一个API: ref API

  • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源
  • 它内部的值是在ref的 value 属性中被维护的
<template>
  <div>
    <!-- 
			在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用 
		-->
    <button @click="increment">{{ count }}</button>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  name: 'Home',

  setup() {
    const count = ref(0)

    // 在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式
    const increment = () => count.value++

    return  {
      count,
      increment
    }
  }
}
</script>
<template>
  <div>
    <!-- 这里的info实际上是解包后的info,即原本的info.value -->
    {{ info.name }} --- {{ info.age }}
    <button @click="changeAge">change age</button>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  name: 'Home',

  setup() {
    // ref的参数也是可以是对象类型的 --- 不推荐 --- 后续无法使用toRefs或toRef进行解构操作
    const info = ref({ name: 'Klaus', age: 23 })

    // 取值的时候依旧是通过value属性来取
    const changeAge = () => info.value.age++

    return {
      info,
      changeAge
    }
  }
}
</script>

tips: 可以使用ref实现的代码,就不要使用reactive来进行实现

因为使用ref函数来实现的响应式数据是独立的,可以和业务逻辑进行整合

但是使用reactive声明的响应式数据,应该是那些相互之间联系比较强的数据

如果联系不强的数据使用reactive整合在一起的时候,会增加代码的耦合性,不利于代码的拆分和复用

// name和age是联系强的数据,但是其和count以及msg之间是没有任何联系的,所以不应该放在一个reactive对象中
const state = reactive({
  name: 'Klaus',
  age: 23,
  count: 0,
  msg: 'Hello World'
})

// 推荐修改为如下形式
// 这样,就可以方便将对应的数据整合到对应的hooks函数中
const info = reactive({
  name: 'Klaus',
  age: 23
})

const count = ref(0)
const msg = ref('Hello World')

readonly

我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个

响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?

Vue3为我们提供了readonly的方法

readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,只不过这个proxy的set方法被劫持,不能对进行值的修改操作)

// 普通对象
setup() {
  const counter = {
    count: 0
  }

  const readOnlyCounter = readonly(counter)

  const change = () => readOnlyCounter.count++ // error

  return {
    counter,
    readOnlyCounter,
    change
  }
}
// reactive对象
const counter = reactive({
  count: 0
})

const readOnlyCounter = readonly(counter)

const change = () => readOnlyCounter.count++ // error

return {
  counter,
  readOnlyCounter,
  change
}
// ref对象
const counter = ref(0)

const readOnlyCounter = readonly(counter)

const change = () => readOnlyCounter.value++ // error

return {
  counter,
  readOnlyCounter,
  change
}

在readonly的使用过程中,有如下规则:

  • readonly返回的对象都是不允许修改的
  • 但是经过readonly处理的原来的对象是允许被修改的
  • 其实本质上就是readonly返回的对象的setter方法被劫持了而已

此时,如果我们传递给其他组件数据时,希望其他组件使用我们传递的内容,但是不允许它们修改时,就可以使用 readonly了