Vue3核心

81 阅读24分钟

Vue3的重大更新

创建Vue应用

Vue3不像Vue2那样会使用默认导出一个叫做Vue的构造函数的方式,而是使用具名导出一个叫做createApp的方法,调用该方法时传递一个App组件对象,它就可以返回一个app应用,app应用中有一个mount方法,调用该方法就可以实现真实DOM的挂载

// main.js

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

this指向

在Vue2中,组件配置中的函数中的this,均指向该组件的组件实例

而在Vue3中,组件配置中的函数中的this,则是指向一个Proxy对象,Proxy对象的所代理的就是该组件的组件实例

image.png

不管是Vue2还是Vue3,模版环境都是组件配置中的this环境,模版中访问的数据,默认都是在访问this.xxx

Composition API

Vue2中的组件配置,每个配置项就是配置中的一个属性,这称为Option API

Option API的一大特点是实现同一个功能是所涉及的一系列属性或方法,有很大可能会分布在不同的配置项中,即实现某个功能的代码是较为零散的,这让代码阅读起来变得十分困难

option api.jpeg

而Vue3除了支持以Option API的形式书写组件配置,还支持另一种叫做Composition API的形式来书写组件配置

Composition API可以将实现同一功能的大量代码聚合到一起,也让代码阅读起来更加方便

composition api.jpg

由于Composition API可以将一个功能的实现代码聚合到一起,因此可以很轻松地将这部分代码提取出来,从而形成一个独立的模块,这样组件配置中的代码就可以变得十分简洁

要使用Composition API,就需要在组件配置中书写一个setup配置项,该配置项是一个函数,具体的功能实现代码就需要书写在该函数中

setup函数会在组件的props被赋值完成之后、所有生命周期钩子函数运行之前被调用

另外,setup函数中的this是undefined,因此在该函数中是不可以使用this关键字的

setup函数可以接收两个参数,分别是当前组件的props,以及组件的上下文ctx,通过props参数可以获取到组件的所有属性,通过ctx参数可以向父组件抛出事件(具体为ctx.emit("xxx", value)

setup函数需要返回一个对象,该对象中的所有属性,最终都会附着在组件实例上

export default {
    props: [],
    setup(props, ctx){
        // ...
        return {};
    }
}

setup函数所返回的对象,其身上的所有属性,是不具有响应式的(附着到组件实例上后同样也不会具有),除非其值被ref函数进行了包裹

ref函数会将传递进去的数据封装成为一个对象,该对象的value属性是一个访问器属性,原始数据就是作为该属性的值存在的。由于value属性是一个访问器属性,因此在访问value属性时Vue就可以进行一些处理,比如重渲染页面等

如果通过Proxy组件实例代理对象访问ref对象,则Proxy对象会进行代理并转变为访问目标ref对象的value属性然后将值返回,因此在页面模版中可以直接书写组件实例上的ref对象属性的名称来获取其对应的value属性值

对于非ref对象属性,则就是正常访问啥Proxy组件实例代理对象就代理访问啥

image.png

但在setup函数中,由于其内部获取不到this,因此要想访问某个ref对象的value属性,就必须书写为ref.value的形式

<template>
<div>
	value: {{ count }}
    type: {{ typeof(count) }}
</div>
</template>

<script>
import { ref } from "vue";

export default {
	setup(){
        let count = ref(0);
        /** 
        count = {
			value: {
				getter(){},
				setter(newVal){}
			}
        }
        */
        const increase = () => {
            count.value++;
        };
        return {
            count
        }
    }
}
</script>

watchEffect

import { watchEffect } from "vue";

export default {
    setup(){
        let count = ref(0);
        watchEffect(()=>{
    		count.value;
		});
    }
}

watchEffect是一个函数,调用时需要传递一个回调函数,在该回调函数中使用到的响应式数据,只要其值发生了变化,回调函数就会重新被调用

生命周期函数

在Composition API下使用生命周期函数,需要对函数进行引入

在生命周期函数中需要传入一个回调函数,当组件的生命周期到达某个阶段时会触发其中的回调函数

import { onMounted, onUnmounted } from "vue";

export default {
    setup(){
        onMounted(()=>{
            console.log("mounted");
        });
        onUnmounted(()=>{
            console.log("unmounted");
        });
        return {};
    }
}

计算属性

在Composition API下使用计算属性,需要引入computed函数

computed中需要传递一个参数,参数可以是一个具有getter和setter的对象,也可以是一个回调函数,此时该回调函数会做为计算属性的getter存在

computed运行过后会返回一个ref对象,如果其所依赖的响应式数据发生了变化,则computed中的getter就会重新执行,然后getter就会修改ref对象的value

<template>
<h1>{{ computedNumRef }}</h1>
<button @click="countRef++">countRef++</button>
</template>

<script>
    import { ref, computed } from "vue";

    export default {
        setup() {
            const countRef = ref(1);
            const computedNumRef = computed(() => {
                return countRef.value * 2;
            });
            return {
                countRef,
                computedNumRef
            };
        },
    };
</script>

插件的使用

以路由插件为例

Vue3所使用的路由插件为VueRouter4,而为Vue3应用VueRouter4的方式也与之前不同,VueRouter4并没有默认导出VueRouter构造函数,而是使用具名导出createRouter函数来创建路由对象

import { createRouter, createWebHistory } from "vue-router";
import Home from "./views/Home.vue";

const routes = [
    {
        path: "/",
        component: Home
    }
]

const router = createRouter({
    history: createWebHistory("/app"),	// 相当与设置了mode: "history" 和 base: "/app"
    routes								// routes配置和原来一样
});

export default router;
// main.js
import { createApp } from "vue";
import router from "@/router/index.js";
import App from "./App.vue";

createApp(App).use(router).mount("#app");

createApp().use()返回的还是createApp()的返回内容,因此支持链式use

在组件配置中使用router时,除setup配置外,其它配置均可以使用this.$router来访问到router对象,而在setup配置中,则需要使用vue通过具名导出的useRouter函数来获取到router对象

<script>
    import { useRouter, useRoute } from "vue-router";

    export default {
        created(){
            this.$router.push(...);
		},
        setup(){
            const router = useRouter();
            const route = useRoute();
            router.push(...);
		}
    }
</script>

其它细节

  • 一个SFC中的模版允许是多根的

  • 在vue2中,beforeCreate钩子函数运行时组件实例中还未注入组件配置中的内容

    而在vue3中,beforeCreate钩子函数运行时,从组件实例中就已经可以获取到props以及setup函数的返回值中的属性了(但data等其他配置中的内容就还获取不到)

Vite原理

Vite是一个脚手架,它可以用于搭建Vue工程,也可以搭建React工程

对于webpack,它对工程进行打包时会根据入口文件分析出它的模块依赖关系,将这些一系列的模块文件的内容合并到最终的打包结果中

打包完成后,如果是在开发阶段且配置了devServer,则Webpack还会开启开发服务器,之后浏览器在请求开发服务器时,开发服务器就会将打包结果中的代码资源响应给浏览器

image.png

之所以说webpack慢,就是因为webpack对代码进行打包的过程时间长,特别是在模块众多的大型项目之中,这一点尤为明显

相比之下,vite在开发阶段的优势就尤为明显,因为vite在开发阶段对工程模块的处理与webpack完全不同

vite在开发阶段并不会对代码进行打包,而是直接启动一个开发服务器,其它什么也不做,之后开发服务器根据浏览器请求,将相应内容响应过去,所以无论工程多么得大,vite在最开始启动时都不会变慢

最开始,浏览器会请求vite的开发服务器工程根目录下的index.html文件资源,在该html文件中,会使用一个script元素引入一个工程src目录下的某个入口js文件(默认是src/main.js),该js文件会被视为一个模块(因为script元素的type属性为"module")

当浏览器解析到此script元素时,就又会向vite的开发服务器请求该js模块,于是开发服务器就将模块内容响应给浏览器

浏览器执行js模块中的代码时,就会执行到import指令,因此又会向vite开发服务器请求其他的模块资源

vite的开发服务器并不是将模块的原始内容响应给浏览器,而是在响应之前会先对代码进行编译转换,之后把编译后的内容响应过去,例如:将非js模块内容编译为普通的JS代码内容等,因此在vite工程的js模块中可以直接import样式文件、图片文件、vue文件等

对于vue模块,开发服务器将其内容响应给浏览器之前,会先进行编译,编译的其中之一就是将模版编译为render函数,即模版预编译,因此在vite搭建的vue工程中也是有预编译步骤的

对于内容已经是js代码的js模块,vite会对模块中使用到的路径进行编译,例如:将相对路径编译为绝对路径

需要注意的是,在使用vite搭建的工程中,开发者编写的源代码只能使用ES Module模块化标准进行导入导出,因为vite并不会对导入导出代码进行编译转换

image.png

虽然说使用vite会使浏览器不断向服务器发送的请求,不过对于大型项目来说,相比于webpack的打包时间,vite工程中资源请求时间反而是更快的,因为浏览器请求的是本地的开发服务器,本地开发服务器从本机中读取文件响应给本机浏览器所需的时间本来就很少,因此可以认为使用vite搭建工程的开发效率是完胜webpack的

总结来说,在开发阶段,vite并不会将入口模块与其所依赖的其他模块进行合并打包,而是让浏览器在需要使用到某个模块时再通过请求响应方式让开发服务器将资源响应给浏览器,因此也就没有了分析模块依赖、合并代码等带来的时间开销,因此vite的启动速度非常快

webpack打包时,不管一开始是否会使用到依赖的模块,都会将其编译并合并进来,而vite只有执行到某个导入语句时才会请求相应资源,之后开发服务器再在响应之前对代码进行编译,这种按需动态编译的方式,能够将代码编译的时间均匀地分配出去,所以项目越复杂,vite的优势就更为明显

在热替换方面,当改动了某个模块的代码时,浏览器只需要重新请求该模块即可,而不用向webpack那样需要重新对该模块和它所依赖的其他进行分析合并,因此热替换效率也更高

在生产阶段,vite和webpack的做法就非常相似了,同样都会分析代码的依赖关系,然后进行打包合并,形成最终的打包结果

注意:使用vite搭建的vue工程,在引入组件时需要加上.vue后缀,并且导入路径中的@符号不代表src目录

配置代理

在工程根目录下建立vite.config.js文件,并书写下面内容:

module.exports = {
    proxy: {
        "/api": {
            target: "http://www.mysite.com",
            changeOrigin: true,
            rewrite: (path)=>path.replace(/^\/api/, "")	// 将请求的url中的"/api"替换为""
        }
    }
}

和Webpack配置代理的方式基本相同

关于Vue3的效率提升

静态提升

静态节点:

  • 普通元素节点
  • 没有绑定静态属性和内容的节点

Vue3中的编译器会将template编译为render函数,而在编译过程中,编译器会挑选出模版中的静态节点,并将这些节点进行提升

当组件发生了重渲染时,组件模版中的静态节点就可以直接使用原来已经生成好的,而不需要和动态节点一样需要重新生成,减少了创建虚拟节点所带来的时间开销

const hoisted = createdVNode("h1", null, "Hello World");

function render(){
    // 直接使用 hoisted 即可
}

对于vue2的编译器,它并没有做静态提升处理,组件的每次重渲染都需要重新生成render函数,而render函数的重新生成就会导致静态的虚拟节点被重新创建,降低了效率

除了静态节点会被提升之外,动态节点中的静态属性也会被提升

<template>
	<div class="container">{{user.name}}</div>
</template>

Vue2中的静态节点:

render(){
    createVNode("h1", null, "Hello World")
}

Vue3中的静态节点:

const hoisted = {
    class: "container"
};

function render(){
    createdVNode("div", hoisted, user.name);
}

预字符串化

Vue3的编译器功能非常强大,当编译器编译模版时,如果发现了大量的连续的静态节点,则会将它们编译为一个普通的字符串节点

<div class="menu-bar-container">
    <div class="logo">
        <h1>logo</h1>
    </div>
    <ul class="nav">
        <li><a href="">menu</a></li>
        <li><a href="">menu</a></li>
        <li><a href="">menu</a></li>
        <li><a href="">menu</a></li>
        <li><a href="">menu</a></li>
    </ul>
    <div class="user">
        <span>{{ user.name }}</span>
    </div>
</div>
const hoisted = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>")

预字符串化可以减少虚拟DOM树的结点量,因此减少了创建虚拟节点的工作量,更可以减少内存消耗以及编译后的代码体积

由于字符串节点中只包含静态节点,因此字符串节点本身也是静态节点,它也可以被提升出来

image.png

image.png

缓存事件处理函数

节点所绑定的事件处理函数通常不会发生变化,对于这些不会变化的事件处理函数,Vue3的编译器会将它们缓存起来,之后重渲染时就会使用缓存的处理函数

<template>
	<button @click="count++">plus</button>
</template>

Vue2的渲染函数:

render(ctx){
    return createdVNode("button", {
        onClick($event){
            ctx.count++;
        }
    });
}

Vue3的渲染函数:

render(ctx, _cache){
    return createdVNode("button", {
        onClick: _cache[0] || (_cache[0] = ($event) => (ctx.count++))
    });
}

Block Tree

Vue2在对比新旧VNode Tree的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能逐个进行比较,这就将时间浪费在了对比大量的静态节点上

而Vue3在这方面就进行了优化,Vue3的编译器会将以某个节点为根的树中的所有动态节点寻找出来,并将它们记录在该根节点的某个属性数组中,而该根节点就称为以它为根的Block Tree的Block Node,之后在对比新旧虚拟DOM树中的某个子树Block Tree时,只需要对比该Block Tree的Block Node所记录的一系列动态节点即可,这样就完美地避开了对大量静态节点的对比

对于不稳定的虚拟DOM树,Vue3编译器就会将不稳定的部分以及它所有的后代节点划分成为一个Block Tree

<form>
    <div>
        <label>账号:</label>
        <input v-model="user.loginId" />
    </div>
    <div>
        <label>密码:</label>
        <input v-model="user.loginPwd" />
    </div>
</form>

image.png

image.png

PatchFlag

Vue2在对比新旧VNode时,只能挨个地将VNode中的所有属性依次进行对比,降低了对比速度,而Vue3能够做到在对比某一个VNode时,只对VNode中可能发生变化的属性进行对比

Vue3依托于其强大的模版编译器,编译器能够发现模版中哪些节点是动态的,甚至能够发现这些动态节点中的哪些属性是动态,并对这些动态的属性进行标记,之后在对比新旧树的虚拟节点时,只需要对比这些动态的属性即可

<div class="user" data-id="1" title="user name">
    {{user.name}}
</div>

image.png

API和响应式的变化

为什么Vue3会去除Vue构造函数?

在Vue2中,调用构造函数的静态方法会影响所有vue应用,这不利于隔离不同的应用

<template>
<div id="app1"></div>
<div id="app2"></div>
</template>

<script>
Vue.use(...);
Vue.mixin(...);
Vue.component(...);
Vue.directive(...);
// 上面的方法由于是在Vue构造函数上使用的,因此会影响所有应用
</script>

通过Vue构造函数创建的实例集成了太多的功能,不利于tree shaking,Vue3通过将这些功能通过普通函数导出,就能够充分利用tree shaking来优化打包结果体积

Vue构造函数并没有很好的与创建组件实例的构造函数VueComponent区别开,通过Vue构造函数创建出的对象,是一个vue应用,但也像是一个组件实例,因此造成了概念上的混乱

而通过Vue3中的createApp函数创建出来的对象,则就是一个明确的vue应用,该对象内部只包含了应用所需要的属性和方法,而不再是一个特殊的组件

通过createApp创建出来的Vue应用,其中包含了很多方法,例如:为Vue应用使用插件的方法use、将其他配置混合到Vue应用的方法mixin、为Vue应用注册全局组件的方法component,这些方法会在内部将Vue应用返回,因此支持链式编程

调用Vue应用上的这些方法只会影响到该Vue应用

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).use(...).mixin(...).component(...).mount("#app");

组件实例中的API

在Vue3中,除setup函数配置外,其它组件配置中的this是一个Proxy对象(该Proxy代理着组件实例对象),并且仅提供了以下属性和方法:

  • $data
  • $props
  • $el
  • $options
  • $parent
  • $root
  • $slots
  • $refs
  • $attrs
  • $watch()
  • $emit()
  • $forceUpdate()
  • $nextTick()

注意:createApp函数创建的是Vue应用,应用并不是组件实例

数据响应式

本节仅探讨使用option API下的数据响应式,不包括setup函数中对数据实现的响应式

Vue3完成数据响应式工作的时间与Vue2相同,都是在beforeCreate生命周期之后,created生命周期之前

不过Vue2使用Object.defineProperty实现数据响应式的,而Vue3则是使用Proxy API完成数据响应式

Vue2实现数据响应式时,会对深度遍历对象上的所有属性,并通过Object.defineproperty为它们设置getter和setter。并且由于为属性添加getter和setter的操作仅在注入阶段发生,因此对应新增的属性,是不具有数据响应式的(为了处理该问题,Vue2便在组件实例中加入了set和\delete)

Vue3实现数据响应式时,仅为对象外层包裹一个Proxy即可,而不需要深度遍历对象的属性,因此实现响应式的效率更高,之后就可以使用Proxy对象对原始对象进行访问,而Proxy作为代理者,就可以对操作进行响应的处理。由于Proxy能够拦截很多种操作,例如:对象属性的增加和删除操作,因此当通过Proxy对被代理的对象的属性进行增加删除时,Vue也能够收到通过并进行处理(因此Vue3中的组件实例中就删除了set和\delete)

Vue3也可以做到对数组成员重新赋值的动作的监控,这也能触发数据响应式

使用Proxy对被代理对象属性进行访问时,如果访问的属性的值是原始类型数据,则Proxy会将该原始数据属性值返回;如果访问的属性的值是引用类型数据时,则Proxy会将该引用类型属性值外层再包裹一个Proxy,然后把这个Proxy返回

image.png

模版的变化

v-model

在Vue2中v-model指令只能双向绑定一个数据,因此便提供了两种数据双向绑定的方法:v-model指令和.sync修饰符,而在Vue3中,它将.sync修饰符移除掉了,同时让v-model指令能够支持绑定多个数据

在Vue2中,无论是为元素还是自定义组件绑定v-model,最终v-model默认都会被转换为valueinput事件,而Vue3为了让v-model在自定义组件上能够做到多数据的双向绑定,则对v-model转换后的结果进行了修改

在默认情况下,对自定义组件上使用v-model,会被转换为modelValue属性和update:modelValue事件

对于普通的表单元素,v-model和Vue2中没有区别,还是会被转换为value属性和input事件

<Child v-model="data"></Child>

等价于

<Child :modelValue="data" @update:modelValue="data = $event"></Child>

要让v-model绑定多个数据,则可以为v-model指令添加参数

<Child v-model:attr="data"></Child>

添加了参数后的v-model,则会被转换为下面的内容

<Child :attr="data" @update:attr="data = $event"></Child>

基于Vue3的v-model的如上变化,组件配置中也就不需要model配置了

Vue3还给v-model加入了另一项功能,即允许给v-model添加自定义修饰符

假设为Comp组件使用添加了cap修饰符的v-model指令来绑定text属性,则在Comp组件内部就可以声明一个叫做textModifiers的属性,该属性是一个对象,对象中就会包含一个叫做cap的属性,其值为true

如果使用的是默认的v-model,即v-model绑定的属性时modelValue,则组件内对应的prop就叫做modelModifiers(而不是modelValueModifiers)

image.png

<!-- Comp -->
<script>
export default {
    props: {
		text: String,
        textModifiers: {
            default: ()=>({})
        }
    },
    created(){
        console.log(this.textModifiers.cap);		// true
    }
}
</script>

v-if与v-for

在Vue2中,v-for指令的优先级是高于v-if的,因此可以书写下面这样的代码:

<template>
<ul>
    <li v-for="(item, index) in products" :key="index" v-if="item.total > 0"></li>
</ul>
</template>

<script>
export default {
	data(){
		products: [
            { name: "iPhone", total: 0 },
            { name: "XiaoMi", totle: 10 },
            { name: "HUAWEI", totle: 20 }
        ]
    }
}
</script>

虽然允许这么书写,但官网前列建议不要这样做,因为这可能会带来效率上的问题

v-for和v-if一起使用时,说明数组中一定是包含不希望显示的元素,需要使用条件判断进行筛选,而对于不希望显示的元素,尽管它不会显示到页面中,但每次模版重新渲染时都需要将整个数组进行循环遍历,因此会将不显示的节点也进行判断,这是没有意义且浪费效率的

正确的做法是使用计算属性对数组进行过滤,然后让v-for循环计算属性值,这么做的好处在于,只要模版的重渲染不是数组变化所导致的,则计算属性就可以使用缓存的结果,因此也就不需要对不显示的元素重新进行判断,提高了页面渲染效率

由于官网不希望开发者书写上面那样的代码,因此而在Vue3中,干脆直接将v-if的优先级设置得高于v-for,这就完全避免了开发者会书写出上面的代码

key

在Vue2中,如果需要循环的元素不止一个根节点,则通常会使用一个template标签将这些根元素包裹起来,然后在template上进行v-for,在这种情况下,key属性的绑定并不能在template上,而是需要在template所包裹的若干个根元素上

<template>
<div>
    <template v-for="(item, index) in products">
		<span :key="index">商品名称:{{item.name}}</span>
    	<span :key="index">商品库存:{{item.title}}</span>
    </template>
</div>
</template>

<script>
export default {
	data(){
		products: [
            { name: "iPhone", total: 0 },
            { name: "XiaoMi", totle: 10 },
            { name: "HUAWEI", totle: 20 }
        ]
    }
}
</script>

而在Vue3中,就要求必须在绑定v-for指令的标签上绑定key,包括template标签

<template>
<div>
    <template v-for="(item, index) in products" :key="index">
		<span>商品名称:{{item.name}}</span>
    	<span>商品库存:{{item.title}}</span>
    </template>
</div>
</template>

<script>
export default {
	data(){
		products: [
            { name: "iPhone", total: 0 },
            { name: "XiaoMi", totle: 10 },
            { name: "HUAWEI", totle: 20 }
        ]
    }
}
</script>

在Vue2中,当使用v-if、v-else-if、v-else分支指令时,可能需要为不同分支的根元素添加唯一的key,这是为了避免diff在对比时复用原来条件分支中的元素,例如下面那这种情况:

<template>
<div>
    <div v-if="isAccount">
        <span>账号:</span>
        <input type="text" />
    </div>
    <div v-else>
        <span>手机号:</span>
        <input type="text" />
    </div>
    <button @click="isAccount = !isAccount">切换登录方式</button>
</div>
</template>

<script>
export default {
	data(){
        return {
            isAccount: true
        }
    }
}
</script>

在不为完全不为分支中的结点绑定唯一的key属性时,可能会导致原来的元素被复用,例如:当使用账号登录时填写了账号,然后点击了切换登录方式时,会出现文本框中的文本未被清空的情况,这就是因为文本框元素被复用导致的,处理此问题的方法是为input元素或绑定分支指令的元素上设置唯一的key值

而在Vue3中,则不需要关心该问题,因为Vue3会自动为分支根节点上绑定唯一的key值,保证了不同分支中的节点不会被复用

Fragment

在Vue3中,一个组件的模版内部可以出现多个根节点

组件的变化

异步组件

Vue3定义组件需要使用defineAsyncComponent,而在调用defineAsyncComponent时,需要传入一个返回值为相关数据是组件配置对象的Promise的函数,或者对象

具体如下:

<template>
	<AsyncComp />
</template>

<script>
import { defineAsyncComponent } from "vue";

const AsyncComp = defineAsyncComponent(()=>import("./Comp.vue"));

export default {
    components: {
        AsyncComp
    }
}
</script>

<template>
	<AsyncComp />
</template>

<script>
import { defineAsyncComponent } from "vue";
import LoadingComp from "./LoadingComp.vue";
import ErrorComp from "./ErrorComp.vue";

const AsyncComp = defineAsyncComponent({
	loader: () => import("./Comp.vue"),		// 相当于component属性
    loadingComponent: LoadingComp,			// 相当于loading
    errorComponent: ErrorComp				// 相当于error
});

export default {
    components: {
        AsyncComp
    }
}
</script>

在路由中使用异步组件的方式也和上面相同:

// routes.js
import { defineAsyncComponent } from "vue";

const routes = [
    {
        path: "/",
        component: defineAsyncComponent(()=>import("./views/Home.vue"));
    }
];

export default routes;

内置组件Teleport

在Vue2中,真实DOM节点的相对位置取决于模版中虚拟节点的相对位置,即逻辑结构能够映射出物理结构

而Vue3中的Teleport内置组件则可以让其内部的虚拟节点的逻辑结构与物理结构相分离

<template>
<Teleport to=".container">
    <div class="box">box...</div>
</Teleport>
<div class="container">
    <span>content...</span>
</div>
</template>

通过Teleport组件,可以将其内部的所有虚拟节点转移到to选择器所对应的虚拟节点的最后,即上面的代码等价于

<template>
<div class="container">
    <span>content...</span>
    <div class="box">box...</div>
</div>
</template>

Reactivity API

所有数据响应式的API都是可以脱离组件进行使用

获取响应式数据

Vue3中提供了如下API来将普通数据转换为响应式数据:

  1. reactive

    传入普通对象(也可以是数组),返回Proxy代理对象,该Proxy对象会对传入的对象进行代理

    如果从Proxy对象中访问的是一个对象成员,则获取到的也将是一个代理着该对象成员的Proxy对象

    import { reactive } from "vue";
    
    const data = {
        name: "zhangsan",
        age: 20,
        addr: {
            province: "GuangDong",
            city: "ShanTou"
        }
    };
    
    const dataProxy = reactive(data);			// Proxy {...}
    
  2. readonly

    传入普通对象(也可以是数组)或Proxy对象,返回Proxy代理对象,该Proxy对象会对传入的对象进行代理

    如果从Proxy对象中访问的是一个对象成员,则获取到的也将是一个代理着该对象成员的Proxy对象

    与reactive不同的是,readonly返回的Proxy只允许进行读操作(任何形式的写操作都是无效的)

    import { readonly } from "vue";
    
    const data = {
        name: "zhangsan",
        age: 20,
        addr: {
            province: "GuangDong",
            city: "ShanTou"
        }
    };
    
    const dataProxy = readonly(data);			// Proxy {...}
    

    如果传递给readonly的是reactive返回的Proxy对象,则readonly会对该Proxy再包裹一个新的Proxy,而这个Proxy就是只允许读操作的代理对象

  3. ref

    传入任何类型的数据,返回ref对象

    如果传入的是原始类型数据,则ref会将返回的ref对象的value属性设置为一个访问器属性,通过访问器就可以设置获取和修改value的值

    如果传入的是引用类型数据,则会转交给reactive进行转换,之后通过返回ref对象的value属性访问到的就是reactive处理过后的Proxy对象

    import { ref } from "vue";
    
    let num = 0;
    
    const numRef = ref(num);				// refImpl { value: { getter, setter }}
    

    如果传入给ref的是reactive或readonly返回的Proxy对象,则不进行任何操作,则ref对象的value直接就是该Proxy对象

    import { ref, reactive } from "vue";
    
    const state = reactive({ a: 1, b: 2 });
    
    const stateRef = ref(state);
    
    console.log(stateRef.value === state);		// true
    

    ref也可用于获取子组件的组件实例:

    使用时,必须在setup函数中返回一个跟子组件ref属性值一样名称的ref对象

    <!-- 父组件 -->
    <template>
    <Child ref="comp" />
    </template>
    
    <script>
    import Child from "./components/Child.vue";
    import { ref, onMounted } from "vue";
    
    export default {
        components: {
    		Child
        },
        setup(){
            const compRef = ref(null);
            onMounted(()=>{
                console.log(compRef.value);		// 只包含“暴露属性”的子组件实例
            });
            return {
                comp: compRef					// 返回出去的属性的名称必须为comp
            }
        }
    }
    </script>
    

    准确来说,父组件通过refs得到的子组件实例,跟真正的子组件实例并不相同,因为通过refs得到的子组件实例中只包含子组件通过expose方法暴露出来的属性

    如果子组件不调用expose,父组件得到的子组件实例和子组件自己使用的组件实例的属性是完全相同的,即不调用expose等同于暴露所有属性

    <!-- 子组件Child -->
    
    <script>
    export default {
        created(){
    		console.log(this);				// 真正的组件实例
        },
    	setup(props, ctx){
            ctx.expose({
                a1: 100
            });
            return {
                a2: 1
            }
        }
    }
    </script>
    

    真正的子组件实例中包含属性a2: 1,但不包含属性a1: 100;父组件通过refs得到的子组件实例中仅包含属性a1: 100,其它什么也没有

  4. computed

    传入函数,返回ref对象

    import { ref, computed } from "vue";
    
    let num1Ref = ref(3);
    let num2Ref = ref(4);
    
    const sumRef = computed(()=>{			// refImpl {...}
        return num1Ref.value + num2Ref.value;
    });
    

    computed中的回调函数需要在computed返回的ref对象的value属性被访问时才会运行(只使用ref对象也不行)

    computed是有缓存的,当传递给computed的回调函数中所使用到的响应式数据都没有发生变化时,computed就会将缓存结果返回,之后再次使用computed所返回ref对象的value时,都会使用缓存的结果

    import { ref, computed } from "vue";
    
    let num1Ref = ref(3);
    let num2Ref = ref(4);
    
    const sumRef = computed(()=>{			// computed在定义时不会立即运行回调函数
        console.log("computed");
        return num1Ref.value + num2Ref.value;
    });
    
    console.log(sumRef.value);
    console.log(sumRef.value);
    num1Ref.value = 10;
    console.log(sumRef.value);
    
    /*
    	"computed"
    	7
    	7
    	"computed"
    	14
    */
    

    computed能够监听到响应式对象中的成员的值的变化,例如:

    监听ref对象的value的值的变化

    import { ref, computed } from "vue";
    
    let num = 0;
    
    const numRef = ref(num);
    
    const computedNumRef = computed(()=>{
        return numRef.value * 2;
    });
    

    监听响应式对象中的所有成员:

    import { reactive, computed } from "vue";
    
    const state = reactive({ a: 1, b: 2 });
    
    const stateRef = computed(()=>{
        return state;
    });
    

    需要注意的是,computed无法监听到顶级变量被重新赋值,如上:如果直接对state重新赋值,computed是监听不到的

监听数据变化

  • watchEffect

    watchEffect执行时需要传入一个回调函数,该回调函数会在定义时立即运行依次,之后当回调函数中使用到的响应式数据发生了变化时,回调函数会重新执行

    watchEffect执行后会返回一个函数,调用该函数可以停止watchEffect对响应式数据的监听

    import { reactive, watchEffect } from "vue";
    
    const state = reactive({ a: 1 });
    
    const stop = watchEffect(()=>{
        console.log(state.a);			// watchEffect会监控state.a的值的变化
    });
    

    watchEffect中的回调函数除在定义时是同步执行的外,之后都是异步执行的(进入的是微队列),且异步执行时最多只会执行一次,因此同步代码执行阶段,即使响应式数据多次发生变化,最后在执行异步任务时也只会执行一次回调

    import { reactive, watchEffect } from "vue";
    
    const state = reactive({ a: 1 });
    
    const stop = watchEffect(()=>{		// 回调函数会在此同步地执行一次
        console.log(state.a);
    });
    
    state.a++;
    state.a++;
    state.a++;
    // 之后回调函数会异步地执行一次
    

    不同于computed能监听到响应式对象内部成员的变化,watchEffect只能监听到回调函数中实际使用到的响应式数据,这些响应式数据被重新赋值时才会触发回调函数的重新执行,其内部的属性变化watchEffect是监听不到的

    import { reactive, watchEffect } from "vue";
    
    let state = reactive({ a: { aa: 1 } });
    
    const stop = watchEffect(()=>{
        console.log(state.a);			// 仅对state.a自身的变化进行监听,内部的变化不做监听
    });
    
    state.a.aa++;			// watchEffect没有反应
    state.a = 2;			// 被watchEffect监听到
    

    此外,所监听的数据不可以是顶级变量,必须得是其它响应式对象中的某个属性

    import { reactive, watchEffect } from "vue";
    
    let state = reactive({ a: { aa: 1 } });
    
    const stop = watchEffect(()=>{
        console.log(state);			// 什么也监听不到,watchEffect的回调只会在同步阶段执行一次
    });
    
    state.a = 10;					// 无效果
    state = reactive({ b: 2 });		// 无效果
    
  • watch

    watch执行时需要传入两个参数,第一个参数是要监听的响应式数据,第二个是具体要运行的回调函数,也可以传入第三个参数,如{ immediate: true, deep: true },表示在watch定义时是否立即执行watch的第二个参数以及是否深度监听

    在watch的第二个参数回调函数中,可以接收两个参数,分别是所监听数据的原始内容,以及数据变化后的新内容

    和computed一样,watch能够监听到响应式对象中的成员的值的变化,但无法监听到顶级变量被重新赋值:

    如果watch要监控的是Proxy响应式对象中的所有属性,则需要监控的对象就是该Proxy对象

    import { reactive, watch } from "vue";
    
    const state = reactive({ a: 1 });
    
    watch(state, (newVal, oldVal)=>{				// 监控state内部所有属性的值的变化
        console.log("监听的数据发生变化了");
    });
    
    state.a = 3;			// 导致回调函数被执行
    

    如果watch要监控的是Proxy响应式对象中的某个原始值属性,则需要将监控的数据书写为一个函数,函数的返回值是该属性的值

    import { reactive, watch } from "vue";
    
    const state = reactive({ a: 1 });
    
    watch(()=>state.a, (newVal, oldVal)=>{				// 监控state.a的值的变化
        console.log("监听的数据发生变化了");
    });	
    

    如果watch要监控的是ref对象的value属性,则直接将监控的数据书写为该对象即可

    import { ref, watch } from "vue";
    
    const numRef = ref(0);
    
    watch(numRef, (newVal, oldVal)=>{					// 监控numRef.value的变化
    	console.log("监听的数据发生变化了");
    });
    

    也支持书写为回调函数的形式

    import { ref, watch } from "vue";
    
    const numRef = ref(0);
    
    watch(()=>numRef.value, (newVal, oldVal)=>{		// 监控numRef.value的变化
    	console.log("监听的数据发生变化了");
    });
    

    watch可以一次监听多个响应式数据,因此应该将第一个参数书写为一个数组,此时第二个参数回调函数中的新值旧值也将能够接收多个

    import { ref, reactive, watch } from "vue";
    
    const numRef = ref(0);
    const state = reactive({ a: 1 });
    
    watch([numRef, ()=>state.a], ([newNumRefVal, newStateAPropVal], [oldNumRefVal, oldStateAPropVal])=>{
    	console.log("监听的数据发生变化了");
    });
    

    watch中的回调函数和watchEffect中的回调函数一样,当监听的响应式数据发生变化时,会进入到微队列等待执行,且微队列同一个回调最多只会出现一次

什么时候下应该使用watchEffect,什么时候又应该使用watch?

除以下场景需要使用watch外,其它场景建议使用watchEffect

  1. 不希望回调函数一开始就运行
  2. 监听的响应式数据变化后,需要获取到旧的值
  3. 监听回调函数中未出现过的响应式数据

判断

  • isProxy

    判断某个数据是否是由reactive或readonly产生的

  • isReactive

    判断某个数据是否是由reactive产生的

  • isReadonly

    判断某个数据是否是由readonly产生的

  • isRef

    判断某个数据是否是一个ref对象

转换

  • unref

    如果传入的响应式数据是一个ref对象,则返回该数据的value属性,否则直接返回该数据

    等同于表达式:isRef(data) ? data.value : data

  • toRef

    得到Proxy响应式对象中某个属性的ref形式

    当响应式对象的属性发生变化时,对应转换后的ref对象的value也会改变

    ref对象的value发生变化时,响应式对象中对应属性也会发生改变

    import { reactive, toRef } from "vue";
    
    const state = reactive({
        foo: 1,
        bar: 2
    });
    
    const fooRef = toRef(state, 'foo'); 		// fooRef = refImpl {value: ...}
    
    fooRef.value++;
    console.log(state.foo); 					// 2
    
    state.foo++;
    console.log(fooRef.value);	 				// 3
    

    如果toRef是对readonly所返回的Proxy响应式对象的某个属性进行的转换,则其返回的ref对象将不能对value进行更改

    import { readonly, toRef } from "vue";
    
    const state = readonly({
        foo: 1,
        bar: 2
    });
    
    const fooRef = toRef(state, 'foo'); 		// fooRef = refImpl {value: ...}
    
    fooRef.value++;
    console.log(fooRef.value);	 				// 1
    
  • toRefs

    返回一个普通对象,该对象中的属性是把Proxy响应式对象中的所有属性都转换为ref形式后的结果

    import { reactive, toRefs } from "vue";
    
    const state = reactive({
        foo: 1,
        bar: 2
    });
    
    const stateAsRefs = toRefs(state);
    /*
    Object {
    	foo: refImpl { value: ... },
    	bar: refImpl { value: ... }
    }
    */
    

    如果toRefs是对readonly所返回的Proxy响应式对象中的属性进行的转换,则其返回的普通对象中的所有ref对象属性都将不能对自己的value进行更改

    应用:当对响应式对象进行展开或解构后,响应式对象中的属性将不再具有响应式特性,利用toRefs就可以完美处理该问题

    // 错误的做法
    setup(){
        const state1 = reactive({ a:1, b:2 });
        const state2 = reactive({ c:3, d:4 });
        return {
            ...state1,	// lost reactivity
            ...state2 	// lost reactivity
        }
    }
    
    // 正确的做法
    setup(){
        const state1 = reactive({ a:1, b:2 });
        const state2 = reactive({ c:3, d:4 });
        return {
            // 利用toRefs可以完美解决该问题
            ...toRefs(state1),	// reactivity
            ...toRefs(state2)	// reactivity
        }
    }
    
    function usePos(){
        const pos = reactive({ x:0, y:0 });
        return pos;
    }
    
    setup(){
        // 错误的做法
        const {x, y} = usePos(); 			// lost reactivity
        
        // 正确的做法
        const {x, y} = toRefs(usePos());	// reactivity
    }
    

Composition API

与Reactivity API不同,Composition API与组件之间的关系十分紧密,不能够脱离组件独立存在

setup

该函数需要书写在组件配置对象中,该函数被调用时会传入两个参数,分别是props和context

props参数中包含了组件的所有prop,通过context参数则可以使用到emit方法、attrs对象、slots对象

  • emit方法

    用于向父组件抛出事件,等同于Vue2中的$emit

  • attrs对象

    等同于Vue2中的this.$attrs

  • slots对象

    等同于Vue2中的this.$slots

setup函数会组件的prop全部被绑定完成后执行,此时组件中还没有一个生命周期钩子函数被执行

export default {
    setup(props, context){ }
}

生命周期函数

vue2 option apivue3 option apivue 3 composition api
beforeCreatebeforeCreate不再需要
createdcreated不再需要
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroy改为 beforeUnmountonBeforeUnmount
destroyed改为 unmountedonUnmounted
errorCapturederrorCapturedonErrorCaptured
-新增 renderTrackedonRenderTracked
-新增 renderTriggeredonRenderTriggered
  • renderTracked

    在组件的render函数运行时,通常会使用到一些响应式数据,每当render函数访问了某个响应式数据时,该响应式数据就会记录render函数的Watcher,同时还会触发执行一次renderTracked钩子函数

    该钩子函数能够接收一个参数,该参数是一个对象,对象中就记录着render函数中的哪个响应式数据记录下来了render函数的Watcher

  • renderTriggered

    当render函数中使用到的某一个响应式数据发生了变化时,就伴随着触发一次renderTriggered钩子函数

    该钩子函数能够接收一个参数,该参数是一个对象,对象中就记录着render函数中的哪个响应式数据发生了变化

共享数据

vuex

Vue3所使用的Vuex版本为4,Vuex4的使用和Vuex3基本相同,只是Vuex4不再是默认导出一个构造函数Vuex来创建store对象,而是使用具名导出一个createStore函数来创建store对象

import { createApp } from "vue";
import { createStore } from "vuex";
import App from "./App.vue";

const user = {
    namespaced: true,
    state: {
        user: null
    },
    mutations: {
        setUser(state, payload){
            state.user = payload;
        }
    },
    actions: {
        async fetchData({ commit }){
            const user = await getUserData();
            commit("setUser", user);
        }
    }
};

const store = createStore({
    modules: {
        user
    }
});

createApp(App).use(store).mount("#app");

store.dispatch("user/fetchData");

在组件配置中使用store时,除setup配置外,其它配置均可以使用this.$store来访问到store对象,而在setup配置中,则需要使用vue通过具名导出的useStore函数来使用到store对象

<script>
    import { useStore } from "vuex";

    export default {
        created(){
            this.$store.commit(...);
		},
        setup(){
            const store = useStore();
            store.commit(...);
		}
    }
</script>

global state

由于Vue3中的响应式系统可以脱离组件独立存在,可以利用这一点手动制造全局响应式数据

image.png

例:

由于模块加载完第一次后内容就会被缓存下来,因此之后再导入得到的也是之前缓存下来的数据,实现了多模块访问同一个共享数据

import { reactive, readonly } from "vue";
import * as userApi from "./api/user";

const state = reactive({
       user: null,
       loading: false,
});

// 暴露只读的响应式数据,只能够通过暴露出去的修改函数来修改响应式数据
export const userStore = readonly(state);

export async function login({ loginId, loginPwd }) {
       state.loading = true;
       const user = await userApi.login(loginId, loginPwd);
       state.user = user;
       state.loading = false;
}

export async function loginOut() {
       state.loading = true;
       await userApi.loginOut();
       state.user = null;
       state.loading = false;
}

export async function whoAmI() {
       state.loading = true;
       const user = await userApi.whoAmI(loginId, loginPwd);
       state.user = user;
       state.loading = false;
}

Provide & Inject

Vue2中提供了provide和inject两个组件配置,当一个组件在自己的provide配置中加入了某些数据后,其后代组件都可以通过inject将这些数据获取到

image.png

在Vue3中也同样支持provide和inject,并且在option API下这两个配置和Vue2中完全相同,而在composition API下,则需要导入vue具名导出的两个函数provide和inject来使用这两个功能

// 父组件
import { ref, provide } from "vue";

export default {
    setup(){
		provide("bar", ref(1));
    }
}
// 后代组件
import { inject } from "vue";

export default {
    setup(){
		const barRef = inject("bar");
    }
}

image.png

考虑到某些数据是整个应用中的任何组件都可能会使用到的,因此在创建出来的应用上进行provide

通过createApp创建出来的vue应用中就带有一个provide方法,通过该方法可以在整个vue应用上提供一些共享数据

// main.js
import { createApp, ref } from "vue";
import App from "./App.js";

createApp(App).provide("foo", ref(1)).provide("bar", ref(2)).mount("#app");

image.png

注意:

  • vue应用提供了provide方法,但没有提供inject方法
  • vue具名导出的inject方法,它只有在组件配置的setup函数运行期间调用有效,其它时候调用得到的永远是undefined

script setup

Vue3支持在vue文件的script标签上加上setup标记,加上setup标记的script标签,标签中的内容(除import语句外)相当于是在组件配置的setup函数中书写的

<script setup>中定义的顶级变量,不会附着在组件实例上,但在模版中可以直接访问

在传统的setup函数配置方式下,setup函数return的内容都会附着在组件实例上,父组件通过refs获取到组件的组件实例时,(在没有调用expose的情况下)也能够获取到这些内容

但如果是使用script setup的方式,因为其中的所有内容都不会附着在组件实例上,因此父组件也将获取不到

在script setup中,可以使用defineExpose定义的宏来将script setup标签中的内容暴露给父组件

<script setup>
import { ref } from "vue";
    
const numRef = ref(0);
    
defineExpose({
    num: numRef			// 把numRef暴露出去
});
</script>

在script setup中,由于不存在组件配置了,且this为undefined,因此要使用props或者emit方法时,需要手动定义宏,并通过宏来使用:

<script setup>
const props = defineProps({
    content: {									// 接收content属性
        type: String,
        default: ""
    }
});

console.log(props.content);

const emit = definedEmits(["change"]);			// 定义change事件
    
emit("change", ...);
</script>

宏在编译阶段就会被转换为其他代码,因此不需要对其进行导入

Pinia

Pinia是一个Vue的状态管理库

相比Vuex,Pinia的API更加简单,并且Pinia还支持组合式API

与Vuex对比

  1. 在Pinia中,不存在mutations,只有state、getters、actions
  2. actions中不仅支持异步修改state,还支持同步修改state
  3. 可以和TypeScript搭配使用,以此来获得类型推断的支持
  4. 每一个Store都是独立存在的,不像Vuex需要通过modules进行合并
  5. 支持插件扩展仓库功能
  6. (体积)更加轻量

使用Pinia

应用pinia:

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

// 创建Pinia实例
const pinia = createPinia();

createApp(App).use(pinia).mount("#app");

定义store:

// store/useCounterStore.js
import { defineStore } from "pinia";

// defineStore的第一个参数是id,用于区分不同的store以及连接devtools调试器
// defineStore的第二个参数可以是一个配置对象(options风格),也可以是一个函数(composition风格)
// defineStore会返回一个函数,组件中要使用该store时,就需要调用defineStore返回的函数
export const useCounterStore = defineStore("counter", {
    // 需要将state配置为一个函数,数据作为该函数的返回值对象中的属性
    state: ()=>{
        return {
			num: 0
        }
    },
    // 相当与vue的计算属性
    getters: {
        doubleNum(){
            return this.num * 2;
        }
    },
    actions: {
        // 同步方法
        increase(){
            this.num++;
        },
        // 异步方法
        async asyncIncrease(){
            setTimeout(()=>{
                this.num++;
            }, 1000);
        }
    }
});
// 如果defineStore使用的是options风格的配置参数,则还可以只传入一个对象参数,此时id配置就需要作为该对象的id属性进行添加



import { ref, computed } from "vue";

export const useCounterStore = defineStore("counter", ()=>{
    // ref&reactive data就是state property
    const numRef = ref(0);
    
    // computed就是getters
    const doubleNumRef = computed(()=>{
        return numRef.value * 2;
    });
    
    // funcitons就是actions
    function increase(payload = 1){
		numRef.value += payload;
    }
    
    async function asyncIncrease(payload = 1){
        setTimeout(()=>{
			numRef.value += payload;
        }, 1000);
    }
	
    // 将数据和方法返回出去
    return {
        num: numRef,
        doubleNum: doubleNumRef,
        increase,
        asyncIncrease
    }
});

使用store:

<script setup>
import { storeToRefs } from "pinia";
import { useCounterStore } from "./store/useXxxStore.js";

const store = useCounterStore();

// 可以直接通过store.xxx修改其state中的xxx属性
store.num++;

// 也可以通过结构出store中的属性来对其进行直接改变,但为了保持从store中解构属性的响应式特性,需要使用storeToRefs()进行解构
const { num, doubleNum } = storeToRefs(store);
const { increase, asyncIncrease } = store;

// 使用store.$reset()可以将store的state重置为初始状态
// composition风格的配置参数下所定义出来的store是不具有该方法的
// store.$reset();

// 使用store.$patch()可以同时将store中的多个数据进行修改
// store.$patch()可以传入一个对象,对象中的属性的值会应用到store的state中的相应属性上
store.$patch({
    num: store.num + 1
});

// store.$patch()也可以传入一个函数,在函数中可以拿到store的state,并对state中的属性进行更改
store.$patch((state)=>{
    state.num++;
});
</script>

注意:

  • 如果在定义store时使用的是composition风格的配置参数,则定义出来的store是不具有$reset方法的
  • 尽管Pinia提供了直接通过store.xxx来修改数据的能力,但也应该尽量避免使用这种方式修改仓库状态,最佳实践是使用action来操作仓库的状态

为Pinia添加插件

一个插件其实就是一个函数,通过为Pinia添加插件,可以实现以下功能:

  • 为store添加新的属性和方法
  • 定义store时增加新的配置项
  • 包装现有的方法
  • 改变或取消action
  • 实现副作用操作

插件分为自定义插件和第三方插件

插件需要应用到Pinia实例当中,通过pinia.use即可应用插件

添加插件后,pinia会在每一次定义store完成后都运行一次插件函数(但也仅会运行这一次)

插件函数返回的对象中的属性,会自动注入到当前的store仓库中

import { createPinia } from "pinia";

const pinia = createPinia();

// 为该pinia实例的所有store都添加一个属性 —— globalProp
function myPiniaPlugin(){
    return {
        globalProp: "123"
    }
}

pinia.use(myPiniaPlugin);

插件函数可以接收一个参数context,通过context可以获取到App应用、pinia实例、当前的store仓库、定义当前store时传递的配置参数

function myPiniaPlugin(context){
    console.log(context.app);
    console.log(context.pinia);
    console.log(context.store);
    console.log(context.options);
}

通过插件的context参数,即可为全部的store或者特定的store添加属性

function myPiniaPlugin(context){
	// 为全部store添加属性a: 1
    context.store.a = 1;
    
    // 为id为"counter"的store添加属性b: 2
    if(context.store.$id === "counter"){
        context.store.b = 2;
    }
}

利用插件,为使用composition风格的配置参数的store实现$reset方法:

function myPiniaPlugin({ store }){
    // 深拷贝初始的仓库状态
    const state = deepClone(store.$state);
    
    // 每一次调用$reset方法时都将初始状态覆盖掉store的当前状态
	store.$reset = ()=>{
        store.$patch(deepClone(state));
    };
}

对于第三方插件,具体的使用方法参阅第三方插件的说明文档

补充

  • 辅助函数

    Pinia中的辅助函数和Vuex中的辅助函数基本相同,包括mapState、mapActions等

    Pinia将mapGetters进行了移除,要想获取getters时,需要使用mapState

    辅助函数适用于使用options API的场景中,composition API中则不需要使用辅助函数

    <script>
    import { useCounterStore } from "./store/useCounterStore.js";
    import { mapState, mapActions } from "pinia";
    
    export default {
        computed: {
            ...mapState(useCounterStore, ["num", "doubleNum"])
        },
        methods: {
            ...mapActions(useCounterStore, ["increase", "asyncIncrease"])
        }
    }
    </script>
    
  • 订阅state

    通过store.subscribe方法可以监听到store的state变化,每当state发生变化时,会触发\subscribe中回调函数的执行

    const store = useCounterStore();
    
    store.$subscribe((mutation, state)=>{
        // 值为"direct"或"patch obj",分别表示本次修改是直接修改的或通过$patch方法修改的
        console.log(mutation.type);
        // 订阅的store的id
        console.log(mutation.storeId);
        // 传递给$patch的补丁对象,仅在mutation.type为"patch obj"时才有效
        console.log(mutation.payload);
        // state是变化后的state
        console.log(state);
    });
    
  • 订阅action

    通过store.onAction方法可以监听到action中函数的执行,每当action即将执行的前一刻,会触发\onAction中回调函数的执行(之后才会执行action)

    $onAction会返回一个函数,调用该函数即可取消对action的订阅

    const store = useCounterStore();
    
    const unsubscribe = store.$onAction(({name, args, after, onError})=>{
        // 执行的action的名称
        console.log(name);
        // 传递给action的参数数组
        console.log(args);
        // action返回或解决后会触发的钩子函数
        after(()=>{ });
        // action抛出错误或拒绝后会触发的钩子函数
        onError(()=>{ });
    });
    
  • 插件选项

    在定义store时,可以为store所应用的插件提供一些配置参数,这些配置参数可以在插件函数中获取到

    // 如果store选用的是options风格的配置参数,则为插件提供的配置可以直接书写在defineStore的第二个参数中
    const useCounterStore = defineStore("counter", {
        state,
        actions,
        // 下面是为插件提供的配置参数
        ...
    });
    
    
    
    // 如果store选用的是composition风格的配置参数,则为插件提供的配置需要书写为第三个参数(是一个对象)
    const useCounterStore = defineStore("counter", ()=>{ }, {
        // 为插件提供的配置参数
        ...
    });
    

    插件函数的context参数中除了包含之前所述的属性,还包含一个options属性,通过该属性即可获取到定义store时为插件提供的配置参数

    function myPiniaPlugin(context){
        console.log(context.options);
    }