Vue3版本学习笔记

1,077 阅读7分钟

组合式API

需要用到的API 都需要import,这个操作会被插件自动补全

import { defineProps, reactive, computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'

响应性API

1 响应性基础API

reactive

返回对象的响应式副本,将全部属性和方法打包为一个对象

转而集合到一个对象之内,使对象 data 内的属性可以响应外部的更改

const data = reactive({
    counter: 1,
    doubleCounter: computed(() => data.counter * 2)
})
let timer;
onMounted(()=> {
    timer = setInterval(() => {
        data.counter++
    }, 1000);
})
onUnmounted(()=> {
    clearInterval(timer);
})

return toRefs(data);

通过 data.counter 和 data.doubleCountert 读取内部属性,让内部的数据可以响应外部方法的更改

打包组合函数

为了代码更有组织性,我们可以将组合函数打包为一个函数,并将属性 return 出来,通过声明函数名调用函数得到对象

const data = useCounter();

function useCounter(()=> {
    const data = reactive({
        counter: 1,
        doubleCounter: computed(() => data.counter * 2)
    })
    let timer;
    onMounted(()=> {
        timer = setInterval(() => {
            data.counter++
        }, 1000);
    })
    onUnmounted(()=> {
        clearInterval(timer);
    })
    
    return data;
})

相比Vue2之下, data、computed、watch 被拆分的写法被破坏,避免了之前的一个操作在这些API之间反复横跳

2 Refs

ref

接受一个单独的值,返回一个响应式且可变的 ref 对象,ref 对象具有指向内部的单个属性 .value

const count = ref(0)

console.log(count.value) // 0

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

如果需要将对象分配为 ref 值,则是需要通过 reactive 方法使该对象具有高度的响应式

unref()

如果参数是一个 ref 则返回内部值,否则返回参数本身的值,这是 val = isRef(val) ? val.value : val 的语法糖函数

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

toRef()

可以用来为源响应式对象的某个 属性 新创建一个 ref,然后 ref 可以被传递, 它会保持对其源属性的响应式链接

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

以上,toRef() 方法内部第一个参数为源对象,第二个参数为内部属性的 key 字符串,所创建的ref对象的value被更改,源对象内的值也将被更改

当需要将响应式对象中的一个属性传递给其他函数使用时,toRef 很有用

setup(props) {
    useSomeFeature(toRef(props, 'foo'))
}

toRefs()

这个方法将响应式对象转换为普通对象,得到的对象之中的每个属性都指向原始对象相应的ref

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

const stateAsRefs = toRefs(state)

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2

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

当组合式函数返回响应式对象时,toRefs 非常有用,

比如上面的reactive打包整个任务为一个函数,我们每次引用其中的属性,都需要 state.foo

这时可以用 toRefs() 方法返回当前data,外部解构这个函数得到对应的ref,

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

  // 操作 state 的逻辑

  // 返回时转换为ref
  return toRefs(state)
}

// 可以在不失去响应性的情况下解构
const { foo, bar } = useFeatureX()

isRef()

检查值是否为一个 ref 对象

3 computed 与 watch

computed

接受一个 getter 函数作为参数,并从 getter 返回的值返回一个不变的响应式 ref 对象

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

console.log(plusOne.value) // 2

plusOne.value++ // 报错

或者,他也可以使用具有 get 和 set 函数的 对象 来创建可写的 ref 对象

const count = ref(1)
const plusOne = computed({
    get: ()=> count.value + 1,
    set: val => count.value = val -1
})

// 当对plusOne 赋值相当于传入了一个参数,val形参等于1,对ref返回函数结果
plusOne.value = 1 
console.log(count.value) // 0

watchEffect

在响应式的跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它

const count = ref(0)
watchEffect(()=> {
    console.log(count.value)
}) // logs 0

setTimeout(()=> {
    count.value++
},1000)
// logs 1
// logs 2
// logs 3
// ...

watch

watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效,需要侦听特定的数据源,并在单独的回调函数中执行副作用,默认情况下也是惰性的,即回调仅在侦听源发生更改时被调用

  • 与 watchEffect 比较,watch 允许我们:
    • 惰性的执行副作用
    • 更具体的说明应触发侦听器重新运行的状态
    • 访问被侦听状态的 newVal 和 oldVal
侦听一个单一源

侦听器数据源可以是具有返回值的 getter 函数,也可以是 ref

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

// 侦听一个 ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
    // ...
})
侦听多个源

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

watch([fooRef, barRef], ([newFoo, newBar], [oldFoo, oldBar]) =>{
    // ...
})

Teleport 传送门

属性

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件

to - string — 通过条件判断渲染,将当前组件 teleport 标签内的 dom 挂载到指定类名的 dom 下

<teleport to="#some-id">...</teleport>
<teleport to=".some-class">...</teleport>
<teleport to="[data-teleport]">...</teleport>

disable - boolean — 此属性可用于禁用 的功能,这意味着其插槽内容将不会移动到任何位置,而是在你在周围父组件中指定了 的位置渲染

<teleport to='#popup' :disabled='displayVideoInline'>
	<video src='./my-movie.mp4'/>
</teleport>

请注意,这将移动实际的 dom 节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的实例活动状态,所有状态的 HTML 元素 ( 即播放的视频 ) 都将保持其状态

在同一个目标上使用多个 teleport

一个常见的使用场景是一个可重用的组件,它可能同时有多个实例处于活动状态,对于这种情况,多个组件可以将其内容挂载到同一个目标元素,书序将是追加 — 稍后挂载的将位于目标元素中较早的挂载之后

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- result-->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

动态创建组件

在父组件内,我们可以通过 控制,动态的创建组件插入当前dom,当前需要渲染的组件需要 import 引入

<template>
    <component :is='bloolear ? Foo : Bar'/>
</template>
<script setup>
    import Foo from './Foo.vue'
    import Bar from './Bar.vue'
</script>

自定义指令

1. 编写和导入

除了默认的内置指令(v-modul 和 v-show),当你需要对普通DOM元素进行底层操作,vue3也允许导入。自定义指令

我们可以像导入一个组件一样,导入一个指令,需要注意的是,要求命名方式里加一个 v ,这是一个标识提高代码可读性

例如我们创建一个vFocus.js , mounted 生命周期是同调用的组件对齐的

export default {
	// 当被绑定指令的元素挂载到 DOM 时
    mounted(el){
        // 当前元素获取焦点
        el.focus()
        console.log('获取焦点成功!');
    }
}

在需要的组件内 import , input标签内 v-focus 直接调用

<template>
	<input type="text" v-focus>
</template>

<script setup>
    import vFocus from '../vFocus.js';
</script>

另一个例子:vHighlight.js

export default {
	// 当被绑定指令的元素挂载到 DOM 时
    beforeMount(el, binding, vnode){
        el.style.background = binding.value
    }
}
<template>
	<p v-highlight="color">这是一段文字</p>
</template>

这样在元素被渲染前,就获取到当前组件内的对应变量,将颜色传递给指令来设置元素的背景色

2. 自定义指令API 的钩子函数

api和组件保持一致,具体表现在:

  • created:在绑定元素的属性或事件监听器被应用之前调用,在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用。

  • bind → beforeMount —— 当指令第一次绑定到元素,并且在挂载父组件之前调用

  • inserted → mounted —— 在绑定元素的父组件被挂载后调用

  • beforeUpdate: 新的 —— 在更新包含组件的 VNode 之前调用

  • componentUpdated → update —— 在包含组件的 VNode 及其子组件的 VNode 更新后调用

  • beforUnmount → 新的 —— 组件或元素将要移除之前调用

  • unbind → unmounted —— 组件或元素移除之后调用

Fragments

Vue3中可以拥有多个根

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

组件通信

Props 父传子

基本同 vue2,在父组件声明的属性需要传递到子组件,需要在组件上绑定 :子组件属性=‘父组件属性’,在子组件内通过 defindProps({ key: 值类型 }) 接收

// 父组件
<template>
<!-- 1.父组件内声明的事件,通过在子组件标签上绑定 -->
<Props :fatherProps="props"></Props>
</template>

<script>
    import { ref } from 'vue'
    const props = ref('abc')
</script>

// 子组件
<template>
{{ fatherProps }}
</template>
<script setup>
    import { withDefaults } from "vue";

    // 2.子组件内通过defineProps获取
    // 2.1 只声明props变量
    defineProps(['fatherProps'])

    // 2.2 指定类型
    defineProps({
        props1: String;
        props2: Number;
    })

    // 2.3 为props设定默认值
    defineProps({
        count1: {
            type: String,
            default: '默认值'
        },
        count2: {
            type: Number,
            default: 321
        }
    })

</script>

Emit 子传父

Vue3 提供一个 emits 选项,和 props 父传子类似,这个选项用来触发父组件事件 (子传父)

// 子组件
<template>
	<!-- 2.在子组件内绑定原生事件,触发自定义事件名,需要传值的话逗号隔开写入变量名 -->
	<button @click="$emit('my-click', str)">触发父组件事件</button>
</template>

<script setup>
    import { ref } from 'vue'
    
    // 1.声明自定义事件
    const str = ref('字符串')
	const emit = defineEmites(['my-click'])
    
</script>

// 父组件
<template>
	<!-- 3.父组件内,子组件标签上绑定触发事件名,属性值为当前组件内的方法 -->
	<Foo @my-click='useFoo'></Foo>
</template>

<script setup>
    import Foo from './Foo.vue'
    // 3.触发当前方法
	function useFoo(str) {
        console.log(str)
    }
</script>

获取、调用、更改子组件内的状态或方法

子组件标签 绑定ref属性,在父组件获取子组件实例,通过在子组件 definExpose 将变量或函数暴露出来,通过 ref.value.* 获取

// 父组件
<template>
<!—— 3.ref绑定子组件 ——>
<Foo ref='foo'></Foo>
<button @click="getCount">getCount</button>
</template>

<script setup>
    import Foo from "./components/Foo.vue";
    import { ref } from 'vue'
    // 2.声明 foo 响应对象
    const foo = ref(null)
    
	const bb = ref('bb')
    
    function getCount() {
        // 4.通过 foo.value 获取子组件整个实例对象,内部会包含第一步暴露出来的内容
        bb.value = foo.value.a;
        foo.value.a = bbb;
        console.log(foo.value.a);
        foo.value.getName()
    }

</script>
// 子组件
// 1.子组件内声明的变量或函数,通过 defindExpose 暴露出去
<script setup>
    const a = ref('aaa')

    function getName() {
        console.log('name');
    }

    defineExpose({
        a,
        getName
    })
</script>

另一个从子组件导出的方法

可以通过 import 的方法将子组件内的变量或方法导入到父组件

impor Foo,{ useFoo } from './Foo.vue'

只是,因为 setup 语法糖的关系,子组件抛出的是一个 setup对象,{ useFoo } 解构是不行的

这时我们可以在

// 子组件-------------------------
<script>
    export function useFoo(){
        return 'useFoo'
    }
    export default {
        name: 'Foo',
        obj: {
            count: 100
        }
    }
</script>

<script setup>
 // 这里是setup语法糖代码,上方为另起一个script脚本
</script>

// 父组件--------------------------
<template>
	<button @click='getFoo'>getFoo</button>
</template>
<script setup>
    import Foo, { useFoo, a } from './Foo.vue'
    
	function getFoo() {
        console.log(useFoo()) // useFoo
        console.log(Foo.name) // Foo
        // ...
    }
</script>

自定义渲染器 custome renderer

Vue3.0支持自定义渲染器,这个API 可以用来自定义渲染逻辑,比如将数据渲染到canvas上

用到的时候再了解........

全局API改为实例方法调用

实例方法定义组件

vue3中使用 createApp返回app实例,由它暴露一些列全局api

// main.js
import { createApp, h } from 'vue'
import App from './App.vue'

createApp(App)
  .component('comp', {
    render() {
      return h('div', 'I am comp')
    }
  })
  .mount('#app')

在组件中通过标签直接调用该组件

<template>
	<comp/>
</template>

Global and internal APIs 重构为可做摇树优化

vue2中不少全局api是作为静态函数直接挂在构造函数之上的,例如Vue.nextTick(),如果我们从未在代码中使用过它们,就会形成所谓的 dead code,这类全局api造成的无用代码无法使用 webpack的tree-shaking排除掉

import Vue from 'vue'

Vue.nextTick(() => {
    //...
})

Vue3中做了相应变化,将它们抽取成为独立函数,这样打包工具的摇树优化可以将这些dead code排除掉

import { nextTick } from 'vue'

nextTick(() => {
    //...
})

受影响api:

  • Vue.nextTick
  • Vue.observable (替换为 Vue.reactive) —— 响应式数据定义方法
  • Vue.version —— 判断版本方法
  • Vue.compile (仅在完整版本中) —— 用来做编译的方法
  • Vue.set (仅在兼容版中)
  • Vue.delete (仅在兼容版中)

v-model使用的变化

子组件的双向绑定

vue2中 .sync 和 v-model 功能有重叠,容易混淆,vue3做了统一

在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model 指令时的混淆并且在使用 v-model 指令时可以更加灵活。

父传子的同时,又允许子组件内修改这个数值传回父组件

// 父组件
<template>
	<comp v-model='counter'></comp>
</template>

<script setup>
    import {ref} from 'vue'
    
	const counter = ref(0)
</script>


// 子组件 —— 通过 v-model="xxx" 传递的数据,内部必须用modelValue接收
<template>
	<div @click="$emit('update:modelValue', modelVlaue + 1)">
        counter: {{modelValue}}
    </div>
</template>

<script setup>
	import { definProps } from 'vue'
    
    definProps({
        modelValue:{
            type: Number,
            default: 0
        }
    })
</script>

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

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->

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

v-model 参数

若果需要修改 modelValue 名称,作为组件内 modelValue 选项的替代,现在我们可以将一个 argument传递给 v-model

v-model:xxx="xxx"

<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>
	<div @click="$emit('update:pageTitle', pageTitle + 1)">
        counter: {{pageTitle}}
    </div>
</template>

<script setup>
	import { definProps } from 'vue'
    
    definProps({
        pageTitle:{
            type: Number,
            default: 0
        }
    })
</script>

异步组件使用变化

为了减少程序的体积,优化加载速度,仅在需要渲染组件的时候加载组件,就需要对组件进行异步导入

因为vu3中函数式组件必须定义为纯函数,异步组件定义时就有了变化:

  • 必须明确使用 defineAsyncComponent 包裹
  • component 选项重命名为 loader
  • Loader 函数不再接收 resolve 和 reject,且必须返回一个 Promise

定义一个不带配置的异步组件

import { defineAsyncComponent  } from 'vue'

const AsyncComp = defineAsyncCompomemt(() =>('./components/AsyncComp.vue'))

定义一个带配置的异步组件,loader选项是以前的 component

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

const AsyncComp = defineAsyncComponent({
    loader: () => import('./components/AsyncComp.vue'),
    delay: 200,
    timeout: 3000,
    errorComponent: ErrorComponent,
    loadingComponent: LoadingComponent
})

自定义组件白名单

vue3中自定义元素检测发生在模板编译时,如果要添加一些vue之外的自定义元素,需要在编译器选项中设置 isCustomElement 选项

使用构建工具时,模板都会用 vue-loader 预编译,设置它提供的 compilerOptions 即可,vue.config.js

rules: [
    {
        test: /\.vue$/,
        use: 'vue=loader',
        options: {
            compolerOptions: {
                //接收一个标签,这个标签等于xxx就忽略掉这个问题,消除警告提示
                isCustomElement: tab => tag === 'plastic-button'
            }
        }
    }
]

在vite项目中,在 vite.config.js 中配置 vueCompilerOptions 即可:

module.exports = {
    vueCompilerOptions: {
        isCustomElement: tag => tag === 'plastic-button'
    }
}

$scopedSlots 属性被移除,都用 $slots 代替

vue3中统一普通插槽和作用域插槽到 $slots ,具体变化如下:

  • 插槽均以函数形式暴露
  • $scopdSlots 移除

函数形式访问插槽内容,MyLink.vue

<script>
import { h } from 'vue'
    export default {
        props: {
            to: {
                type: String,
                required: true
            },
        },
        render() {
            return h("a", { href: this to }, this.$slots.default());
        },
    }
</script>

transition类名变更

vue2 写法不统一,vue3做了更正

  • v-enter → v-enter-from — 对应 — v-enter-to ---- v-enter-active ( 过渡 )
  • v-leave → v-leave-from — 对应 — v-leave-to ---- v-leave-active ( 过渡 )

组件watch选项和实例方法$watch不再支持点分隔符串路径

以 . 分隔的表达式不再被 watch 和 watch支持,可以使用计算函数作为watch 支持,可以使用计算函数作为 watch 参数实现

this.$watch(() => this.foo.bar, (v1, v2) => {
    console.log(this.foo.bar)
})

keyCode 作为 v-on 修饰符被移除

vue2中可以使用keyCode指代某个按键,由于可读性差,vue3不再支持

<!-- keyCode方式不再被支持 -->
<input v-on:keyup.13="submit" />

<!-- 只能使用alias方式 -->
<input @keyup.enter="submit" />

$on, $off$once 移除

上述3个方法被认为不应该由vue提供,因此移除了,事件的派发和监听可以使用其他第三方库实现

npm i mitt -S
// 创建emitter
const emitter = mitt()

// 发送事件
emitter.emit('foo', 'fooooooo')

// 监听事件
emitter.on('foo', msg => console.log(msg))

filter移除

vue3中移除了过滤器,请调用方法或者计算属性代替