欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue3 - Composition API

程序员文章站 2022-05-17 10:28:39
...

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全局API 3.x实例API(app)
Vue.config.production 已经删除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.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 创建的只读代理。
unref val = isRef(val) ? val.value : val 的语法糖
toRef toRef 可以用来为一个 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那样,只是一堆选项配置的堆砌,无法直白的看出,某个地方具体作用。