2022年,一文读懂Vue3 的新功能和原理

658 阅读8分钟

对 vue3 进行全面讲解,文章较长,但保证都是干货,请耐心看完。

概述

本文会从以下Vue3 新功能原理两个方面进行讲解。

Vue3 新功能

  • createApp
  • emits属性
  • 多事件处理
  • Fragment
  • 移除.sync 改为v-model
  • 异步组件的引用方式
  • 移除filter
  • Teleport
  • Suspense
  • Composition API
    • reactive
    • ref toRef toRefs
    • readonly
    • computed
    • watch watchEffect
    • 钩子函数生命周期

原理

  • Proxy 实现响应式
  • 编译优化
    • PathFlag 静态标记
    • hoistStatic 静态提升
    • cacheHandler 缓存事件
    • SSR 优化
    • Tree-shaking优化
  • Vite - ES6 module

1. Vue3 比Vue2 有什么优势?

  • 性能更好
  • 体积更小
    • 只是相对Vue2
  • 更好的ts支持
  • 更好的代码组织
  • 更好的逻辑抽离
    • 日益丰富的业务需求和框架代码编写之间的矛盾
  • 更多的新功能
    • vue3 的一些新功能,我们vue2可以通过一些方式实现,但是vue3 官方支持肯定更好

2 描述Vue3 生命周期

2.1 Options API 生命周期

- beforeDestroy 改为 beforeUnmount
- destroyed 改为 unmounted
- 其他沿用 Vue2 的生命周期

2.2 Composition API 生命周期

- 差不多,从 vue 中引入使用罢了
- 不过前面都加上on
- import { onBeforeMount } from 'vue'

2.3 LifeCycles.vue

<template>
    <p>生命周期 {{msg}}</p>
</template>

<script>
import { 
    onBeforeMount,
    onMounted,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onUnmounted 
} from 'vue'

export default {
    name: 'LifeCycles',

    props: {
        msg: String
    },

    // 等于 beforeCreate 和 created
    setup() {
        console.log('setup')

        onBeforeMount(() => {
            console.log('onBeforeMount')
        })
        onMounted(() => {
            console.log('onMounted')
        })
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate')
        })
        onUpdated(() => {
            console.log('onUpdated')
        })
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount')
        })
        onUnmounted(() => {
            console.log('onUnmounted')
        })
    },

    // beforeCreate() {
    //     console.log('beforeCreate')
    // },
    // created() {
    //     console.log('created')
    // },
    // beforeMount() {
    //     console.log('beforeMount')
    // },
    // mounted() {
    //     console.log('mounted')
    // },
    // beforeUpdate() {
    //     console.log('beforeUpdate')
    // },
    // updated() {
    //     console.log('updated')
    // },
    // // beforeDestroy 改名
    // beforeUnmount() {
    //     console.log('beforeUnmount')
    // },
    // // destroyed 改名
    // unmounted() {
    //     console.log('unmounted')
    // }
}
</script>

3 如何理解Composition API 和 Options API?

3.1 Composition API 带来了什么?

  • 更好的代码组织
    • 把散落的代码,抽离放置到了一起
  • 更好的逻辑复用
  • 更好的类型推导
{
    data(){
        return {
            a: 10
        }
    }
    method: {
        fn(){
            // 这里的this.a 为什不是this.data.a
            const a = this.a;
        }
    },
    mounted(){
          // 这里的this.fn 为什不是this.method.fn
        this.fn()
    }
}

项目太大,逻辑过于复杂时,可以更好的发挥作用

Composition API 主要是针对大型项目应用要用的,如果是简单的1+1=2,反而体现不出来以三点

image.png

3.2 如何选择 Composition API 和 Options API?

  • 不建议共用,会引起混乱(写法、代码组织和逻辑复用方式都不一样)
  • 小型项目、业务逻辑简单,用Options API
  • 中大型项目、逻辑复杂,用Composition API

4. 如何理解ref、toRef 和 toRefs?

4.1 ref、toRef 和 toRefs是什么?

4.1.1 ref

  • 生成值类型的响应式数据(注意不是引用类型)
  • 可用于模板和reactive
  • 通过.value修改值 Ref.vue 响应式
<template>
    <p>ref demo {{ageRef}} {{state.name}}</p>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
    name: 'Ref',
    setup() {
        const ageRef = ref(20) // 值类型 响应式
        const nameRef = ref('服部')

        const state = reactive({
            name: nameRef
        })

        setTimeout(() => {
            console.log('ageRef', ageRef.value)

            ageRef.value = 25 // .value 修改值
            nameRef.value = '服部平次&远山和叶'
        }, 1500);

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

RefTemplate.vue

<template>
    <p ref="elemRef">我是一行文字</p>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
    name: 'RefTemplate',
    setup() {
        const elemRef = ref(null)

        onMounted(() => {
            console.log('ref template', elemRef.value.innerHTML, elemRef.value)
        })

        return {
            elemRef
        }
    }
}
</script>

4.1.2 toRef

  • 针对一个响应式对象(reactive封装)的prop(属性)
  • 创建一个ref,具有响应式
  • 两者保持引用关系

// toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式

ToRef.vue

<template>
    <p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>

<script>
import { ref, toRef, reactive } from 'vue'

export default {
    name: 'ToRef',
    setup() {
        // 创建一个响应式对象
        const state = reactive({
            age: 20,
            name: '服部'
        })

        const age1 = computed(() => {
            return state.age + 1
        })

        // // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
        // const state = {
        //     age: 26,
        //     name: '服部'
        // }

        const ageRef = toRef(state, 'age')

        setTimeout(() => {
            state.age = 25
        }, 1500)

        setTimeout(() => {
            ageRef.value = 37 // .value 修改值
        }, 3000)

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

4.1.3 toRefs (注意这里是toRefs,不是toRef)

  • 将响应式对象 (reactive封装)转换为普通对象
  • 对象的每个prop都是对应的ref
  • 两者保持引用关系

ToRefs.vue

<template>
    <p>toRefs demo {{age}} {{name}}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
    name: 'ToRefs',
    setup() {
        const state = reactive({
            age: 26,
            name: '服部'
        })

        const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象

         // 每个属性,都是 ref 对象
        // const { age: ageRef, name: nameRef } = stateAsRefs 
       
        
        // return {
        //     ageRef,
        //     nameRef
        // }

        setTimeout(() => {
            state.age = 27
        }, 1500)

        return stateAsRefs
        
        // 直接返回 state, 将会失去响应式
        // return {
        //    ...state
        // }
    }
}
</script>

4.2 ref、toRef 和 toRefs最佳使用方式

4.2.1 最佳使用方式

  • reactive对象 的响应式,用ref 类型响应式
  • setup中返回toRefs(state),或者toRef(state,‘xxx’)
  • ref的变量命名都用xxxRef
  • 合成函数返回响应式对象,使用toRefs

4.2.2 合成函数返回响应式对象

image.png

4.3 进阶,深入理解

4.3.1(一)为什么需要ref?

  • 返回值类型,会丢失响应式;
    • 定义时需要值类型为响应式可以用reactive,但是函数返回值等,也可能是值类型需要响应式
  • 如setup、computed、合成函数都有可能返回值类型,需要响应式;
  • Vue不定义ref,用户会自造ref,反而制造混乱

WhyRef.vue

<template>
    <p>why ref demo {{state.age}} - {{age1}}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive, computed } from 'vue'

function useFeatureX() {
    const state = reactive({
        x: 1,
        y: 2
    })

    return toRefs(state)
}

export default {
    name: 'WhyRef',
    setup() {
        const { x, y } = useFeatureX()

        const state = reactive({
            age: 26,
            name: '服部'
        })

        // computed 返回的是一个类似于 ref 的对象,也有 .value
        const age1 = computed(() => {
            return state.age + 1
        })

        setTimeout(() => {
            state.age = 27
        }, 1500)

        return {
            state,
            age1,
            x,
            y
        }
    }
}
</script>

4.3.2(二)为何ref需要value属性

  • ref是一个对象(不丢失响应式),value存储值
  • 通过.value属性的get和set实现响应式
  • 用于模板、reactive时,不需要.value,其他情况都需要

值类型不是引用数据,所以值会丢失,响应式无法实现,所以需要用ref,ref的原理就是一个对象,value存储值

4.3.3(三)为什么需要toRef和toRefs

  • 初衷:不丢失响应式的情况下,把对象数据 分解/扩散
  • 前提:针对的是响应式对象(reactive封装的)非普通对象
  • 注意:不创造响应式, 而是延续响应式

5. Vue3 升级了哪些重要的功能?

# 从Vue2迁移-官方

5.1 creatApp

image.png

5.2 emits属性

image.png

5.3 生命周期

# 生命周期钩子-官方

5.4 多事件处理

image.png

5.5 Fragment

Vue3可以使用 多个节点的模版 image.png

5.6 移除.sync 改为v-model

image.png

5.7 异步组件的写法

5.7.1 Vue2 写法

image.png

5.7.2 Vue3 写法

defineAsyncCompontent

image.png

5.8 移除filter

image.png

使用computed也是可以处理的

移除原因可能是

  • 类型推导、类型检测以及自动化的东西 有一些影响

用js的方式完成,让开发在某些方面会更简单些

5.9 Teleport

通过将 UI 和相关行为封装到组件中来构建 UI。我们可以将它们嵌套在另一个内部,以构建一个组成应用程序 UI 的树

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

image.png

5.9 Suspense

Suspense 是一个试验性的新特性,其 API 可能随时会发生变动。特此声明,以便社区能够为当前的实现提供反馈。

该 <suspense> 组件提供了另一个方案,允许将等待过程提升到组件树中处理,而不是在单个组件中。

没什么创新的东西,只是封装了一个具名插槽

image.png

5.10 Composition API

  • reactive
  • ref toRef toRefs
  • readonly
  • setup
  • watch watchEffect
  • 生命周期钩子函数

6. Composition API 如何实现代码逻辑复用?

  • 抽离逻辑代码到一个函数
  • 函数命名约定为useXxxx格式(React Hooks也是)
  • 在setup中引用useXxxx函数

6.1 以 MousePosition 为例

index.vue

<template>
    <p>mouse position {{x}} {{y}}</p>
</template>

<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
// import useMousePosition2 from './useMousePosition'

export default {
    name: 'MousePosition',
    setup() {
        const { x, y } = useMousePosition()
        return {
            x,
            y
        }

        // const state = useMousePosition2()
        // return {
        //     state
        // }
    }
}
</script>

useMousePosition.js

import { reactive, ref, onMounted, onUnmounted } from 'vue'

function useMousePosition() {
    const x = ref(0)
    const y = ref(0)

    function update(e) {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        console.log('useMousePosition mounted')
        window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
        console.log('useMousePosition unMounted')
        window.removeEventListener('mousemove', update)
    })

    return {
        x,
        y
    }
}

// 使用 reactive 实现,不用ref
// function useMousePosition2() {
//     const state = reactive({
//         x: 0,
//         y: 0
//     })

//     function update(e) {
//         state.x = e.pageX
//         state.y = e.pageY
//     }

//     onMounted(() => {
//         console.log('useMousePosition mounted')
//         window.addEventListener('mousemove', update)
//     })

//     onUnmounted(() => {
//         console.log('useMousePosition unMounted')
//         window.removeEventListener('mousemove', update)
//     })

//     return state
// }

export default useMousePosition
// export default useMousePosition2

7. Vue3 如何实现响应式?

# 深入响应性原理-vue 官方

7.1 回顾Vue2.x 的object.defineProperty

observe.js

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

7.1.1 Object.defineProperty的缺点

  • 深度监听需要一次性递归
    • 深度比较深可能会页面卡着不动
  • 无法监听新增属性/删除属性(Vue.set Vue.delete)
  • 无法原生监听数组,需要特殊处理

7.2 Proxy 基本使用

image.png

  • Proxy和Reflect很协调 参数也是对应好的
  • target就是原生的data,deleteProperty删除哪个对象的哪个属性
  • receiver就是proxyData,通过Reflect.get()的形式获取,通过Reflect.set()的形式设置

proxy-demo.js

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target)
        if (ownKeys.includes(key)) {
            console.log('get', key) // 监听
        }

        const result = Reflect.get(target, key, receiver)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true
        }

        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        // console.log('result', result) // true
        return result // 是否设置成功
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        // console.log('result', result) // true
        return result // 是否删除成功
    }
})

Reflect作用

  • 和Proxy能力一一对应
  • 规范化、标准化、函数式
  • 替代掉Object上的工具函数

image.png

7.3 vue3用Proxy 实现响应式

7.3.1 Proxy 实现优点

  • 深度监听,性能更好
  • 可监听 新增/删除 属性
  • 可监听数组变化

7.3.2 proxy-observe.js

// 创建响应式
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是对象或数组,则返回
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key) // 监听
            }
    
            const result = Reflect.get(target, key, receiver)
        
            // 深度监听
            // 性能如何提升的?
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true
            }
    
            // 判断是否是新增的数据
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的 key', key)
            } else {
                console.log('新增的 key', key)
            }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            // console.log('result', result) // true
            return result // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            // console.log('result', result) // true
            return result // 是否删除成功
        }
    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}

// 测试数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        city: 'beijing',
        a: {
            b: {
                c: {
                    d: {
                        e: 100
                    }
                }
            }
        }
    }
}

const proxyData = reactive(data)

7.3.3 Proxy总结

  • Proxy 能规避 Object.defineProperty 的问题
  • Proxy 无法兼容所有浏览器,无法polyfill

请持续关注

# 8. v-model参数的用法
# watch 和 watchEffect 的区别是什么?
# setup 中 如何获取组件实例?
# Vue3 为何比Vue2 快?
# Vite 是什么?
# Composition API 和 React Hooks 的对比