Vue3学习笔记

亮点

  • Performance(比Vue2 runtime快了两倍)
  • Tree shaking (按需编译代码)
  • Ts support
  • Composition API(组合API)
  • Custom Renderer API(自定义渲染器)

数据响应

Vue3.0 之前的对象数据响应式的原理是 Object.defineProperty(), 数组的响应式原理是拦截了数组的7个方法(包括 push、pop、shift、unshift、 splice、 sort、 reverse)。这种方式存在的问题:对于对象,我们无法直接检测到属性的新增和删除;对于数组我们无法检测到直接去修改数组下标对应的内容以及利用 length 修改数组的长度。

Vue3.0的数据响应的原理是利用 Proxy 实现的。Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

  • Proxy 可以直接监听对象而非属性。因此对象的属性新增和删除也可以被监听。
  • Proxy 可以直接监听数组的变化。因此数组直接修改下标的内容以及长度也可以被监听。
  • Proxy 有多达13种拦截方法,不限于apply、deleteProperty、has等

diff算法优化

  • Vue2中的虚拟dom是进行全量比对
  • Vue3新增了静态标记(PatchFlag

在与上次虚拟节点进行比对时候,只对比带有patch flag的节点(绑定了响应式数据的节点), 并且可以通过flag的信息 得知当前节点要比对的具体内容。

新特性

组合式API

为什么

总结一下就是:避免逻辑关注点过于碎片化,提高代码的可读性和可维护性。

在Vue2中,我们如果要实现一个需求,那么这些逻辑会被分散在data、methods、computed等各个Options API,而在Vue3中,我们可以将同一个需求的各个逻辑模块整合起来,放在Composition API。如果用颜色来区分各个逻辑块,那么下图可以直观地展示这种区别。

setup

  • 一个接受propscontext的函数,从setup返回的内容都将暴露给组件的其余部分

    • props: 传入组件的属性:setup 中接收的props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式
    • context:暴露attrsslotsemit这三个组件的property
  • 创建组件之前,初始化 props 之后调用执行,因此setup中无法访问组件实例this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      beforeCreate() {
    console.log('beforeCreate');
    },
    created() {
    console.log('created');
    },
    mounted() {
    console.log('mounted');
    },
    setup (props, context) {
    console.log('setup');
    }
    // setup
    // beforeCreate
    // created
    // mounted
  • 我刚开始使用setup的时候,也产生过疑惑,这样不是将所有的代码都塞到setup里面,让它变得非常庞大臃肿吗?官方文档给出了解决方案——将各逻辑模块分别提取到独立的组合式函数

reactive

  • reactive是Vue3中提供的实现响应式数据的方法
  • 在Vue2中响应式数据是通过defineProperty来实现的,而在Vue3中响应式数据是通过ES6的Proxy来实现的

注意:

  • reactive参数必须是对象(json/arr)

  • 如果给reactive传递了其他对象

    • 默认情况下修改对象,界面不会自动更新
    • 如果想更新,可以通过重新赋值的方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <template>
    <div>
    <p>{{state.age}}</p>
    <button @click="myFn">点我</button>
    </div>
    </template>

    <script>
    import { reactive } from 'vue'
    export default {
    name: 'App',
    setup() {
    let state = reactive({
    age: 123,
    })

    function myFn() {
    state.age = 666
    console.log(state) // Proxy{age:666}
    }

    return {
    state,
    myFn,
    }
    },
    }
    </script>

带ref 的响应式变量 (响应式引用)

  • 在setup中直接声明的变量是非响应式的,因此需引入ref函数

  • ref接受参数并返回一个包装对象,包装对象具有 value property ,可使用该 property 访问或更改响应式变量的值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { ref } form 'vue'

    export default {
    const visible = ref(false)
    console.log(visible) // { value: false }
    console.log(visible.value) // false

    visible.value = true
    console.log(visible.value) // true
    }
  • 为什么要返回一个包装对象?

    提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这个容器可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    setup() {
    const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新
    const valueB = useLogicB()
    return {
    valueA,
    valueB
    }
    }

    // 作者:摸鱼架构师
    // 链接:https://juejin.cn/post/6844904042242523144
    // 来源:掘金
  • ref底层的本质其实还是reactive,系统会自动根据我们给ref传入的值将它转换成ref(xx) -> reactive({value: xx})

ref和reactive的区别

其目的都是对数据添加响应式效果,区别在于是否需要添加一层引用包装

Vue在处理的时候会先判断数据是什么类型的:

  • 如果在template里面使用的是ref类型(__v_isRef)的数据,那么Vue会自动帮我们添加.value
  • 如果template里使用的是reactive类型的数据,那么Vue不会自动帮我们添加.value

isRef和isReactive

通过 isRefisReactive可以判断数据是 ref 还是 reactive

1
2
3
4
5
6
const age = ref(18)
const state = reactive({ age: 18 })
console.log(isRef(age),'isRef--age') // true
console.log(isRef(state),'isRef--state') //false
console.log(isReactive(age),'isReactive--age') //false
console.log(isReactive(state),'isReactive--state') // true

toRefs

解构的同时保持propsreactive内部变量的响应式

1
2
3
4
5
import { toRefs } from 'vue'
setup(props){
const { user } = toRefs(props)
consloe.log(user.value)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>{{ title }}</h1>
<button @click="handleClick">✌</button>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";

export default defineComponent({
name: "App",
setup() {
const data = reactive({
title: "你好, Vue3",
handleClick: () => {
data.title = "数据来了";
},
});
const dataAsRefs = toRefs(data);
/*
Type of dataAsRefs:
{
title: Ref<string>,
handleClick: Ref<() => void>
}
*/
return { ...dataAsRefs };
},
});
</script>

computed属性

  • 使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性

    1
    2
    3
    4
    5
    6
    7
    8
    import { ref, computed } from 'vue'

    const counter = ref(0)
    const twiceTheCounter = computed(() => counter.value * 2)

    counter.value++
    console.log(counter.value) // 1
    console.log(twiceTheCounter.value) // 2
  • computed函数返回一个只读响应式引用,由一个作为 computed 的第一个参数传递的 getter 类回调输出。为了访问新创建的计算变量的 value,我们需要像使用 ref 一样使用 .value property。

watch

watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调

侦听单个数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})

侦听多个数据源

1
2
3
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})

watchEffect

watchEffect方法接收的第一个参数:effect函数,用于定义副作用。他会立即执行传入的effect函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

目的:为了根据响应式状态自动应用重新应用副作用

注:函数副作用是指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。副作用的函数不仅仅只是返回了一个值,而且还做了其他的事情

如下代码中,副作用函数的作用是:当 count 被访问时,旋即在控制台打出日志。

1
2
3
4
5
6
7
8
9
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
count.value++
// -> logs 1
}, 100)

副作用刷新时机

默认情况下,会在所有的组件 update 执行,如果需要在组件更新重新运行侦听器副作用,可以传递带有 flush 选项的附加 options 对象 (默认为 'pre')

1
2
3
4
5
6
7
8
9
// fire before component updates
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)

停止侦听

watchEffect会返回一个用于停止这个监听的函数。

  • 这个函数可以在组件被卸载时隐式调用:当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止

  • 也可以在setup函数里被显式调用,以停止侦听

    1
    2
    3
    4
    5
    6
    const stop = watchEffect(() => {
    /* ... */
    })

    // later
    stop()

清除副作用

有时副作用函数会执行一些异步的副作用,比如当数据变化时发送一次异步请求,如果请求过程中数据发生多次变化,那么就得多次发送请求。这样不仅浪费资源,还会因无法判断异步请求的执行顺序增加不确定性。

为了解决这个问题,watchEffect副作用传入的函数接收一个onInvalidate函数作为入参,用来注册清理失效时的回调。

onInvalidate只作用于异步函数,并且只有在如下两种情况下才会被调用:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)

我的理解中对异步副作用的清除就相当于函数防抖,执行这一次的副作用时,清理上一次的异步副作用,使得之前挂起的异步操作无效。

1
2
3
4
5
6
7
8
watchEffect(onInvalidate => {
// 异步api调用,返回一个操作对象
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// 取消异步api的调用。
token.cancel()
})
})

Vue3 之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。

在执行数据请求时,副作用函数往往是一个异步函数:

1
2
3
4
5
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {...}) // 在Promise解析之前注册清除函数
data.value = await fetchData(props.id)
})

watchEffect和watch的区别

  • watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而 watch 只能监听指定的属性而做出变更(v3开始可以同时指定多个)。
  • watch 访问侦听状态变化前后的值。
  • watch可以懒执行回调: watchEffect 如果存在,组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。

Fragment

组件现在可以有多个根节点,此时需要明确定义属性应该分布在哪里。

1
2
3
4
5
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>

Teleport组件

vue3提供了一个非常方便的新特性——teloport,可以理解为传送门,允许我们控制在 DOM 中哪个父节点下呈现 HTML,将模板渲染的位置与组件逻辑剥离开来。

基本使用

teleport接受两个属性:

to - string

Dialog组件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<Teleport to="#app">
<div class="xx-dialog-overlay" @click="onClickOverlay"></div>
<div class="xx-dialog-wrapper">
<div class="xx-dialog">
<header :class="{ 'title': title || $slots.title }">
<div v-if="$slots.title" class="title-slot">
<slot name="title"></slot>
</div>
<span v-else>{{ title }}</span>
<span class="xx-dialog-close" @click="close"></span>
</header>
<main>
<slot v-if="$slots.content" name="content"></slot>
<div v-else>{{ content }}</div>
</main>
<footer>
<x-button size="small" @click="onCancel">取消</x-button>
<x-button size="small" theme="primary" @click="onConfirm">确定</x-button>
</footer>
</div>
</div>
</Teleport>

效果演示:

可以看到通过to属性,指定该组件挂载到了<div id="app"></div>中。

注意

  • to属性传入的值必须是有效的查询选择器或 HTMLElement

    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 正确 -->
    <teleport to="#some-id" />
    <teleport to=".some-class" />
    <teleport to="[data-teleport]" />

    <!-- 错误 -->
    <teleport to="h1" />
    <teleport to="some-string" />
  • 默认挂载到第一个选择的元素

    1
    2
    3
    4
    <div class="container">container1</div>
    <div class="container">container2</div>

    <teleport to=".container"></teleport>

挂载到第一个.container

disabled-boolean

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

与 Vue components 一起使用

如果 teleport 包含 Vue 组件,则它仍将是 teleport 父组件的逻辑子组件,仍接收父组件传来的属性。

Suspense组件

Suspense 是Vue3推出的一个内置特殊组件,用来定义具有异步请求数据的组件的显示。如果使用 Suspense,要 setup函数中需要返回一个 Promise

新建 AyncShow.vue 文件,setup函数需要返回一个Promise对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<h1>{{result}}</h1>
</template>
<script lang="ts">
export default {
setup(){
return new Promise((resoluve)=>{
setTimeout(()=>{
resoluve({result: 100})
}, 2000)
})
}
}
</script>

App.vue中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<Suspense>
<template #default>
<div>
<AsyncShow/>
</div>
</template>
<template #fallback>
<h1>Loading...</h1>
</template>
</Suspense>
</template>

<script lang="ts">
import { defineComponent, ref, toRefs} from 'vue';
import AsyncShow from './components/AsyncShow.vue';
export default defineComponent({
name: 'App',
components: {
AsyncShow
}
});
</script>
  • Suspense组件内置了两个具名插槽slot,一个是default,用来显示异步组件请求成功的内容;一个是fallback用来显示异步组件请求响应前页面显示的内容。
  • default插槽可以有多个组件,但是需要有一个根节点。

defineComponent

defineComponent为了支持 TypeScript 存在的。defineComponent并没有实现特殊逻辑,可以将传入的对象获得对应的类型。我们使用defineComponent定义的组件可以很好的支持 setup、props等的类型提示。

非兼容的变更

全局API

Vue2的全局API

1
2
3
4
5
6
7
8
9
10
Vue.component('button-counter', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})

Vue.directive('focus', {
inserted: el => el.focus()
})

这样会导致同一个 Vue 构造函数创建的每个根实例共享相同的全局配置

为了避免这些问题,在Vue3中引入了createApp

Vue3 createApp

调用 createApp 返回一个应用实例,应用实例暴露当前全局 API 的子集,经验法则是,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
})

// 现在所有应用实例都挂载了,与其组件树一起,将具有相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境
app.mount('#app')

v-model

Vue2的v-model

  • Vue2.x中,在组件上使用 v-model 相当于绑定 value prop 和 input 事件

    1
    2
    3
    4
    5
    <x-switch v-model="checked" />

    <!-- 简写: -->

    <x-switch :value="checked" @input="checked = $event" />
  • 如果想更改绑定的属性名,或绑定多个变量,可使用.sync

    子组件:

    1
    this.$emit('update:value', newValue)

    父组件:

    1
    <x-switch :value.sync="checked" />

Vue3的v-model

  • 属性名任意,假设为 x
  • 事件名必须为 'update:x'
1
2
3
4
5
<x-switch v-model:value="checked" />

<!-- 简写: -->

<x-switch :value="checked" @update:value="checked = $event" />

h() 渲染函数

Vue2中的渲染函数createElement(),而在Vue3中被称为h(),用于创建虚拟节点(更准确地命名为createVNode(),出于频繁使用和简洁的目的,改为了h())。

h()同样接受三个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// @returns {VNode}
h(
// {String | Object | Function | null} tag
// 一个 HTML 标签名、一个组件、一个异步组件,或者 null。
// 使用 null 将会渲染一个注释。
//
// 必需的。
'div',

// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 我们会在模板中使用。
//
// 可选的。
{},

// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 Vnode" 或者
// 有 slot 的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)

实际应用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import XDialog from '../Dialog.vue'
import { createApp, h } from 'vue'

export const openDialog = ({
title = '',
content = '',
onConfirm = () => {},
onCancel = () => {},
closeOnClickOverlay = true
}) => {
const div = document.createElement('div')
document.body.appendChild(div)
const close = () => {
app.unmount()
div.remove()
}
const app = createApp({
render() {
return h(
XDialog,
{
visible: true,
title,
content,
onConfirm,
onCancel,
closeOnClickOverlay,
'onUpdate:visible':(newVisible) => {
if (!newVisible) {
close()
}
}
}
)
}
})
app.mount(div)
}

ref

在 Vue 2 中,在 v-for 里使用的 ref 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。

在 Vue 3 中,这样的用法将不再在 $ref 中自动创建数组。如果要从单个绑定获取多个 ref,需将 ref 绑定到一个更灵活的函数上

1
<div v-for="item in list" :ref="setItemRef"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref, onBeforeUpdate, onUpdated } from 'vue'

export default {
setup() {
let itemRefs = []
const setItemRef = el => {
itemRefs.push(el) // 这里的el即为循环的DOM元素
}
return {
itemRefs,
setItemRef
}
}
}

v-bind=”object“排序

Vue2 单独的 property优先

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

Vue3 绑定顺序决定渲染结果

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

1
2
3
4
5
6
7
8
9
<!-- 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>