熟练使用vue3?这一篇就够了~(非标题党!)

6,707 阅读9分钟

Composition API

vue2的Options API

vue3的由多个hook组合起来的形成的Composition API框架设计模式

setup

setupname,components一样实际上也是vue的一个配置项

但是组合式API是在setup中调用的

所以说vue3的模式从本质上来说是由options+composition两种模式结合的

  • 初始化属性之后,在beforeCreate之前调用

    所以this === undefined,不让使用this的主要原因是:

    避免在optionscomposition两种模式混用时出现的不必要的this冲突

    这违背了vue开箱即用的初衷

  • 需要return出模板中需要的数据

    return出的对象中的属性会被merge到模板编译的render context

    return也可以返回一个render函数

    const count = ref(0)
    const obj = reactive({foo:'bar'})
    return ()=> h('h1',[count.value,obj.foo])
    

    会直接编译到模板成为h1DOM节点

  • 响应式数据可以有reactive,ref两种声明方式

    ref声明的数据在模板中被引用时会被拆解开

    不必使用.value的形式获取

  • setup(props,ctx)的第一个参数是经过解析的父组件传来的属性

    1.props已经是被proxy代理过的响应式,不必对其再进行包装
    2.当某个props中的属性有改变的话会对props整个进行更新
    3.可以同时被watchEffect和watch观测到
    4.不要去解构props中的属性否则会失去响应式
    
  • 第二个参数ctx是一个对象,包含了在vue2.x中的this上暴露出的一些属性

    ctx.attrs / ctx.slots / ctx.emit
    attrs/slots也是在当前组件实例中的响应式代理过的值
    总是抛出最新的值,即使是在更新之后,可以去解构
    setup(props,{attrs,slots,emit})
    1.slots即当前组件的插槽
    2.attrs即当前组件的属性
    3.emit即vue2.x的this.$emit()
    
  • setup语法糖

    setup默认是和return搭配使用,是同步的
    setup语法糖省略组件注册 defineProps、defineEmits代替props和ctx.emit
    ​
    defineProps接收父组件传递属性名赋默认值并拿到props:
    import {defineProps,withDefaults} from 'vue'
    const props = withDefaults({
        defineProps<{
            width?:string,
            otherVals?:Array<ISth>
        }>(),
        {
            width:'100%',
            otherVals:[] || ISth[]
        }
    })
    

Reactivity APIs

reactive

接收一个对象返回原对象的响应式proxy对象,是深度的代理

其实vue2.x也有一个Vue.observable()和其功能相同

isReactive

检测一个对象是否由reactive创建出来的对象

注意以下情况

const state = reactive({
    name:'zs'
})
const test = readonly({
    name:'zs'
})
const copyState = readonly(state)
​
isReactive(test) === false
isReactive(copyState) === true
shallowReactive

不会对嵌套的对象执行深度响应

ref

1.接收一个inner value返回一个reactive并且可变的ref对象

这个对象有唯一的一个.value属性指向inner value

2.如果接收一个对象,会默认调用reactive进行处理

3.如果reactive某个属性值是ref:

const state = reactive({
    count:ref(1)
})
//访问和修改
state.count

4.如果传入的是个Array或者Map对象或者其他原生集合类型包含的ref,默认不会对ref自动展开

const arr = reactive([ref(0)])
const map = reactive(new Map([['foo',ref(0)]]))
//访问和修改
arr[0].value
map.get('foo').value
unref/isRef

如果参数是一个 ref,则返回内部值,否则返回参数本身。

isRef检测值是否是一个 ref对象

unref === isRef(val) ? val.value : val

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) 
  // unwrapped 现在一定是数字类型
}
toRef

为一个reactive对象的某个属性新创建一个ref

这个新创建的ref对象会和源property进行响应式连接

经常用于只需要使用响应式对象的某个property的情况

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

将响应式对象转为普通对象, 结果对象的每个property都指向原始对象相应property的ref

通常用于在不丢失响应式的前提下对返回的对象进行解构/展开

const state = reactive({
  foo: 1,
  bar: 2
})
​
const stateAsRefs = toRefs(state)
//stateAsRefs 的类型:
{
  foo: Ref<number>,
  bar: Ref<number>
}
​
//在template中可以直接使用foo / bar
return{
    ...toRefs(stateAsRefs)
}
customRef

本质是一个自定义的composition API,接收一个工厂函数

该函数接收tracktrigger两个函数,返回一个带getset的对象

track即追踪依赖,trigger即触发更新

通常用于图片的懒加载和防抖或节流

//使用customRef的实现debounce
function useDebounce(value,delay=200){
    let t=null;
    return customRef((track,trigger)=>{
        return {
            get(){
                track();
                return value
            },
            set(newVal){
                clearTimeout(t)
                t = setTimeout(()=>{
                    value = newVal
                    trigger()
                },delay)
            }
        }
    })
}
const text = useDebounce('',500)
return {
    text
}
shallowRef

可以追踪自身的.value的变化但是不会使其变为响应式

triggerRef

手动执行与shallowRef关联的任何作用(effect)

1.正常使用ref声明的变量
const info = ref({name:'zs'})
info.value = {name:'ls'} 
//info.value===Proxy{name:'ls'}
​
2.使用shallowRef声明的变量
const info = shallowRef({name:'zs'})
info.value = {name:'ls'} 
//info.value==={name:'ls'}
//此时watchEffect追踪不到info.value.name的变化
triggerRef(info) //执行之后可以追踪到info.value.name的变化

computed

1.可以接收一个getter函数并返回一个不可更改的响应式对象

2.可以接收一个setget函数的对象,创建一个可写的ref对象

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

接收一个对象(reactiveplainref)

返回一个只读的readonlyoriginal源对象的proxy代理

而且是深度代理,任何访问都可以被watchEffect监测到

const original = reactive({ count: 1 })
const copy = readonly(original)
​
watchEffect(() => {
  console.log('copy.count', copy.count)
})
original.count++ // 先后打印1 2
isReadonly

检测是否是由readonly创建的proxy代理的对象

shallowReadonly
const state = readonly({
  info: {
    age: 23,
  },
})
const shallowState = shallowReadonly({
  info: {
    age: 23,
  },
})
console.log(isReadonly(state), isReadonly(state.info)) //true true
console.log(isReadonly(shallowState), isReadonly(shallowState.info)) //true false

isProxy

检测是否是一个由reactivereadonly创建的proxy代理的对象

注意以下情况

const state = new Proxy({
    a:'属性a'
},{})
//原生Proxy代理的这种情况
isProxy(state)===false

watchEffect

  • 默认在onBeforeUpdate前被调用,若观测DOMref注册节点

需要在onMountedhook里面执行watchEffect

  • 接收一个回调函数并响应式追踪其依赖(数据)
  • 当依赖改变会re-run
  • 返回一个stop handle,调用可以明确停止其监听

在组件卸载时会自动停止

const count = ref(0)
setTimeout(() => {
  count.value = 1
}, 1000)
​
const stop = watchEffect(async (onInvalidate) => {
  onInvalidate(() => {
    //在此处进行拦截来消除副作用 需要在Promise被resolve前被注册
    //此外,Vue依赖这个返回的Promise来自动处理Promise链的潜在错误
    console.log('onInvalidate is triggered')
  })
  console.log(count.value) 
  //在此处有可能是异步操作 为了取消异步操作的副作用(失败的回调)
  //类似data.value = await fetchData(props.id)
},
{ //接收第二个参数 是个对象 对象内两个函数只有在开发模式下可以使用
    onTrack(e){ 
      console.log('trick') //追踪 一开始就会执行
    },
    onTrigger(e){
      //通常在这里放置debugger 交互式的调试检查
      console.log('trigger') //依赖 以来改变就会执行
    },
    flush:'post' //默认是`pre`在beforeUpdate执行,
    //可以通过设置为`post`使其在beforeUpdate之后执行,但是肯定是在updated之前执行
})
​
setTimeout(() => {
  stop()
  console.log('watchEffect is closed')
}, 2000)
/**先后打印为:
 * 0
 * onInvalidate is triggered
 * 1
 * onInvalidate is triggered
 * watchEffect is closed
**/

watch

传入一个返回需要观测的明确的数据的回调(getter的方式)

传入一个执行副作用的回调

对比watchEffect:

  • 可以懒执行副作用
  • 更加明确了什么时候执行副作用函数
  • 可以访问newValoldVal
  • 同样是在onBeforeUpdate前执行
  • 同样返回stop具柄函数,可以调用手动关闭watch监听
  • 同样接收第三个回调,里面可以调用onTrackonTrigger回调
  • watch可接受的回调和回调中常用的方法和属性如下
<template>
  <h2>count: {{ count }}</h2>
  <button @click="changeBtn" type="button">更新count</button>
</template><script lang="ts" setup>
const count = ref(1)
const changeBtn = () => {
  count.value++
}
onBeforeUpdate(() => {
  console.log('onBeforeUpdate') //点击后4
})
​
const stop = watch(
  () => count.value,
  (newVal, oldVal, onInvalidate) => {
    console.log('newVal', newVal) //初始化 2 点击后 3
    onInvalidate(() => {
      console.log('onInvalidate', onInvalidate) //点击后 2
    })
  },
  {
    onTrack() {
      console.log('onTrack') // 初始化 1
    },
    onTrigger() {
      console.log('onTrigger') //点击后 1
    },
    immediate: true,
    deep: true,
  }
)
</script>

toRaw

raw可以理解为不是reactivereadonly创建的proxy代理的原始对象

是一个逃生舱,用于临时读取数据不承担跟踪的开销

视图不进行更新的话,比如列表数据仅展示,可以使用此属性

markRaw

标记一个对象,此对象不会被转化成proxy代理对象,而会返回其本身

会给标记的对象添加一个_v_skip:true的属性,在响应式代理时会被跳过

在另外的响应式对象中的某个属性使用markRaw仍然是有效的

注意以下情况

//情况1
const foo = markRaw({
  nested: {},
})
const bar = reactive({
  nested: foo.nested,
})
console.log(foo.nested === bar.nested) //false 因为前者是原始对象,后者是Proxy代理对象//情况2
const foo = markRaw({
  nested: markRaw({
    a: 1,
  }),
})
const bar = reactive({
  nested: foo.nested,
})
bar.nested.a++ //a:2 表明指向的仍是同一个内存对象
console.log(foo.nested === bar.nested) //true 表明markRaw也是表层标注
markRaw小结

markRawshallowXXX都是允许不参与深度reactive/readonly转换

通常见于Vue component object或第三方的class instance

当渲染庞大且不可变的数据图表跳过响应可以提高性能

即手动对嵌套层进行markRaw或者直接在最外层使用shallowXXX

生命周期函数

vue2.x

生命周期流程如下

  1. new Vue()初始化Eventslifecycle

  2. 进入beforeCreate钩子:

    初始化一些injections注入内容和reactivity响应式

  3. 进入created钩子:

    判断是否有el属性

    • 如果有el属性判断是否有template配置项

      如果有template配置项,将模板编译到渲染函数中,即compile template into render function

      如果没有template配置项,将el outerHTML#app节点的全部模板作为模板编译

    • 如果没有el属性,当进入到vm.$mount(el)会被回调

  4. 进入beforeMount钩子:

    创建vm.$el并且替换掉el,即进行模板编译过程

    模板转成AST树--->AST树转成render函数-->render函数创建出虚拟DOM--->虚拟DOM再打到真实节点

  5. 进入mounted钩子:

    • 初始化时可以写一些逻辑
    • 当数据更改时,虚拟DOM重新渲染并打补丁,即beforeUpdateupdate钩子在此时也会被调用
  6. vm.$destroy()回调触发时,进入beforeDestroy钩子:

    卸载掉监听器,子组件和事件的监听等

  7. 进入destroyed钩子,组件销毁

vue3.x

生命周期钩子都是以onX为名称,同步使用在setup配置项中

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked(dev环境使用)
renderTriggeredonRenderTriggered(dev环境使用)
activatedonActivated
deactivatedonDeactivated
  1. onErrorCaptured表示在父组件里面可以捕获子组件的错误信息
  2. onRenderTracked追踪render的行为,通常在此debugger调试
  3. onRenderTriggeredre-render时触发
  4. onActivated被 keep-alive 缓存的组件激活时调用
  5. onDeactivated被 keep-alive 缓存的组件失活时调用

依赖注入

Provide/Inject

当你仅需要祖级组件的某些数据

祖级组件不需知道哪些子组件使用了它provideproperty

子组件不需要知道injectproperty来自哪里

可以使用这一对属性

处理响应式:

provide时使用computed进行处理数据;

或者在定义数据的时候使用refreactive

修改响应式数据:

祖级组件直接provide一个方法给子组件,仍然在祖级组件进行操作

如果不想让子组件修改的话,用readonly进行数据修饰

节点引用

vue2.x中为了直接访问到子组件,可以通过ref属性给子组件或者DOM节点注册一个reference ID,可以在refs访问到

vue3.xtemplate中使用ref注册组件或者DOM节点时,在setup可以定义同名ref代理的变量,会指向同一个引用reference

如果一个VNode ref的在渲染函数中和setup定义的ref的变量的key是对应一致的,虚拟节点对应的元素或组件实例会被指定为这个变量的值,并且在VDOM的挂载和更新进程中起作用,所以在初始化render之后才会设置这个值

v-for的情况下使用refs

<template>
 <ul>
    <li
      v-for="(item, index) in students"
      :key="index"
      :ref="
        (el) => {
          if (el) lists[index] = el
        }
      "
    >
      {{ item.name }}
    </li>
 </ul>
</template>
​
const students = reactive([{ name: 'zs' }, { name: 'ls' }])
const lists = reactive([])
onMounted(() => {
  console.log('lists', lists)
})

应用配置项

globalProperties

添加一个可以再应用的任何组件实例中访问的全局property

此属性命名优先级高于某个组件setup声明的变量

const app = createApp({})
app.config.globalProperties.$util = ()=>{}
//等同于vue2.x的
Vue.prototype.$util = () => {}

compilerOptions(3.1+)

配置运行时编译器的选项,设置在这个对象上的值会被传入浏览器内的模板编译器,并影响应用内的每个组件

只有在完整的构建版本中生效,通常用vue-cli的配置项或vite@vitejs/plugin-vue选项传入

compilerOptions.isCustomElement

指定一个方法识别Vue以外 (小程序的语法) 定义的自定义元素

compilerOptions.whitespace

默认情况下,Vue 会移除/压缩模板元素之间的空格以产生更高效的编译结果:

  1. 元素内的多个开头/结尾空格会被压缩成一个空格
  2. 元素之间的包括折行在内的多个空格会被移除
  3. 文本结点之间可被压缩的空格都会被压缩成为一个空格

将值设置为 'preserve' 可以禁用 (2) 和 (3)。

compilerOptions.comments

  • 类型boolean
  • 默认值false
  • 用法
app.config.compilerOptions.comments = true

默认情况下,Vue 会在生产环境下移除模板内的 HTML 注释。将这个选项设置为 true 可以强制 Vue 在生产环境下保留注释。而在开发环境下注释是始终被保留的。

这个选项一般会用于依赖 HTML 注释的其它库和 Vue 配合使用。

compilerOptions.delimiters

  • 类型Array<string>
  • 默认值['{{', '}}']
  • 用法
// 将分隔符设置为 ES6 模板字符串风格
app.config.compilerOptions.delimiters = ['${', '}']    

用于配置模板内文本插值的分隔符。

这个选项一般会用于避免和同样使用双大括号语法的服务端框架发生冲突

optionMergeStrategies

仅针对于options api

为自定义选项定义合并策略

合并策略是在Mixin选项中被定义的

performance

组件性能相关的配置,通常进行前端性能优化时会用到此配置进行调试

仅在开发模式下可用并且支持performance.mark()的浏览器

设置为 true 以在浏览器开发工具的 performance/timeline 面板中启用对组件初始化、编译、渲染和更新的性能追踪。

复用性

Mixin

vue2.x中,mixin是将部分组件逻辑抽象成可重用块的主要工具,但是仍然存在不足:

1.Mixin很容易发生变量名冲突

2.可重用性有限,不能传递参数改变其逻辑

3.隐式依赖,mixin和使用它的组件没有层次关系

组合式API

vue3.x中添加了一种通过逻辑关注代码组织的方法:

组合式API进行代码重用,通常被称为hooks

//js文件中定义
import { ref, computed } from "vue";
 
export default function () {
  const count = ref(0);
  const double = computed(() => count.value * 2)
  function increment() {
    count.value++;
  }
  return {
    count,
    double,
    increment
  }
}
​
// vue文件中使用
import useCounter from "./useCounter.js";
 
export default {
  setup() {
    const { count, double, increment } = useCounter();
    return {
      count,
      double,
      increment
    }
  }
}

内置组件

内内置组件可以直接在模板使用,不需要注册

<keep-alive> <transition> <transition-group>和新增的<teleport>组件都可以被打包工具tree-shake

即需要使用的时候需要将其显性导入

import { KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

component

渲染一个元组件为动态组件

根据is的值,决定哪个组件被渲染出来,通常和keep-alive结合使用

<!-- 失活的组件将会被缓存!还可以和下面的transition连用-->
<transition>
  <keep-alive>
    <component :is="currentTabComponent"></component>
  </keep-alive>
</transition>

transition

为单个元素/组件提供过渡效果

传入name属性和其他配置项属性进行过渡效果定义

<!-- 动态组件 -->
<transition name="fade" mode="out-in" appear>
  <component :is="view"></component>
</transition>

slot

使用非常频繁的内置组件,简单来说

父组件使用子组件的结构,但是子组件的内容是由父组件控制并传入

<slot>即为我们想要插入内容的占位符

teleport

移动实际的DOM节点到特定的节点目录下,比如根节点

是移动而不是销毁!

特殊属性

共包含包含ref key is三个特殊属性

key

主要用做Vue的虚拟DOM算法的提示,以在比对新旧节点组时辨识 VNodes。

如果不使用 key,Vue 会使用一种算法来最小化元素的移动并且尽可能尝试就地修改/复用相同类型元素。

使用 key 时,它会基于 key 的顺序变化重新排列元素,并且那些使用了已经不存在的 key 的元素将会被移除/销毁。

有相同父元素的子元素必须有唯一的 key。重复的 key 会造成渲染错误

应用API

vue3.x中是在const app = createApp()上进行应用API的挂载,整个组件树都会共享这个app的上下文,类似于vue2.x的const vm = new Vue()

directive

对视图上一些逻辑的抽离封装通常利用directive来操作

// 注册 (函数指令)
app.directive('my-directive', () => {
  // 这将被作为 `mounted` 和 `updated` 调用
})
// 也可以把第二个参数写成对象写法
app.directive('my-directive', {
  created(el, binding, vnode) {},
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value
  },
  mounted() {},
  beforeUpdate(a,b,c,prevNode) { 
    //第四个参数 prevNode 只在beforeUpdate和updated才有!
    a.style.background = b.value
  },
  updated() {},
  beforeUnmount() {
    // 当指令绑定的元素 的父组件销毁前调用。  
  },
  unmounted() {},// 当指令与元素解除绑定且父组件已销毁时调用。
​
})
​
// getter, 如果已注册,则返回指令定义
const myDirective = app.directive('my-directive')

component

app实例进行全局注册组件

mount

所提供 DOM 元素的 innerHTML 将被替换为应用根组件的模板渲染结果

返回值是根组件实例

unmount

卸载应用实例的根组件

use

安装插件时,如果插件是一个对象,则必须暴露一个install方法

如果插件本身是一个函数,则会被视为install方法

返回值是当前应用实例,所以可以进行链式调用

config

参考应用配置项

provide

参考composition API里的依赖注入

实例属性

通俗理解就是*.vue单页面组件可以使用的属性

实例方法

在vue3中想要得到当前组件实例,需要使用getCurrentInstance方法

但是此方法在生产模式下是不生效的

所以实例方法通常是在vue2.x中有this的情况下使用较多

全局API

简单来说在任何地方都可以访问到,以下是常用的几个api

defineComponent

defineComponent是从vue中导入,传入对象返回另一个对象,返回的对象实际上是对该组件的描述
--->可以看做是一个类,表明在父组件调用子组件方法时实际上是生成了新的子组件实例进行操作的
​
具体如下:
子组件不能直接改变 props 的数据,需要改变的时候使用 v-model

resolveDirective

返回一个Directive,如果没有找到返回undefined

//App.vue
const app = createApp({})
app.directive('highlight', {})
​
//component.vue
import { resolveDirective } from 'vue'
render () {
  const highlightDirective = resolveDirective('highlight')
}

withDirectives

把指令应用VNode,返回一个包含应用指令的VNode

import { withDirectives, resolveDirective } from 'vue'
const foo = resolveDirective('foo')
const bar = resolveDirective('bar')
​
return withDirectives(h('div'), [
  [foo, this.x],
  [bar, this.y]
])

nextTick

将回调推迟到下一个DOM更新周期之后执行

在更改了一些数据之后等待DOM更新的时机立即使用它