Vue3.0时代你必须了解的:compositon api 用法和注意事项

10,155 阅读9分钟

前言:

直奔主题,看就完事了。

                                            

一些有用的地址

目前 Vue 3 处于 Pre-Alpha 版本。后面应该还会有 Alpha、Beta 等版本。

源码点这里

API文档点这里,当然,应该是国外友人写的英语版本。

Vue 3 改动的地方

  1. 使用 Typescript
  2. 放弃 class 采用 function-based API
  3. option API => Composition API
  4. 重构 complier
  5. 重构 virtual DOM
  6. 新的响应式机制

本文着重介绍第3点Composition API的用法和一些注意事项,并且通过demo介绍如果使用provider+inject 代替vuex

基本例子

import { value, computed, watch, onMounted } from 'vue'

const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = value(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}

setup() -API入口

setup是一个新的组件选项,也是其他API的入口。也就是说,你所有的操作都将在setup函数内部定义和执行,Vue3.0也将用函数代替Vue2.x的类,也就是new Vue()

setup是在一个组件实例被创建时,初始化了 props 之后调用,其实就是取代了Vue2.x的careted和beforeCreate。

setup 第一个参数是props,这里的props和Vue2.x中的props一致。具体的后面会说到。

setup第二个参数提供一个上下文对象,这个上下文对象提供一个可选的属性列表,和Vue2.x中挂载在this上的属性列表一致。

举个栗子:比如第二个参数取名content, 你想获取路由对象的话就是

content.$route

const MyComponent = {
  props: {
    name: String
  },
  setup(props) {
    console.log(props.name)
     return {      msg: `hello ${props.name}!` 
    }
}
 template: `<div>{{ msg }}</div>`}

setup返回一个对象,对象中的属性讲直接暴露给模板渲染的上下文。而在Vue2.x中,你定义的属性都会被Vue内部无条件暴露给模板渲染上下文。

reactive -数据监听函数

reactive函数接收一个对象作为参数,返回这个对象的响应式代理,等价Vue2.x的Vue.observable()

用法:

setup() {
        let reactiveData = reactive({name: 'lin', age: 20})
        return {
          reactiveData 
        }
      }

等价于Vue2.x:

data(){
    return {
        reactiveData :{name: 'lin', age: 20}    }
}

对比Vue2.x的observable(),组件实例在初始化的时候会将 data 整个对象变为可观察对象,通过递归的方式给每个 Key 使用 Object.defineProperty 加上 getter 和 settter ,如果是数组就重写代理数组对象的七个方法。虽然给我们带来的便利,但是在大型项目上来说,性能开销就很大了。Vue3.0之后不再将主动监听所有的数据,而是将选择权给你,实例在初始化时不需要再去递归 data 对象了,从而降低了组件实例化的时间。

Composition API

ref

用法:

ref接收一个原始值,返回一个包装对象,包装对象具有.value属性。通过.value访问这个值。

import {ref} from vue

const count = ref(0)
console.log(count.value) // 0

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

在渲染上下文中访问ref

在渲染上下文中使用,Vue帮你自动展开,无须用.value访问。

<template>
  <div>{{ count }}</div>
</template>

<script>
export default {
  setup() {
    return {
      count: ref(0)
    }
  }
}
</script>

将ref作为对象访问

如果作为对象访问,ref定义的值将自动展开,不用.value访问。

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) //  不用state.count.value

state.count = 1
console.log(count.value) // 作为值任然需要通过.value访问

isRef

检查一个对象是被ref包装过的对象

const unwrapped = isRef(foo) ? foo.value : foo

ref 和 reactive区别

其实ref相当于reactive的小弟,ref背后也是通过reactive实现的,唯一的区别是ref返回的是包装对象

const count = ref(0)  等价 const count = reactive({value:0})

为什么ref要返回一个包装对象?

关于什么是包装对象,如果不懂的,请看这里

我们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 useRef —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新):类似某act的自定义Hook

setup() {
  const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新
  const valueB = useLogicB()
  return {
    valueA,
    valueB
  }
}

ref和reactive需要注意的点:

在setup函数中,如果通过结构返回ref和reactive,那么在模板渲染上下文中,获取不到他们的响应式变化。因为解构他们就意味着copy了他们的引用。所以尽量不要用解构去返回一个你期望响应式的数据

 var App = {  template: `    <div class="container">    <div>    {{name1}}    {{name2}}    </div>    <button @click="add1"> add count</button>      </div>`,  setup() {    const name1 = ref({name1:'我是name'})    const name2 = reactive({name2:'aa'})    const add1 = () => {      console.log(name1.value.name1 = 'test')      console.log(name2.name2 = 'test')    }    return {      count, add1, ...pop,...name1.value,...name2    }  }}

如果你非要通过解构来使用,你可以使用toRefs()来使用

return{
    ...toRefs({name:'name'})
}

Props 

props对比Vue2.x主要要注意的地方

  1. Vue2.x中props绑定在this上,我们可以通过this.props.propsName获取对应的值,Vue3.0后Props将变成setup的第一个参数,而setup也是在初始化props之后才被调用。有点像某act的感觉。。。

  2. 类型定义的时候,任然可以像Vue2.x一样,同时也支持TS。     

  3. props 对象是响应式的 —— 它可以被当作数据源去观测,当后续 props 发生变动时它也会被框架内部同步更新。但对于用户代码来说,它是不可修改的(会导致警告)。                                        

    interface IProps{
        name:string
    }
    const MyComponent = {
    
      setup(props:IProps) {
        return {
          msg: `hello ${props.name}!`
        }
      },
      template: `<div>{{ msg }}</div>`
    }
    

computed -计算

定义:

计算值的行为跟计算属性 (computed property) 一样:只有当依赖变化的时候它才会被重新计算。类型某act的useCallback useMemo 

computed() 返回的是一个包装对象,它可以和普通的包装对象一样在 setup() 中被返回 ,也一样会在渲染上下文中被自动展开。

用法:

computed可以传两种参数

第一种:直接传一个函数,返回你所依赖的值的计算结果,这个值是个包装对象,默认情况下,如果用户试图去修改一个只读包装对象,会触发警告,说白了就是你只能get无法set

第二种:传一个对象,对象包含get函数和set函数。

总的来说这两点和Vue2.x相同。

import {computed,reactive} from vue

setup(){
    const count = reactive({count:0})
//第一种
    const computedCount1 = computed(()=>count.count++})
//第二种
    const computedCount2 = computed({
     get: () => count.value + 1,    set: val => { count.value = val - 1 }

computedCount2.value = 1
console.log(computedCount1.value) // 0
})
}

唯一不同的是,3.0中,computed 被抽成一个API,直接从vue中获取,而Vue2.x中,computed是一个对象,在对象中定义一个个computed

Vue2.x
var vm = new Vue({
  data: { a: 1 },
  computed: {
    // 仅读取
    aDouble: function () {
      return this.a * 2
    },
    // 读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})

Vue3.0
import {ref,computed} from Vue
setup(){
    const a = ref(0)
    const b = ref(1)

    const a_computed = computed(()=>a.value++)

    const b_computed = computed({
        get(){return a.value},
        set(val){return a.value+val }
)
}

readonly

接收一个ref或者reactive包装对象,返回一个只读的响应式对象。

const original = reactive({ count: 0 })

const copy = readonly(original)

watch(() => {
  // works for reactivity tracking
  console.log(copy.count)
})

// mutating original will trigger watchers relying on the copy
original.count++

// mutating the copy will fail and result in a warning
copy.count++ // warning!

watch

watch() API 提供了基于观察状态的变化来执行副作用的能力。

watch() 接收的第一个参数被称作 “数据源”,它可以是:

  • 一个返回任意值的函数
  • 一个包装对象
  • 一个包含上述两种数据源的数组

第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发:

这里有一些需要注意的点:

1.如果你不传数据源,只传一个回调函数,Vue会被动监听你回调函数中用到的每一个响应式数据。

2.如果你不传数据源,回调函数参数中,没有监听函数的当前值和变化前一次的值

    const count = ref(0)
    const count1 = ref(1)
    watch(() => console.log(count.value)) //监听count
    watch(()=>{
        console.log(count.value)
        console.log(count1.value)
}) //监听count和count1
  • 一个返回任意值的函数

    const value = ref(0)   watch((newValue,oldValue)=>value.value,() => {
    
    //监听Value
      console.log(value.value, 'value')    })
    
  • 一个包装对象

这里需要注意的点是:如果你监听reactive包装的数据,不能用这种方法,因为reactive返回的不是一个包装对象。你可以用第一种方法

const count = reactive({count:0})
watch(()=>count.count,()=>{....})   

 const value = ref(0)   
    watch(value,() => {        //监听Value
       console.log(value.value, 'value')   
     })
  • 一个包含上述两种数据源的数组

这种情况下,任意一个数据源的变化都会触发回调,同时回调会接收到包含对应值的数组作为参数:

 const count = ref(0)
 const test = ref(0)
 watch([value,count,()=>test.value],([newValue,newCount,newTest],[oldValue,oldCount,oldTest]) => {    console.log(value.value, 'value') })

停止watch

watch自动链接到组件的生命周期,在组件卸载的时候自动停止watch。不同的是,Vue3.0的watch函数返回一个停止watch的函数,供你手动停止watch

`const value = ref(0)`
`const stop = watch(value,(val,old)=>{.....})`

stop()

清理副作用

其实,回调函数还有第三个参数,这个参数是用来注册清理副作用的函数的。熟悉react 的useEffect的同学就知道,useEffect可以return 一个函数来清理自身的副作用,而Vue3.0是以参数的形式。一般情况下,在生命周期销毁阶段或是你手动stop这个监听函数的情况下,都会自动清理副作用,但是有时候,当观察的数据源变化后,我们可能需要执行一些异步操作,如setTimeOut,fetch,当这些异步操作完成之前,监测的数据源又发生变化的时候,我们可能要撤销还在等待的前一个操作,比如clearTimeOut。为了处理这种情况,watcher 的回调会接收到的第三个参数是一个用来注册清理操作的函数。调用这个函数可以注册一个清理函数。清理函数会在下属情况下被调用:
 watch(value, (val, oldVal, onCleanup) => {  
    const token = setTimeout(() => console.log(val, '我更新了'), 2000)
    onCleanup(() => {  
         // id 发生了变化,或是 watcher 即将被停止.        
        // 取消还未完成的异步操作。        
        console.log('我是清理函数')        
        clearTimeout(token)      
    })    
})
那么为什么Vue不像React那样,return一个清理副作用的函数,而是通过参数呢?

这是因为,我们可能这么写watch:

 watch(value, async (val, oldVal, onCleanup) => {       const token = await setTimeout(() => console.log(val, '我更新了'), 2000)             onCleanup(() => {        // id 发生了变化,或是 watcher 即将被停止.        // 取消还未完成的异步操作。        console.log('我是清理函数')        clearTimeout(token)      })    })

async函数隐性地返回一个promise,这样的情况下,我们是无法返回一个需要被立刻注册的清理函数的

控制watch回调调用时机

默认情况下,watch会在组件更新之后调用,如果你想在组件更新前调用,你可以传第三个参数,

第三个参数是个对象,有几个选项

flush  表示回调调用时机

post 默认值,在组件更新之后

pre 组件更新之前

sync 同步调用

deep 深度监听

类型: boolean  default :false

watch(
  () => state.foo,
  foo => console.log('foo is ' + foo),
  {    flush: 'post', // default, fire after renderer flush
    flush: 'pre', // fire right before renderer flush
    flush: 'sync' // fire synchronously
  })

和Vue2.x行为一致,都是对对象的深度监听

   const count1 = reactive({count:{count:0}})    watch(()=>count1.count, (val,oldVal)=>{      console.log(count1,"count1")    },{      deep:true    })    const add1 = ()=>{      count1.count.count = Math.random()    }

Lazy - 和Vue2.x**immediate** 正好相反

type:Boolean, default:false

  const count1 = reactive({count:{count:0}})    watch(()=>count1.count, (val,oldVal)=>{      console.log(count1,"count1")    },{      lazy:true    })

onTrack和 onTrigger

debugger钩子函数,分别在依赖追踪和依赖发生变化时调用。

生命周期

所有现有的生命周期钩子都会有对应的 onXXX 函数(只能在 setup() 中使用),去除created、beforeCreate。

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

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    // destroyed 调整为 unmounted
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}

provide,inject

依赖注入,和Vue2.x的provide、Inject类型。依靠依赖注入,我们可以跨组共享数据,你甚至可以在不依赖VueX的前提下,实现全局状态共享。熟悉React的同学知道,这和React的context类似。个人实战过后觉得比context好用。

provide用法:

provide接受两个参数,第一个参数是provide唯一名称,最好用Symbol,避免重复。第二个参数是你要暴露的数据

 provide(ThemeSymbol, 'dark')

inject 用法:

inject 接收两个参数,第一个参数是provide名称,第二个参数是默认数据。如果provider没有暴露自己的数据,那么使用inject默认数据。

provide和inject都只能在setup函数中使用。

 const ThemeSymbol= Symbol(); const Ancestor = {  setup() {
    provide(ThemeSymbol, 'dark')
  }
}

const Descendent = {
  setup() {
    const theme = inject(ThemeSymbol, 'light' /* optional default value */)
    return {
      theme
    }
  }
}

provide+inject 取代Vuex

类似React的 Context + useReducer 一定程度上可以取代redux一样,效果也非常不错。而Vue项目中,如果你不想引入Vuex,也可以考虑用provide+inject取代它。

以下是借鉴这篇文章

但是Vue3中新增了Hook,而Hook的特征之一就是可以在组件外去写一些自定义Hook,所以我们不光可以在.vue组件内部使用Vue的能力, 在任意的文件下(如context.ts)下也可以,

如果我们在context.ts中

  1. 自定义并export一个hook叫useProvide,并且在这个hook中使用provide并且注册一些全局状态,

  2. 再自定义并export一个hook叫useInject,并且在这个hook中使用inject返回刚刚provide的全局状态,

  3. 然后在根组件的setup函数中调用useProvide

  4. 就可以在任意的子组件去共享这些全局状态了。

项目实战

接下来是用Vue最新的库,做一个todolist的demo.

1.待办事件

2.已完成事件

3已取消(删除)事件

创建项目

利用Vue-cli 创建项目,并引入Vue3最新的库vue-composition-api

import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);

路由

路由的创建还是和原来的一样。

import Vue from 'vue';import VueRouter from 'vue-router';
const routes = [  {
    name: 'todo',
    path: '/todo',
    component: Todo,
    children: [
      {
        name: 'list',
        path: 'list',
        component: List 
     },
      {
        name: 'finished',
        path: 'finished',
        component: Finished 
     },
      {
        name: 'cancle',
        path: 'cancle',
        component: Cancle
      },    ]  },];
const router = new VueRouter({  routes,});export default router;

context

创建context文件夹,我们将在这里创建todo.ts,编写我们的useProvide和useInject

1.引入 api

import { provide, inject, computed, ref, Ref } from '@vue/composition-api';

2.定义数据类型

Vue3.0结合了TS,所以我们要先定义数据类型。

list:待办事件数组

type list = listItem[]

listItem:待办事件对象

type listItem = {
  title: string,  context: string,  status: string,  id: number
}

3.创建useListProvide并导出

export const useListProvide = () => {
  // 全部事件  const list = ref<list>([
    {
      title: '第一个待办事件',
      context: "前端Vue conposition-api 分享文档",
      id: 0,      status: 'todo'    },
    {      title: '第一个待办事件',
      context: "前端Vue conposition-api 分享文档",
      id: 1,      status: 'todo'    },  ]  );
   //修改状态
  const changeStatus = (id: number,status:string) => {
    if(!status){
      throw new Error(`can not find status`);
    }
    const removeIndex = list.value.findIndex((listItem: listItem) => listItem.id === id)
    console.log(removeIndex)
    if (removeIndex !== -1) {
      list.value[removeIndex].status = status    }
  };  
provide(listymbol, {
    list,
    changeStatus,
  })}

4.编写useListInject

export const useListInject = () => {
  const listContext = inject<ListContext>(listymbol);
  if (!listContext) {
    throw new Error(`useListInject must be used after useListProvide`);
  }
  return listContext
};

5.全局状态肯定不止一个模块,所以在context/index.ts下做统一的导出

import { useListInject,useListProvide } from './todo'
export { useListInject };
export const useProvider = () => {
  useListProvide()
};

6.然后在main.ts的根组件里使用provide,在最上层的组件中注入全局状态。

new Vue({
  router,
  setup() {
    useProvider();
    return {};
  },
  render: h => h(App),
}).$mount('#app');

这样,provide+inject就完成了,接下来是如何使用

在View/Todo/List.vue中

import { useListInject } from "@/context"; 
//引入useListInject setup() {
    const form = reactive({
      title: "",
      context: "",
    });
    const { list, changeStatus } = useListInject();
 //结构,得道list,changeStatus
     console.log(list.value, "list");
    const handleAddList = () => {
      console.log(form, "form");
      list.value.push({ 
       title: form.title,
        context: form.context,
        id: Math.random(),
        status: "todo"
      });      
     form.context = ''
     form.title = ''
     dialogFormVisible.value = false
    };    
return {
      list,
      changeStatus
    }
  }

最终代码:

<template>
  <div>
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>全部待办事件</span>
        <el-button class="button" type="text">批量删除</el-button>
        <el-button class="button" type="text" @click="dialogFormVisible = true">新增</el-button>
      </div>
      <div v-for="item in list" :key="item.index" class="listItem">
        <ListItem v-if="item.status === 'todo'" :item="item" style="margin:18px 0"></ListItem>
      </div>
    </el-card>
    <el-dialog title="新增待办事件" :visible.sync="dialogFormVisible">
      <el-form :model="form">
        <el-form-item label="事件名称" :label-width="formLabelWidth">
          <el-input v-model="form.title" auto-complete="off" size="medium"></el-input>
        </el-form-item>
        <el-form-item label="事件内容" :label-width="formLabelWidth">
          <el-input v-model="form.context" auto-complete="off" size="medium"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleAddList">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script  lang="ts">
import ListItem from "./components/listItem.vue";
import { computed, createComponent, ref, reactive } from "@vue/composition-api";
import { listItem as listItemType } from "../../types/index";
import { useListInject } from "@/context";
export default createComponent({
  components: {
    ListItem
  },
  setup() {
    const form = reactive({
      title: "",
      context: "",
    });
    const dialogFormVisible = ref(false);
    const formLabelWidth = ref("120px");
    const { list, addList, changeStatus } = useListInject();
    console.log(list.value, "list");
    const handleAddList = () => {
      console.log(form, "form");
      list.value.push({
        title: form.title,
        context: form.context,
        id: Math.random(),
        status: "todo"
      });
      form.context = ''
      form.title = ''
      dialogFormVisible.value = false
    };
    return {
      list,
      form,
      formLabelWidth,
      dialogFormVisible,
      handleAddList,
      changeStatus
    };
  }
});
</script>

<style>
.text {
  font-size: 14px;
}

.item {
  margin-bottom: 18px;
}

.clearfix:before,
.clearfix:after {
  display: table;
  content: "";
}
.clearfix:after {
  clear: both;
}

.box-card {
}
</style>

总体来说,Vue3.0之后

优点:

1.结合TS,更好的编写你的数据类型。

2.以API的形式重写Vue2.x的一些选项,让我们更好的将相关逻辑抽离和复用,向React的Hook形式靠拢。

3.新的状态共享模式,一定程度上可以代替Vuex。

缺点:1.容易写出面条式代码。

以上就是Vue3.0 一些API的新用法和注意的坑,希望能给大家带来帮助。