【Vue系列】Vue2 和 Vue 3 的差异

258 阅读11分钟

响应式

我们无法直接追踪对上述示例中局部变量的读写,原生 JavaScript 没有提供任何机制能做到这一点。但是,我们是可以追踪对象属性的读写的。

在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。

function reactive(obj) {
    return new Proxy(obj, { 
        get(target, key) { 
            track(target, key) 
            return target[key] 
        }, 
        set(target, key, value) { 
            target[key] = value 
            trigger(target, key) 
        } 
    }) 
} 

function ref(value) { 
    const refObject = { 
        get value() { 
            track(refObject, 'value') 
            return value 
        }, 
        set value(newValue) { 
            value = newValue 
            trigger(refObject, 'value') 
        } 
    } 
    return refObject 
}

reactive 的局限性:

  • 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumber 或 boolean 这样的原始类型
  • 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
  • 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,访问或赋值该变量是非响应式的,因为它将不再触发源对象上的 get / set 代理。我们将丢失响应性连接,注意这种“断开”只影响变量绑定——如果变量指向一个对象之类的非原始值,那么对该对象的修改仍然是响应式的。
  • 从 reactive() 返回的代理尽管行为上表现得像原始对象,但我们通过使用 === 运算符还是能够比较出它们的不同。
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
  • 当你将一个响应式对象的属性赋值或解构到一个本地变量时,注意这种“断开”只影响变量绑定——如果变量指向一个对象之类的非原始值,那么对该对象的修改仍然是响应式的。
  • 从 reactive() 返回的代理尽管行为上表现得像原始对象,但我们通过使用 === 运算符还是能够比较出它们的不同。 由于这些限制,建议使用 ref() 作为声明响应式状态的主要 API。

计算属性 - computed

Vue2 computed

var vm = new Vue({  
    el: '#example',  
    data: {  
        message: 'Hello'  
    },  
    computed: {  
        // 计算属性的 getter  
        reversedMessage: function () {  
            // `this` 指向 vm 实例  
            return this.message.split('').reverse().join('')  
        }  
    }  
})

Vue3 computed

<script setup> 
import { reactive, computed } from 'vue' 

const author = reactive({ 
    name: 'John Doe', 
    books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] 
    }) 
    // 一个计算属性 ref 
    const publishedBooksMessage = computed(() => { 
        return author.books.length > 0 ? 'Yes' : 'No' 
    }) 
</script> 
<template> 
    <p>Has published books:</p> 
    <span>{{ publishedBooksMessage }}</span> 
</template>

监听属性 - watch

Vue2 watch

<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->  
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->  
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>  
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>  
<script>  
var watchExampleVM = new Vue({  
    el: '#watch-example',  
    data: {  
        question: '',  
        answer: 'I cannot give you an answer until you ask a question!'  
    },  
    watch: {  
      // 如果 `question` 发生改变,这个函数就会运行  
        question: function (newQuestion, oldQuestion) {  
            this.answer = 'Waiting for you to stop typing...'  
            this.debouncedGetAnswer()  
        }  
    },  
    created: function () {  
          // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。  
          // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率  
          // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于  
        // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,  
        // 请参考:https://lodash.com/docs#debounce  
        this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)  
    },  
    methods: {  
        getAnswer: function () {  
            if (this.question.indexOf('?') === -1) {  
                this.answer = 'Questions usually contain a question mark. ;-)'  
                return  
            }  
            this.answer = 'Thinking...'  
            var vm = this  
            axios.get('https://yesno.wtf/api')  
                .then(function (response) {  
                    vm.answer = _.capitalize(response.data.answer)  
                })  
                .catch(function (error) {  
                    vm.answer = 'Error! Could not reach the API. ' + error  
                })  
        }  
    }  
})  
</script>

Vue3 中,watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

const x = ref(0) 
const y = ref(0) 
// 单个 ref 
watch(x, (newX) => { 
    console.log(`x is ${newX}`) 
}) 
// getter 函数 
watch( 
    () => x.value + y.value, 
    (sum) => { 
        console.log(`sum of x + y is: ${sum}`) 
    } 
) 
// 多个来源组成的数组 
watch([x, () => y.value], ([newX, newY]) => { 
    console.log(`x is ${newX} and y is ${newY}`) 
})

// 监听某个对象的属性
watch( 
    () => obj.count, 
    (count) => { 
        console.log(`count is: ${count}`) 
    } 
)

增加watchEffect

const todoId = ref(1) 
const data = ref(null) 
watch( 
    todoId, 
    async () => { 
        const response = await fetch( `https://jsonplaceholder.typicode.com/todos/${todoId.value}` ) 
        data.value = await response.json() 
    }, 
    { immediate: true } 
)

用 watchEffect 函数 来简化上面的代码。watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:

watchEffect(
    async () => { 
        const response = await fetch(
            `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
        ) 
        data.value = await response.json() 
    }
)

对于这种只有一个依赖项的例子来说,watchEffect() 的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。

watch vs watchEffect:

watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

v-for & v-if

在 Vue2 中,当 v-forv-if 处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。当你只想为部分项渲染节点时,这种优先级的机制会十分有用,如下:

// 只渲染未完成的 todo
<li v-for="todo in todos" v-if="!todo.isComplete">  
    {{ todo }}  
</li>

而在 Vue3 中,当 v-forv-if 同时存在于一个节点上时,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

<!-- 这会抛出一个错误,因为属性 todo 此时 没有在该实例上定义 --> 
<li v-for="todo in todos" v-if="!todo.isComplete"> 
    {{ todo.name }} 
</li>

在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

<template v-for="todo in todos"> 
    <li v-if="!todo.isComplete"> {{ todo.name }} </li> 
</template>

生命周期

Vue3 中,在 setup() 函数中返回的对象会暴露给模板和组件实例。setup() 自身并不含对组件实例的访问权,setup 在 beforeCreate 之前执行的,并且只执行一次,setup 在执行的时候,组件还没有被创建,组件实例的对象 this 不能用,所以在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。唯一可以使用 async setup() 的情况是,该组件是 Suspense 组件的后裔。

                                                                                                                                                                                       
Vue2 Vue3 描述
beforeCreate - 实例创建前
created - 实例创建后
beforeMount onBeforeMount 组件被挂载之前被调用
mounted onMounted 组件挂载完成后执行
beforeUpdate onBeforeUpdate 在组件即将因为响应式状态变更而更新其 DOM 树之前调用
updated onUpdated 组件因为响应式状态变更而更新其 DOM 树之后调用
beforeDestroy onBeforeUnmount 组件实例被卸载之前调用
destroyed onUnmounted 组件实例被卸载之后调用
activated onActivated 组件实例是 KeepAlive 缓存树的一部分,当组件被插入到 DOM 中时调用
deactivated onDeactivated 组件实例是 KeepAlive 缓存树的一部分,当组件从 DOM 中被移除时调用
- onErrorCaptured 在捕获了后代组件传递的错误时调用
- onRenderTracked 当组件渲染过程中追踪到响应式依赖时调用
- onRenderTriggered 当响应式依赖的变更触发了组件渲染时调用
- onServerPrefetch 组件实例在服务器上被渲染之前调用

⚠️特别说明:

  • onErrorCaptured: 注册一个钩子,在捕获了后代组件传递的错误时调用。错误可以从以下几个来源中捕获:

    • 组件渲染
    • 事件处理器
    • 生命周期钩子
    • setup() 函数
    • 侦听器
    • 自定义指令钩子
    • 过渡钩子

    错误传递规则

    • 默认情况下,所有的错误都会被发送到应用级的 app.config.errorHandler (前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。
    • 如果组件的继承链或组件链上存在多个 errorCaptured 钩子,对于同一个错误,这些钩子会被按从底至上的顺序一一调用。这个过程被称为“向上传递”,类似于原生 DOM 事件的冒泡机制。
    • 如果 errorCaptured 钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到 app.config.errorHandler
    • errorCaptured 钩子可以通过返回 false 来阻止错误继续向上传递。即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的 errorCaptured 钩子或 app.config.errorHandler 因这个错误而被调用。
  • onRenderTracked: 注册一个调试钩子,当组件渲染过程中追踪到响应式依赖时调用。这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。

  • onRenderTriggered: 注册一个调试钩子,当响应式依赖的变更触发了组件渲染时调用。这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。

  • onServerPrefetch: 注册一个异步函数,在组件实例在服务器上被渲染之前调用。

逻辑复用

Vue2 使用 mixins 实现组件的逻辑复用。

// 定义一个混入对象  
var myMixin = {  
    created: function () {  
        this.hello()  
    },  
    methods: {  
        hello: function () {  
            console.log('hello from mixin!')  
        }  
    }  
}  
  
// 定义一个使用混入对象的组件  
var Component = Vue.extend({  
    mixins: [myMixin]  
})  
  
var component = new Component() // => "hello from mixin!"

vue3 使用组合式函数实现组件的逻辑复用。

“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

比如,实现一个可复用的鼠标跟踪功能:

// mouse.js 
import { ref, onMounted, onUnmounted } from 'vue' 

// 按照惯例,组合式函数名以“use”开头 
export function useMouse() { 
    // 被组合式函数封装和管理的状态 
    const x = ref(0) 
    const y = ref(0) 
    // 组合式函数可以随时更改其状态。 
    function update(event) { 
        x.value = event.pageX 
        y.value = event.pageY 
    } 
    // 一个组合式函数也可以挂靠在所属组件的生命周期上 
    // 来启动和卸载副作用 
    onMounted(() => window.addEventListener('mousemove', update)) 
    onUnmounted(() => window.removeEventListener('mousemove', update)) // 通过返回值暴露所管理的状态 return { x, y } 
}

下面是它在组件中使用的方式:

<script setup> 
    import { useMouse } from './mouse.js' 
    const { x, y } = useMouse() 
</script> 
<template>
    Mouse position is at: {{ x }}, {{ y }}
</template>

Vue3 组合式函数和 mixins 的对比 mixins 主要有以下三个短板:

  1. 不清晰的数据来源:使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
  2. 命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
  3. 隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。 基于上述理由,不推荐在 Vue3 中使用 mixin,但 Vue3 为了项目迁移的需求和照顾熟悉它的用户,仍然保留了该功能。

Vue3和无渲染组件的对比

组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。

官方推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。

⚠️使用限制

组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。

这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:

  1. 将生命周期钩子注册到该组件实例上
  2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。

自定义指令

自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。

只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。 Vue2 局部注册自定义指令:

directives: {  
    focus: {  
        // 指令的定义  
        inserted: function (el) {  
            el.focus()  
        }  
    }  
}  

Vue2 全局注册某个自定义指令:

Vue.directive('focus', {  
    // 当被绑定的元素插入到 DOM 中时……  
    inserted: function (el, binding, vnode) {  
        // 聚焦元素  
        el.focus()  
    }  
})

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数的参数 (即 elbindingvnode 和 oldVnode):

  • el:指令所绑定的元素,可以用来直接操作 DOM。

  • binding:一个对象,包含以下 property:

    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。

  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

Vue3 自定义局部指令:

<script setup>
    // 在模板中启用 v-focus
    const vFocus = {
        mounted: (el) => el.focus()
    } 
</script>
<template
    <input v-focus />
</template>

Vue3 自定义全局指令:

const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('color', (el, binding) => { 
    // 这会在 `mounted` 和 `updated` 时都调用
    el.style.color = binding.value 
})

一个指令的定义对象可以提供几种钩子函数 (都是可选的):

const myDirective = {
    // 在绑定元素的 attribute 前
    // 或事件监听器应用前调用
    created(el, binding, vnode, prevVnode) {
        // 下面会介绍各个参数的细节
    }, 
    // 在元素被插入到 DOM 前调用
    beforeMount(el, binding, vnode, prevVnode) {}, 
    // 在绑定元素的父组件 
    // 及他自己的所有子节点都挂载完成后调用 
    mounted(el, binding, vnode, prevVnode) {}, 
    // 绑定元素的父组件更新前调用 
    beforeUpdate(el, binding, vnode, prevVnode) {}, 
    // 在绑定元素的父组件 
    // 及他自己的所有子节点都更新后调用 
    updated(el, binding, vnode, prevVnode) {}, 
    // 绑定元素的父组件卸载前调用 
    beforeUnmount(el, binding, vnode, prevVnode) {}, 
    // 绑定元素的父组件卸载后调用 
    unmounted(el, binding, vnode, prevVnode) {} 
}

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

依赖注入

Vue2 中,provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。 inject 选项应该是:

  • 一个字符串数组,或

  • 一个对象,对象的 key 是本地的绑定名,value 是:

    • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或

    • 一个对象,该对象的:

      • from property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)
      • default property 是降级情况下使用的 value
// 父级组件提供 'foo'  
var Provider = {  
    provide: {  
        foo: 'bar'  
    },  
    // ...  
}  
  
// 子组件注入 'foo'  
var Child = {  
    inject: ['foo'],  
    created () {  
        console.log(this.foo) // => "bar"  
    }  
    // ...  
}

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

Vue3 中

<script setup> 
import { provide } from 'vue' 

provide(/* 注入名 */ 'message', /* 值 */ 'hello!') 
</script>
<script setup> 
import { inject } from 'vue' 

const message = inject('message') 
</script>

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

组件 v-model

Vue2 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的

Vue.component('base-checkbox', {  
    model: {  
        prop: 'checked',  
        event: 'change'  
    },  
    props: {  
        checked: Boolean  
    },  
    template: `  
        <input  
            type="checkbox"  
            v-bind:checked="checked"  
            v-on:change="$emit('change', $event.target.checked)"  
        >  
    `  
})

这时,可以在这个组件上使用 v-model

<base-checkbox v-model="lovingVue"></base-checkbox>

在使用时,这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。

Vue3 中,v-model 使用在一个组件上时, 会被展开为如下的形式:

<CustomInput
    :model-value="searchText"
    @update:model-value="newValue => searchText = newValue" 
/>

<CustomInput> 组件内部需要做两件事:

  1. 将内部原生 <input> 元素的 value attribute 绑定到 modelValue prop
  2. 当原生的 input 事件触发时,触发一个携带了新值的 update:modelValue 自定义事件
<!-- CustomInput.vue -->
<script>
export default {
    props: ['modelValue'],
    emits: ['update:modelValue']
} 
</script>

<template>
    <input
        :value="modelValue"
        @input="$emit('update:modelValue', $event.target.value)"
    />
</template>

v-model 可以在这个组件上正常工作了:

<CustomInput v-model="searchText" />

状态管理

组件之间的共享部分状态 Vue2 的状态管理使用:

  1. store 模式
    var store = {  
    debug: true,  
    state: {  
        message: 'Hello!'  
    },  
    setMessageAction (newValue) {  
        if (this.debug) console.log('setMessageAction triggered with', newValue)  
        this.state.message = newValue  
    },  
    clearMessageAction () {  
        if (this.debug) console.log('clearMessageAction triggered')  
        this.state.message = ''  
    }  
}

store 中 state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么。

每个实例/组件仍然可以拥有和管理自己的私有状态:

var vmA = new Vue({  
    data: {  
        privateState: {},  
        sharedState: store.state  
    }  
})  
  
var vmB = new Vue({  
    data: {  
        privateState: {},  
        sharedState: store.state  
    }  
})
  1. Vuex

Vue3 的状态管理可以使用:

  1. 使用reactive 创建一个响应式对象,并将其导入到多个组件中
// store.js
import { reactive } from 'vue'
export const store = reactive({
    count: 0,
    increment() { this.count++ } 
})
  1. 使用其他响应式 API 例如 ref() 或是 computed(),或是甚至通过一个组合式函数来返回一个全局状态
  2. Pinia

异步组件

Vue2 局部注册异步组件:

new Vue({  
    // ...  
    components: {  
        'my-component': () => import('./my-async-component')  
    }  
})

Vue3 异步组件

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
)

多根节点

Vue2 中,一个组件只允许有一个根节点,多个根节点会报错 Vue3 支持多个根节点,会默认包裹一个 fragment 标签,但是不会渲染到页面。

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

新增组件

  • Teleport

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

这类场景最常见的例子就是全屏的模态框。

<button @click="open = true">Open Modal</button> 
<Teleport to="body"> 
    <div v-if="open" class="modal"> 
        <p>Hello from the modal!</p> 
        <button @click="open = false">Close</button> 
    </div> 
</Teleport>

<Teleport> 接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue“把以下模板片段传送到 body 标签下”。

  • Suspense

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 <Suspense>,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。

有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

<Suspense> 可以等待的异步依赖有两种:

  1. 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件。
  2. 异步组件

<Suspense> 组件有两个插槽:#default 和 #fallback。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。

<Suspense> 
    <!-- 具有深层异步依赖的组件 --> 
    <Dashboard /> 
    <!-- 在 #fallback 插槽中显示 “正在加载中” --> 
    <template #fallback> 
        Loading... 
    </template> 
</Suspense>

在初始渲染时,<Suspense> 将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,<Suspense> 会进入完成状态,并将展示出默认插槽的内容。

如果在初次渲染时没有遇到异步依赖,<Suspense> 会直接进入完成状态。

进入完成状态后,只有当默认插槽的根节点被替换时,<Suspense> 才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成 <Suspense> 回退到挂起状态。

发生回退时,后备内容不会立即展示出来。相反,<Suspense> 在等待新内容和异步依赖完成时,会展示之前 #default 插槽的内容。这个行为可以通过一个 timeout prop 进行配置:在等待渲染新内容耗时超过 timeout 之后,<Suspense> 将会切换为展示后备内容。若 timeout 值为 0 将导致在替换默认内容时立即显示后备内容。

<Suspense> 组件会触发三个事件:pendingresolve 和 fallbackpending 事件是在进入挂起状态时触发。resolve 事件是在 default 插槽完成获取新内容时触发。fallback 事件则是在 fallback 插槽的内容显示时触发。

例如,可以使用这些事件在加载新组件时在之前的 DOM 最上层显示一个加载指示器

<Suspense> 组件自身目前还不提供错误处理,不过你可以使用 errorCaptured 选项或者 onErrorCaptured() 钩子,在使用到 <Suspense> 的父组件中捕获和处理异步错误。

createRenderer

创建一个自定义渲染器。通过提供平台特定的节点创建以及更改 API,你可以在非 DOM 环境中也享受到 Vue 核心运行时的特性。

具体详见官方文档

选项式 API VS 组合式 API

Vue2 是选项式 API, Vue3 是组合式 API,为什么要用组合式 API

  • 更好的逻辑复用

    组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷

  • 更灵活的代码组织

    选项式 API 在默认情况下就能够让人写出有组织的代码:大部分代码都自然地被放进了对应的选项里。然而,选项式 API 在单个组件的逻辑复杂到一定程度时,会面临一些无法忽视的限制。这些限制主要体现在需要处理多个逻辑关注点的组件中。处理相同逻辑关注点的代码会被强制拆分在不同的选项中,位于文件的不同部分。在一个几百行的大组件中,要读懂代码中的一个逻辑关注点,需要在文件中反复上下滚动,这并不理想。

    另外,如果我们想要将一个逻辑关注点抽取重构到一个可复用的工具函数中,需要从文件的多个不同部分找到所需的正确片段,我们无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。此外,我们现在可以很轻松地将这一组代码移动到一个外部文件中,不再需要为了抽象而重新组织代码,大大降低了重构成本,这在长期维护的大型项目中非常关键。

  • 更好的类型推导

    选项式 API 的类型推导在处理 mixins 和依赖注入类型时依然不甚理想。

    组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多!

  • 更小的生产包体积

    搭配 <script setup> 使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。这是由于 <script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 <script setup> 中定义的变量,无需从实例中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。

整体比较

Vue3 相比 Vue2:

  • 速度更快

    • 重写了虚拟Dom实现
    • 编译模板的优化
    • 更高效的组件初始化
    • undate性能提高1.3~2倍
    • SSR速度提高了2~3倍
  • 体积减少

    • 通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的

      能够tree-shaking,有两大好处:

      • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大
      • 对使用者,打包出来的包体积变小了

    vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多

  • 更易维护

    • 组合式 API 可与现有的Options API一起使用
    • 灵活的逻辑组合与复用
    • Vue3模块可以和其他框架搭配使用
  • 更好的 Typescript 支持

  • 编辑器重写

  • 更接近原生

    • 可以自定义渲染 API
  • 更易使用

    • 响应式 Api 暴露出来
    • 轻松识别组件重新渲染原因
  • diff算法优化,vue2中的虚拟dom是全量的对比,vue3新增了静态标记(patchflag),与上次虚拟节点对比时,只对比带有(patchflag)标记的节点(动态数据所在的节点),以减少非动态数据的比对时的性能消耗。

  • 静态提升,vue2无论元素是否参与更新,每次都会重新创建然后再渲染。vue3对于不参与更新的元素,会做静态提升(提升到render函数之外),只会被创建一次,在渲染时直接复用即可。

有问题欢迎指正,谢谢!

参考文档: