Vue3 - Composition API

前言:Composition API 官网的解释是,一组附加的,基于函数的api,允许灵活组合组件的逻辑。本文首先讲述vue3相比vue2变更的地方,然后再逐一讲解常用的Cmposition API


篇幅较长,如果想直接查看composition api,而不想看vue3与vue2的变更, 点这里

安装指引

下面讲述vue3如何安装和使用:

安装

vite

npm init vite-app hello-vue3 # 或者 yarn create vite-app hello-vue3

vue-cli v4.5.0 以上版本

npm install -g @vue/cli # 或者 yarn global add @vue/cli
vue create hello-vue3
# 然后选择vue3的预设

使用

import { createApp } from 'vue'
import MyApp from './MyApp.vue'

const app = createApp(MyApp)
app.mount('#app')

Vue2 与 Vue3 的差异

下面讲述Vue3破坏性变更的地方

全局API

全局api已迁移至 createApp()创建的实例下

2.x全局API3.x实例API(app)
Vue.config.production已经删除
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
use
const app = createApp(MyApp)
app.use(VueRouter)
component & directive
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()
})

app.mount('#app')
provide & inject
// 在入口文件
app.provide('guide', 'Vue 3 Guide')

// 在子组件中
export default {
  inject: {
    book: {
      from: 'guide'
    }
  },
  template: `<div>{{ book }}</div>`
}
plugin
const plugin = {
  install: app => {
    app.directive('focus', {
        mounted: el => el.focus()
    })
  }
}
tree shaking

tree shaking主要作用是打包项目时会将用不到的方法不打包进项目中,这样得以优化项目体积。

import { nextTick } from 'vue'

nextTick(() => {
  // something DOM-related
})

以前的api改为es模块导入,以及vShow,transition等内部助手方法,都启用了摇树优化(tree shaking),只有实际用到的才会被打包进去。

模板指令

下面讲述模板指令相关破坏性变更的地方:

v-model

v-model propevent 默认的名称已经更改

<ChildComponent v-model="pageTitle" />

<!-- 上下等价 -->
<!-- value-> modelValue -->
<!-- input-> update:modelValue -->
<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

同时,v-model.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"
/>
key
  1. keyv-if/v-else/v-else-if 分支上不再需要,因为vue会自动生成唯一key
  2. 如果你手动提供key,则每个分支必须使用唯一key,你不再可以有意使用它来强制重用分支。
  3. <template v-for> key应放置在<template>标签上(而不是其子标签上)。
<!-- Vue 2.x -->
<template v-for="item in list">
  <div v-if="item.isVisible" :key="item.id">...</div>
  <span v-else :key="item.id">...</span>
</template>

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div v-if="item.isVisible">...</div>
  <span v-else>...</span>
</template>
v-if & v-for

如果用于同一元素,v-if则优先级高于v-for

  • 在2.x中,当在同一元素上使用v-if和v-for时,v-for将优先使用。
  • 在3.x中,v-if优先级始终高于v-for。
v-bind

v-bind的绑定顺序将影响渲染结果。

<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>

<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>

规则是,绑定相同属性,在后边的具有最高优先级。(在2.x中,单个属性的优先级,比v-bind高)。

ref

ref的用法发生改变,需要一个变量来接收dom对象的引用, 不再自动合并到$refs

<div v-for="item in list" :ref="setItemRef"></div>
export default {
  data() {
    return {
      itemRefs: []  // 存储节点引用
    }
  },
  methods: {
    setItemRef(el) {
      this.itemRefs.push(el)
    }
  },
  beforeUpdate() {
    this.itemRefs = []
  },
  updated() {
    console.log(this.itemRefs)
  }
}

Components

组件方面发生变更的地方

functional components

函数式组件:

  • 在2.x中,函数式组件的性能提升现在可以忽略不计,因此我们建议仅使用有状态组件
  • 只能使用接收一个普通的函数来创建函数式组件,参数包括props和context(即,slots,attrs,emit)
  • <template> 与 单文件组件中的 functional选项已经删除

通过函数创建:

import { h } from 'vue'

const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}

DynamicHeading.props = ['level']

export default DynamicHeading

通过单文件组件创建

<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
    // v-on="listeners" listeners现在作为$attrs的一部分传递,可以删除
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
async components

异步组件发生改变的地方:

  • 新增了defineAsyncComponent方法定义异步组件
  • component 选项重命名为 loader
  • 加载函数本身不接收resolve和reject传递参数,必须返回Promise

2.x中:

const asyncPage = () => import('./NextPage.vue')

3.x中:

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 无选项
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// 带选项
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

Render Function

渲染函数,这个改变不会影响使用<template>的用户,不用render api的可以略过。

render函数api

有以下改变:

  • h函数以全局导入的方式替代render函数参数传递的方式
  • vnode的props的格式变的扁平化

2.x中:

export default {
  render(h) {
    return h('div')
  }
}

3.x中:

import { h, reactive } from 'vue'   // 手动导入h

export default {
  setup(props, { slots, attrs, emit }) {
    const state = reactive({
      count: 0
    })

    function increment() {
      state.count++
    }

    // 返回一个渲染函数
    return () =>
      h(
        'div',   // 节点名
        {
          onClick: increment   // 节点属性
        },
        state.count  // 子节点
      )
  }
}

vnode的属性结构(h的第二个参数):

// 2.x
{
  staticClass: 'button',
  class: {'is-outlined': isOutlined },
  staticStyle: { color: '#34495E' },
  style: { backgroundColor: buttonColor },
  attrs: { id: 'submit' },
  domProps: { innerHTML: '' },
  on: { click: submitForm },
  key: 'submit-button'
}

// 3.x Syntax
{
  class: ['button', { 'is-outlined': isOutlined }],
  style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
  id: 'submit',
  innerHTML: '',
  onClick: submitForm,
  key: 'submit-button'
}

注册组件,2.x中:

// 假设有个ButtonCounter的自定义组件
export default {
  render(h) {
    return h('button-counter')
  }
}

3.x中

import { h, resolveComponent } from 'vue'

export default {
  setup() {
    const ButtonCounter = resolveComponent('button-counter') // 先解析组件
    return () => h(ButtonCounter) // 再传递组件
  }
}
slots

变化:

  • this.$slots 现在将slots公开为功能
  • this.$scopedSlots 已经移除

在渲染函数中使用:

// 2.x 语法
h(LayoutComponent, [
  h('div', { slot: 'header' }, this.header),
  h('div', { slot: 'content' }, this.content)
])

// 3.x 语法
h(LayoutComponent, {}, {
  header: () => h('div', this.header),
  content: () => h('div', this.content)
})

编程方式使用:

// 2.x 语法
this.$scopedSlots.header

// 3.x 语法
this.$slots.header()

自定义元素

有以下发生改变:

  1. 自定义元素的白名单,在编译器选项中配置。
  2. is属性的使用,仅限于保留组件的标签名
  3. 新增指令v-is,解决html 元素限制。
自定义元素白名单

如果想要指定vue之外的自定义元素(比如web组件),以plastic-button为例

<plastic-button></plastic-button>

2.x中

Vue.config.ignoredElements = ['plastic-button'] // 将plastic-button列入白名单

3.x中有两种方式可选,一种是作为编译选项配置,一种是运行时配置:

// webpack的vue-loader里配置
rules: [
  {
    test: /\.vue$/,
    use: 'vue-loader',
    options: {
      compilerOptions: {
        isCustomElement: tag => tag === 'plastic-button' // 指定组件加入白名单
      }
    }
  }
  // ...
]

// 运行时配置
const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'
定制内置元素
<button is="plastic-button">Click Me!</button>

在2.x中,它被解释为使用is的值作为组件的name去解析并渲染plastic-button组件,但该做法阻止了原生button元素的行为。

在3.x中,仅当使用<component>标签的时候,is才会和2.x中的用法相同。

<component is="plastic-button"/>

在普通组件上使用,它的行为类似与普通属性:

<foo is="bar" />
  • 2.x行为: 渲染bar组件
  • 3.x行为: 渲染foo组件,并传递is属性。

在普通元素上使用:

<button is="plastic-button">Click Me!</button>
  • 2.x行为:渲染plastic-button组件。
  • 3.x行为:通过调用呈现原生按钮
// 创建了web组件plastic-button的实例,但保留了button的特性
document.createElement('button', { is: 'plastic-button' })
v-is In-Dom模板解析

In-Dom模板,主要用于需要遵守html特定元素的解析规则的情况,比如<ul><ol><table><select>有什么元素可以在其内部出现的限制,以及一些元素,如<li><tr><option>只能出现某些其他元素中。

2.x中通常是这样使用:

<table>
  <tr is="blog-post-row"></tr>
</table>

而3.x则改成了v-is:

<table>
<!-- 注意v-is是指令,里面接受的是表达式,要填字符串,必须加引号‘’ -->
  <tr v-is="'blog-post-row'"></tr>
</table>

其他变化

  • destroyed生命周期改名为unmounted
  • beforeDestroy生命周期改名为beforeUnmount
  • propsdefalut工厂函数不再支持this上下文访问
    import { inject } from 'vue'
    
    export default {
      props: {
          theme: {
          default (props) {
              // props 是通过组件传递的原始属性
              // before any type / default coercions
              // 仅能使用inject来访问provide注入的属性
              return inject('theme', 'default-theme')
          }
          }
      }
    }
    
  • 自定义指令的生命周期api改为跟组件一致
    • bind → beforeMount
    • inserted → mounted
    • beforeUpdate 新增,在元素更新前调用
    • update 已经移除,因为与updated类似
    • componentUpdated → updated
    • beforeUnmount 新增,卸载元素之前立即调用
    • unbind -> unmounted
      于是最终形成的样子如下:
    const MyDirective = {
        beforeMount(el, binding, vnode, prevVnode) {
            // const vm = vnode.context  2.x中访问组件实例
            const vm = binding.instance // 3.x中访问组件的实例
        },
        mounted() {},
        beforeUpdate() {},
        updated() {},
        beforeUnmount() {}, // new
        unmounted() {}
    }
    
  • data选项只能使用函数声明方式
    <script>
      import { createApp } from 'vue'
      createApp({
          data() {
            return {
              apiKey: 'a1b2c3'
            }
          }
      }).mount('#app')
    </script>
    
  • data选项的合并行为被改变,只合并已有的属性。
    const Mixin = {
      data() {
          return {
            user: {
              name: 'Jack',
              id: 1
            }
          }
      }
    }
    
    const CompA = {
      mixins: [Mixin],
      data() {
          return {
            user: {
              id: 2
            }
          }
      }
    }
    
    结果如下:
    // 2.x中的合并结果
    {
      user: {
        id: 2,
        name: 'Jack'
      }
    }
    // 3.x中的合并结果
    {
      user: {
        id: 2
      }
    }
    
  • transition过渡类重命名,v-enter-> v-enter-from , v-leave-> v-leave-from
    <!-- 实现渐入渐出效果 -->
    .v-enter-from,
    .v-leave-to {
      opacity: 0;
    }
    
    .v-leave-from,
    .v-enter-to {
      opacity: 1;
    }
    
  • 监听数组时,仅当替换整个数组的时候才会触发,如果需要监听内部值的改变,则需要指定deep选项。
    watch: {
      bookList: {
        handler(val, oldVal) {
          console.log('book list changed')
        },
        deep: true
      },
    }
    
  • 没有携带特殊指令(v-if/else-if/else, v-for, 或者 v-slot)的<template>标签,将被视作原生的<template>html标签,这将导致不会渲染其内部的内容。
  • 在vue2.x中,应用根容器的outerHTML会被根组件替换(如果根组件没有template或者render选项,则最终编译为一个模板)。而vue3.x现在使用根容器的innerHTML代替。这意味着根容器自身不再作为模板的一部分。举个例子:
     <body id="body">
          <div id="app"></div>
     </body>
     <!-- vue2.x会将容器div一起替换了,而vue3,只会把app组件的内容替换到innerhtml里 -->
     <!-- 这也是vue2.x为什么不能把body作为app根元素的原因,因为body会消失= =! -->
    

移除的API

以下为移除的API

keycode

由于KeyboardEvent.keyCode已弃用, 因此vue3不再支持该功能。

  • v-on不再支持使用数字(即keyCodes)作为修饰符
  • config.keyCodes不再受支持
<!-- 不支持 -->
<input v-on:keyup.13="submit" />

<!-- 支持 -->
<input v-on:keyup.delete="confirmDelete" />
Vue.config.keyCodes = { // 不支持
  f1: 112
}
Events API

$on$off$once 不再支持,官方建议使用第三方库mitt或者tiny-emitter替换。$emit仍作为现有api,触发父组件事件而受支持。

filters

filters选项已被移除,在vue3中不再受支持(因为该语法打破了{}内只是javascript的假设),官方建议用方法或者computed代替。

如果你想使用全局过滤器,那么可以这么做:

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

app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}
<template>
  <h1>Bank Account Balance</h1>
  <p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>

使用这种方法时,你只能使用方法,而不能使用计算属性。因为后者仅在单个组件的上下文中定义才有意义。

Inline Template Attribute

内联模板不再受支持:

<!-- 这个没用过= =! -->
<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

官方建议用script标签或者slot代替。具体用法见官网

$destroy

$destroy实例方法。用户不应再手动管理各个Vue组件的生命周期。

支持库

目前所有的官方库和工具都支持vue3,但其中大多数仍处于beta(公测)状态。官方计划在2020年底之前稳定并切换所有项目以使用dist标签。

Vue CLI

从v4.5.0开始,vue-cli现在提供了内置选项,可以在创建新项目时选择Vue 3预设。

Vue Router

vue router 4.0 提供vue3的支持,并且具有许多重大更改。

Vuex

Vuex 4.0通过与3.x大致相同的API提供了Vue 3支持。唯一的重大变化是插件的安装方式


Composition API

下面开始正式讲解常用的组合式API。

reactive

reactive 基本等价于2.x中的Vue.observable(),返回一个响应式对象,就像2.x中定义在data选项里的数据一样,最终都会被转换成响应式对象。基于 ES2015 的 Proxy 实现。

import { reactive } from 'vue'

// state 现在是一个响应式的状态
const state = reactive({
  count: 0,
})

ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性.value

const count = ref(0)   // 相当于返回{value:0}
console.log(count.value) // 0

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

可能有些同学会问了,既然reactive和ref都能创建响应式对象,他们之间有什么区别呢,或者说各自使用在哪种场景呢?下面看一个例子:

// 风格 1: 将变量分离
let x = ref(0)
let y = ref(0)

function updatePosition(e) {
  x.value = e.pageX
  y.value = e.pageY
}

// --- 与下面的相比较 ---

// 风格 2: 单个对象
const pos =  reactive({
  x: 0,
  y: 0,
})

function updatePosition(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}

从上可以看出,他们的使用符合js的值类型和引用类型的概念:

  • ref 适合基础类型值
  • reactive 适合对象类型的值

有人会说,既然这样,那为什么不把变量全塞对象里直接用reactive呢?这是因为对象解构的时候,数据会丢失响应式特性,如下:

const pos =  reactive({
  x: 0,
  y: 0,
})

function updatePosition(e) {
// 解构对象,导致响应式丢失,相当于重新将值赋给了一个变量,之后的更改不会改变原属性的值
  let {x,y} = pos

  x = e.pageX
  y = e.pageY
}

正因为此,官方提供了toRefstoRef的函数,来将一个响应式对象的基础类型属性值转换为ref对象,这才不可避免的有了ref的概念。

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

const fooRef = toRef(state, 'foo') // 转换单个的foo属性为ref对象

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

state.foo++
console.log(fooRef.value) // 3
const state = reactive({
  foo: 1,
  bar: 2,
})

const stateAsRefs = toRefs(state)  // 转换state对象的所有属性为ref对象
/*
stateAsRefs 的类型如下:

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

注意:ref对象在以下情况会自动解套,也就是,不需要写.value也能访问值。

  • 当嵌套在reactive Object 中
  • 当作为setup的返回值返回

setup

setup是组件的新选项,作为在组件内使用 Composition API 的入口点。

  • 调用时机
    创建组件实例,初始化props → 调用setup → beforeCreate

  • 模板中使用

<template>
  <div>{{ count }} {{ object.foo }}</div>
</template>

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

  export default {
    setup() {
      const count = ref(0)
      const object = reactive({ foo: 'bar' })

      // 暴露给模板
      return {
        count,
        object,
      }
    },
  }
</script>

注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value。

  • 参数
    第一个参数收传递给组件的属性,第二个参数,从原来的this上下文选择性暴露了一些属性。
export default {
  props: {
    name: String,
  },
  setup(props, ctx) {  // 不要解构props,会导致其失去响应式
    watchEffect(() => {
      console.log(`name is: ` + props.name)
    })
    conosole.log(ctx)
    // context.attrs
    // context.slots
    // context.emit
  },
}
  • this的用法
    this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。

watchEffect

预期接收一个含有副作用的函数,仅当该过程中用到的响应式状态发生改变时,会重新执行该函数。

import { reactive, watchEffect } from 'vue'

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

onMounted(()=>{
    // 立即执行一次,之后会在state.count发生改变的时候执行,组件卸载的时候销毁
    watchEffect(() => {
        document.body.innerHTML = `count is ${state.count}`
    })
})

watchEffect回调的执行时机:

  1. 立即执行传入的一个函数,并响应式追踪其依赖
  2. 依赖变更时重新运行该函数(里面用到的响应式属性发生变更时)
  • 清除副作用

组件卸载时候会自动停止侦听器,当然也有显式调用停止的方式:

// 同步的方式
const stop = watchEffect(() => {
  /* ... */
})

// 之后
stop()

// 如果是回调里有异步,可以用回调的参数onInvalidate去取消监听
const data = ref(0)
watchEffect((onInvalidate) => { // 立即执行,其后data改变,组件更新后执行
  console.log(data.value)
  
  const timer = setInterval(()=>{
    data.value ++
  },1000)

  // 第一次初始化时候不执行该回调,仅注册回调,data改变时以及停止侦听时,会触发该回调
  onInvalidate(() => {  
    // 取消定时器
    clearInterval(timer)
  })
})
// output: 0 1

onInvalidate 触发时机

  • 副作用即将重新执行时(也就是追踪的依赖发生改变时)
  • 侦听器被停止时(如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)

watch

watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。

  • 对比watchEffectwatch 的作用:

    • 懒执行副作用(回调);
    • 更明确哪些状态的改变会触发侦听器重新运行副作用;
    • 能够侦听到数据变化的新值与旧值。
  • watch的数据源
    watch的数据源可以是一个或多个拥有返回值的 getter 函数,也可以是 ref:

      // 侦听一个 getter
      const state = reactive({ count: 0 })
      watch(
        () => state.count,   // 返回count的getter
        (count, prevCount) => { // 回调,新值旧值
          /* ... */
        }
      )
    
      // 直接侦听一个 ref
      const count = ref(0)
      watch(count, (count, prevCount) => {  // 监听ref
        /* ... */
      })
    
      // 监听多个属性,参数以数组方式传递即可
      watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
        /* ... */
      })
    

清除副作用与watchEffect类似,不同的地方就是onInvalidate会作为回调的第三个参数传入。

const data = ref(0)

watch(data, (newData, oldData, onInvalidate) => {
  console.log(newData.value)

  onInvalidate(() => {  
    // 取消定时器
    clearInterval(timer)
  })
})

computed

传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。

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

console.log(plusOne.value) // 2

plusOne.value++ // 错误用法,由于默认只设置了getter!

传入一个拥有 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

readonly

传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。只读代理是“深只读”,对象内部任何嵌套的属性也都是只读的。

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 依赖追踪
  console.log(copy.count)
})

// original 上的修改会触发 copy 上的侦听
original.count++

// 无法修改 copy 并会被警告
copy.count++ // warning!

生命周期钩子函数

直接导入onXXX即可使用周期函数:

import { onMounted, onUpdated, onUnmounted } from 'vue'

const MyComponent = {
  setup() {
    // beforeMount
    onMounted(() => {
      console.log('mounted!')
    })
    // beforeUpdate 
    onUpdated(() => {
      console.log('updated!')
    })
    // beforeUnmount
    onUnmounted(() => {
      console.log('unmounted!')
    })
  },
}

注意,这些生命周期钩子函数只能在setup中使用,因为他们依赖当前组件的实例。

组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的watchcomputed也将自动删除。

与2.x相比:

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

新增的钩子函数

  • onRenderTracked
  • onRenderTriggered

两个钩子函数都接收一个 DebuggerEvent:

export default {
  onRenderTriggered(e) {
    debugger
    // 检查哪个依赖性导致组件重新渲染
  },
}

依赖注入

// 提供者:
const themeRef = ref('dark')
provide(ThemeSymbol, themeRef)

// 使用者:
const theme = inject(ThemeSymbol, ref('light'))
watchEffect(() => {
  console.log(`theme set to: ${theme.value}`)
})

模板Refs

<template>
  <!-- 将div的引用赋值给root -->
  <div ref="root"></div>
</template>

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

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // 在渲染完成后, 这个 div DOM 会被赋值给 root ref 对象
        console.log(root.value) // 打印出 <div/>
      })

      return {
        root,
      }
    },
  }
</script>

配合 render 函数 / JSX 的用法

export default {
  setup() {
    const root = ref(null)

    // 使用render函数渲染
    return () =>
      h('div', {
        ref: root,
      })

    // 使用 JSX , 有木有感觉跟react很像:)
    return () => <div ref={root} />
  },
}

v-for中使用:

<template>
  <div v-for="(item, i) in list" :ref="el => { divs[i] = el }">
    {{ item }}
  </div>
</template>

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

  export default {
    setup() {
      const list = reactive([1, 2, 3])
      const divs = ref([])

      // 确保在每次变更之前重置引用
      onBeforeUpdate(() => {
        divs.value = []
      })

      return {
        list,
        divs,
      }
    },
  }
</script>

响应式系统工具集

API用途
isRef检查一个值是否为一个 ref 对象
isProxy检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
isReactive检查一个对象是否是由 reactive 创建的响应式代理。
isReadonly检查一个对象是否是由 readonly 创建的只读代理。
unrefval = isRef(val) ? val.value : val 的语法糖
toReftoRef 可以用来为一个 reactive 对象的属性创建一个 ref。
toRefs把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref

具体用法见官网

高级响应式系统API

customRef

customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应。

<template>
    <input v-model="text" />
</template>
<script>
function useDebouncedRef(value, delay = 200) {
  let timeout   
  return customRef((track, trigger) => {
    return {
      get() {
        track()  // 调用track收集依赖
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // 调用trigger,触发响应
        }, delay)
      },
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello'),
    }
  },
}
</script>
markRaw

显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。作用有点类似Object.freeze, 去除响应式。

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
shallowReactive

reactive类似,唯一的区别就是只创建“浅代理”,嵌套对象不会变成响应式。

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

// 变更 state 的自有属性是响应式的
state.foo++
// ...但不会深层代理
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
shallowReadonly

readonly类似,唯一的区别就是只限制“浅只读”。嵌套对象仍然可以赋值。

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

// 变更 state 的自有属性会失败
state.foo++
// ...但是嵌套的对象是可以变更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套属性依然可修改
shallowRef

ref类似,唯一的区别只是“浅引用” ,只会追踪它的 .value 更改操作,但是如果赋值的是一个对象,则该对象不是可响应,并且后续的对象的属性更改均不会触发视图响应。

const foo = shallowRef({})
foo.value.a = 1  // 这个a也不会响应到视图上去
isReactive(foo.value) // false
// 更改对操作会触发响应
foo.value = []
// 但上面新赋的这个对象并不会变为响应式对象,只是会同步这个值,视图上会同步显示[]
isReactive(foo.value) // false

const bar = shallowRef(0)
bar.value ++ // 1 , 这个是响应式的
toRaw

返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。简单来说就是返回代理之前的原始对象。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

组合式API的应用

前面已经讲述了常用Composition API,下面再看看,在实际使用中如何提取重用逻辑的。

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}
// 组件中使用
import { useMousePosition } from './mouse'

export default {
  setup() {
    const { x, y } = useMousePosition() // 官方推荐组合函数命名,以use打头,= =!莫名有点像hook
    // 其他逻辑...
    return { x, y }
  },
}

可以看到,使用这种方式的好处在于,可以将组件中任意一段逻辑提取出来复用。并且通过规范的命名,还能看出来这个函数的功能是做什么的,易于维护,不再像2.x那样,只是一堆选项配置的堆砌,无法直白的看出,某个地方具体作用。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐