vue3初探

482 阅读8分钟

组合式API

我们先来看一个简单的vue2小例子

<template lang="pug">
div
    div {{ number }}
    button(@click="handleDoubleNumber") 倍增数字
    div {{ string }}
    button(@click="handleDoubleString") 倍增字符串
</template>
<script>
export default {
    data() {
        return {
            number: 1,
            string: 'hi',
        }
    },
    methods: {
        handleDoubleNumber() {
            this.number = this.number * 2
        },
        handleDoubleString() {
            this.string += this.string
        },
    },
}

code1.png

当我们继续新增功能时,代码块就会越来越多,导致书写以及阅读困难。相信小伙伴们有在公司碰到过要去修改前人很臭很长的代码,明明只要改动一点逻辑,但是却要捋清楚整个代码,让人着实焦躁。另外如果有部分功能有多个页面需要使用时,一般我们需要抽离出来使用mixin混入,但是mixin存在命名冲突,变量的来源未知,代码难以理解,难以维护等弊端。这种碎片化使得理解和维护复杂组件变得困难,选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

我们能够将与同一个逻辑关注点相关的代码配置在一起会更好,而这正是组合式API使我们能够做到的。我们使用组合式API来重写下上面的代码。

新建一个useDoubleNumber.js

import { ref } from 'vue'
export default function useDoubleNumber() {
    const number = ref(1)

    const handleDoubleNumber = () => {
        number.value = number.value * 2
    }

    return {
        number,
        handleDoubleNumber,
    }
}

新建一个useDoubleString.js

import { ref } from 'vue'
export default function useDoubleString() {
    const string = ref('hi')

    const handleDoubleString = () => {
        string.value += string.value
    }

    return {
        string,
        handleDoubleString,
    }
}

页面中使用

<template lang="pug">
div
    div {{ number }}
    button(@click="handleDoubleNumber") 倍增数字
    div {{ string }}
    button(@click="handleDoubleString") 倍增字符串
</template>
<script>
import { defineComponent } from 'vue'
import useDoubleNumber from './useDoubleNumber'
import useDoubleString from './useDoubleString'

export default defineComponent({
    setup() {
        const { number, handleDoubleNumber } = useDoubleNumber()
        const { string, handleDoubleString } = useDoubleString()
        return {
            number,
            handleDoubleNumber,
            string,
            handleDoubleString,
        }
    },
})
</script>

通过上面的改造,我们可以发现页面的逻辑非常的清晰,功能复用也非常的方便。这样代码的可读性和可维护性也大大的提高了。

setup组件选项

为了开始使用组合式API,我们首先需要一个可以实际使用它的地方。在Vue组件中,我们将此位置称为setup

新的setup组件选项在创建组件之前执行,一旦props被解析,并充当合成API的入口点。由于在执行setup的时候尚未创建组件实例,因此在setup选项中没有this

setup选项应该是一个接受propscontext的函数。此外,我们从setup返回的所有内容都将暴露给组件的其余部分(计算属性、方法、生命周期钩子等等)以及组件的模板。

Props

setup中的第一个参数就是propssetup函数中的props是props是响应式的,当传入新的prop时,它将被更新。因为props是响应式的,所以我们不能对其进行解构,这样会消除它的响应式。

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
    // 对props进行解构是错误的
    const { title } = props
  }
}

那如果我们既想对props进行解构又想保持它的响应性要怎么做呢,下文中我们将会介绍。

上下文

传递给setup函数的第二个参数是contextcontext是一个普通的JavaScript对象,它暴露三个组件的property:

export default {
  setup(props, context) {
    // Attribute (非响应式对象),相当于this.$attrs
    console.log(context.attrs)

    // 插槽 (非响应式对象),相当于this.$slots
    console.log(context.slots)

    // 触发事件 (方法),相应于this.$emits
    console.log(context.emit)
  }
}

上面我们有说到setup中是没有this的,那么在setup中我们如何获取组件的实例呢?

我们可以使用getCurrentInstance方法,该方法只能在setup或者生命周期钩子中使用。

<template>
    <p @click="handleClick">{{ num }}</p>
</template>
<script>
import { ref, getCurrentInstance } from 'vue'
export default {
    setup() {
        const num = ref(3)
        const instance = getCurrentInstance() // works
        console.log(instance)

        const handleClick = () => {
            getCurrentInstance() // doesn't work
        }
        return { num, handleClick }
    },
}
</script>

打印出来后,会发现有两个属性ctxproxy,如下(仅截取部分属性):

code2.png

这便是我们想要获取的实例的内容,两者唯一的区别是proxy属性是响应式的。

ref、reactive、toRefs

在vue2中数据都是定义在data中,那vue3怎么来定义响应式变量呢?

我们在上面的例子中看到了从vue中引入了一个ref函数声明了一个变量,该变量发生变化的时候我们的视图会随着更新。在vue3中,我们可以通过一个新的ref函数使任何响应式变量在任何地方起作用。ref接受参数并返回它包装在具有value的property的对象中,然后可以使用该property访问或者更改响应式变量的值,在template里可以直接访问.

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

一般ref是用来定义一个基本类型的响应式数据(也可用来定义引用类型的),定义引用类型的响应式数据采用reactive(不可用来定义基本类型)。

reactive返回对象的响应式副本。响应式转换时"深层"的-它影响所有嵌套property。

import { reactive } from 'vue'

const state = reactive({ counter: 0 })
console.log(state.counter) // 0

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

原本我们在上文中有提到不能对props进行解构,否则会失去其响应性。这个时候我们可以使用toRefs来将响应式对象转换为普通对象,其中结果对象的每个property都是指向原始对象相应property的ref

import { reactive } from 'vue'

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

const stateAsRefs = toRefs(state)

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

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

ref其实还有一个作用,那就是可以获取到标签元素和组件,在vue2中,我们获取元素都是通过给元素一个ref属性,然后通过this.$refs.xx来访问,但是在vue3中已经不适用了,我们来看看vue3中怎么获取元素的:

<template>
  <div>
    <div ref="el">div元素</div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
  setup() {
    // 创建一个DOM引用,名称必须与元素的ref属性名相同
    const el = ref(null)

    // 在挂载后才能通过 el 获取到目标元素
    onMounted(() => {
      el.value.innerHTML = '内容被修改'
    })

    // 把创建的引用 return 出去
    return {el}
  }
}
</script>

生命周期钩子

为了使组合式API的特性与选项式API相比更加完整,我们还需要一种在setup中注册生命周期钩子的方法。组合式API上的生命周期钩子与选项式API的名称相同,但前缀为on,如下:

选项式APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

这些函数接受一个回调函数,当钩子被组件调用时将会被执行:

import { onMounted } from 'vue'

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

watch 响应式更改

在vue2中,如果我们想要监听某个数据的改变从而执行一些操作的时候,我们一般会使用watch

export default {
  data() {
    return {
      counter: 0
    }
  },
  watch: {
    counter(newValue, oldValue) {
      console.log('The new counter value is: ' + this.counter)
    }
  }
}

在vue3中,我们可以使用从Vue导入的watch函数执行相同的操作。它接受3个参数:

  • 一个响应式引用或我们想要侦听的getter函数
  • 一个回调
  • 可选的配置选项
import { ref, reactive, watch } from 'vue'

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

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

它还可以使用数组同时侦听多个源:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

在vue3中,还有个侦听器watchEffect,它与watch的不同在于它是立即执行的,同时响应式追踪其依赖,并在依赖变更时重新运行该函数。但是watchEffect访问不了依赖变更之前的值。

import { ref, watchEffect } from 'vue'

const count = ref(0)

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

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

watchwatchEffect均在组件的setup()函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。也可以根据需要,显示调用返回值以停止侦听。

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

独立的computed属性

依赖于其它状态的状态,在vue中,我们一般是用计算属性来处理的,在vue2中,计算属性使用方法如下:

export default {
  data() {
    return {
      counter: 0
    }
  },
  computed: {
    doubleCounter() {
       return this.counter * 2
    }
  }
}

在vue3中,计算属性使用方法如下:

import { ref, watchEffect } from 'vue'

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

console.log(plusOne.value) // 2

plusOne.value++ // error

或者,它可以使用一个带有getset函数的对象来创建一个可写的ref对象

import { ref, watchEffect } from 'vue'

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

Teleport

Teleport这个单词是"传输"的意思,利用它,我们可以把子组件或者dom节点插入到任何我们想要插入的地方。

<template>
    <div>
        第一层包裹元素
        <div>
            第二层包裹元素
            <teleport to="#app"> 瞅瞅我在哪 </teleport>
        </div>
    </div>
</template>

我们打开控制台看下

code3.png

可以看到我们的元素跑到了id为app的元素下,这个暂时还不理解到底什么情况下可以使用。。。

片段

在vue2中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个<div>中。但是在vue3中,组件可以有多个根节点。

<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

非兼容的变更

全局API

vue2有许多的全局API和配置,这些API和配置可以全局改变Vue的行为。我们定义的应用只是通过new Vue()创建的根Vue实例。从同。一个Vue构造函数创建的每个根实例共享相同的全局配置。这会使得同一个页面上的多个"app"之间共享同一个Vue副本变的困难。

// 这会影响两个根实例
Vue.mixin({
  /* ... */
})

const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

为了避免这些问题,vue3中有个新的全局API:createApp,调用createApp返回一个应用实例。

import { createApp } from 'vue'

const app = createApp({})

应用实例暴露当前API的子集,任何全局改变Vue行为的API都会移动到应用实例下:

2.x全局API3.x实例API(app)
Vue.configapp.config
Vue.config.productionTipremoved
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
const app = createApp(MyApp)

app.component('button-counter', {
  data: () => ({
    count: 0
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>'
})

app.directive('focus', {
  mounted: el => el.focus()
})

// 现在所有应用实例都挂载了,与其组件树一起,将具有相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境
app.mount('#app')

所有其它不全局改变行为的全局API只能作为ES模块构建的命名导出进行访问,例如:

import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})

若多个应用之间需要共享配置,一种方式是创建工厂功能,如下所示:

import { createApp } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const createMyApp = options => {
  const app = createApp(options)
  app.directive('focus' /* ... */)

  return app
}

createMyApp(Foo).mount('#foo')
createMyApp(Bar).mount('#bar')

现在,Foo和Bar实例及其后代中都可以使用focus指令。

v-model

在vue2中,在组件上使用v-model相当于绑定valueprop和input时间:

<ChildComponent v-model="pageTitle" />

<!-- 简写: -->

<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

如果要将属性或事件名称更改为其它名称,则需要在ChildComponent组件中添加model选项(也可使用v-bind.sync):

<!-- ParentComponent.vue -->

<ChildComponent v-model="pageTitle" />
// ChildComponent.vue

export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    // 这将允许 `value` 属性用于其他用途
    value: String,
    // 使用 `title` 代替 `value` 作为 model 的 prop
    title: {
      type: String,
      default: 'Default title'
    }
  }
}

所以,在这个例子中v-model的简写如下:

<ChildComponent :title="pageTitle" @change="pageTitle = $event" />

在vue3中,自定义组件上的v-model相当于传递了modelValueprop并接收抛出的update:modelValue事件:

<ChildComponent v-model="pageTitle" />

<!-- 简写: -->

<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

若需要更改model名称,而不是修改组件内的model选项,那么现在我们可以将一个argument传递给model

<ChildComponent v-model:title="pageTitle" />

<!-- 简写: -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

这也可以作为.sync修饰符的替代,而且允许我们在自定义组件上使用多个v-model

<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 简写: -->

<ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>

<template v-for> 和非 - v-for 节点上 key 用法已更改

特殊的 key attribute 被用于提示 Vue 的虚拟 DOM 算法来保持对节点身份的持续跟踪。这样 Vue 可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。

Vue2建议在v-if/v-else/v-else-if 的分支中使用 key。若设置了key在 Vue3中仍能正常工作。但是我们不再建议在 v-if/v-else/v-else-if 的分支中继续使用 key attribute,因为没有为条件分支提供 key 时,也会自动生成唯一的 key

在 Vue 2中 <template> 标签不能拥有 key。不过你可以为其每个子节点分别设置 key。在 Vue 3 中 key 则应该被设置在 <template> 标签上。

v-if 与 v-for 的优先级对比

vue2版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。vue3版本中 v-if 总是优先于 v-for 生效。

slot具名插槽的语法

在vue2中,插槽的用法如下

// 子组件
<slot name="content" :data="data"></slot>
export default {
    data(){
        return{
            data:["喜羊羊","懒羊羊","美羊羊"]
        }
    }
}
// 父组件使用
<template slot="content" slot-scope="scoped">
    <div v-for="item in scoped.data">{{item}}</div>
<template>

在vue3中父组件使用变更如下:

// 父组件中使用
 <template v-slot:content="scoped">
   <div v-for="item in scoped.data">{{item}}</div>
</template>

// 也可以简写成:
<template #content="{data}">
    <div v-for="item in data">{{item}}</div>
</template>

注意,v-slot只能添加在<template>上,只有一种情况例外,那就是当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把v-slot直接用在组件上。

<todo-list v-slot:default="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>

也可以这样写:

<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>

不带参数的v-slot被假定对应默认插槽。默认插槽的缩写语法不能和具名插槽混用。

被移除的语法

1、vue3中不再支持数字(即键码)作为v-on的修饰符,例如:

<!-- 键码版本 -->
<input v-on:keyup.13="submit" />

<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />

2、$on$off$once 实例方法已被移除,应用实例不再实现事件触发接口。

在vue2中,我们经常使用$bus来进行组件通信,采用$on注册事件,$emit触发事件,$off移除事件,$once事件只执行一次。在Vue3中仅保留$emit,因为它用于触发父组件已声明方式附加的事件处理程序,其它的均移除。

3、过滤器已删除

在Vue2中我们经常使用过滤器,对我们要展示的数据进行格式处理等。在Vue3中,过滤器已移除,建议使用方法调用或者计算属性替换。

如果在应用中全局注册了过滤器,那就不方便在每个组件中使用计算属性或者方法来替换它了,这个时候我们可以采用全局属性。

// main.js
const app = createApp(App)

app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}

然后可以通过$filters对象修改所有的模板,像下面这样:

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>

参考文档

1、Vue3中文文档 - vuejs