Vue3.0通关秘籍

前言

不知不觉中,使用 Vue3 开发挺长一段时间了,经历过大大小小的项目,其中项目形式主要涉及两种,要么是 Vue3+Webpack 要么就是 Vue3+Vite,而对于 TS 并不会每个项目都会使用,像比较小且急的项目就不会考虑使用 TS ,感觉没啥必要,增加 TS 只会徒增编译时间、开发难度、开发成本等等。呃…总的来说,还是要看实际的场景考虑吧。

然后对于为什么会有两种项目形式的选择,也就是 WebpackVite 的比拼,这其中考虑的因素有很多,但最重要的一个衡量点小编觉得还是在于项目的规模,对于项目规模太大的项目,小编还是不太敢上 Vue3+Vite 形式,怕其中带来的后果承担不起>_<。

  • Vite 可用插件还不是很多。
  • Vite 开发环境与生产环境打包器不同,线上出现bug不容易定位问题。
  • 可能你会想,用上Vue3不用Vite不是白用了?直接用vue2不就行了?Vue3的优点有很多,可不止 Vite,像代码体积更小,底层性能更佳等等。

Vue3优点

1.源码体积小Vue2 压缩后的体积大约有20kb,Vue3 压缩后的体积只有10kb。Vue3 中移除了部分API,如 filter$on$off$once$destory、按键修饰符全部改用按键名,不再支持按键值。

2.响应式系统升级:将原来的 Object.defineProperty 替换成了ES6中的 Proxy ,它是通过在目标对象前加了一层拦截,代理的是对象而不是对象的属性,比原来的颗粒度变大了。现在可以监听到对象的新增与删除属性的操作、数组的索引赋值操作、数组的 length 属性操作。

3.更友好的静态类型支持Vue2 需要额外采用 Flow 作为静态类型检查,它是由 Facebook 出品的 JavaScript 静态类型检查工具;而 Vue3 直接全部采用 TS 进行开发,对 TS 支持更友好。

4.性能提升Vue3 重写了虚拟 DOM 和底层的 Diff 算法,拥有更好的 tree-shaking 支持,整体性能得很大的一个提升。

5.CompositionAPIVue3 最大的一个变动应该就是推出了 CompositionAPI,可以说它受ReactHook 启发而来;它我们编写逻辑更灵活,便于提取公共逻辑,代码的复用率得到了提高,也不用再使用 mixin 担心命名冲突的问题。

6.更先进的组件Teleport - 传送/瞬移组件、Suspense - 等待异步组件过程中可以先提前渲染占位内容、Fragment - 多根性质等等。

7.自定义渲染器Vue3 支持创建自定义的渲染器,能实现跨平台,通过改写 Vue 底层渲染逻辑,如渲染成小程序形式等等。

一、初始化项目

初始化 Vue3+Vite 项目:

npm init vue@latest

// or

npm init vite@latest projectName

初始化 Vue3+Webpack 项目:

vue create projectName

二、setup

setup() 函数是 Vue3 新增的一个选项,它是组合式 API 的统一入口,简单来说,就是所有的 CompositionAPI 新特性都应该写在这个函数内部。

<script>
export default {
  setup(props, context) {
    return {}
  }
}
</script>

它接收两个参数:

  • props:这个还是和 Vue2 使用的组件之间通信的 props一样。
  • context:定义上下文,这个参数身上有一些我们比较常用的属性,比如
    • context.emit:等同于 Vue2this.$emit
    • context.slots:等同于 Vue2this.$slots
    • context.attrs:等同于 Vue2this.$attrs
    • context.expose():当前组件对外暴露属性的函数。

expose

关于 context.expose() 函数,下面小编写两个例子,看完你应该就能明白了。

// parent.vue
<template>
  <child ref="childRef" />    
</template>
<script>
import { ref, onMounted } from 'vue'
import child from './components/child.vue'
export default {
  components: { child },
  setup() {
   let childRef = ref(null)
   onMounted(() => {
     console.log(child.value.name)
     console.log(child.value.age)
   })

   return { childRef }
  }
}
</script>

//child.vue
<template>
  <div>子组件</div>
</template>
<script>
import { ref } from 'vue'
export default {
  setup() {
    let name = ref('橙某人')
    let age = ref(18)

    return { name, age };
  }
}
</script>

从上面例子中,我们可以看到子组件在 setup() 函数中返回的所有东西都可以被父组件直接访问,也就是子组件所有方法或数据等都是公开的。

但如果你想开发一个开源的组件或库,你有可能想保持一些内部方法的私有性,并不想它能被父类所调用,那么这个时候应该怎么做呢?我们来看看下面这个例子,给子组件添加 context.expose() 函数。

//child.vue
<template>
  <div>子组件</div>
</template>
<script>
import { ref } from 'vue'
export default {
  setup(props, context) {
    let name = ref('橙某人')
    let age = ref(18)
    
    context.expose({
      name: 'ydydydq',
    })
    return { name, age };
  }
}
</script>

在这里插入图片描述

可以看到,父组件已经无法访问 age 属性了,而且 name 属性值也是我们重新规定的值了,并不会访问到组件内部的数据。

这就是 context.expose() 函数的作用,它更加直观的允许组件暴露规定的一切方法和数据。

三、生命周期

Vue3 生命周期相比 Vue2 做了一些改动,如下:

  • Vue3setup() 函数替代了 beforeCreatecreate 钩子。

  • Vue3 生命周期钩子都以 on+xxx 开头,并且需要手动导入且只能在 setup() 函数内部使用。

    <script>
      import { onMounted }from 'vue';
      export default {
        setup(props, context) {
          onMounted(() => {
            console.log('onMounted');
          })
          return {}
        }
      }
    </script>
    
  • 卸载组件过程的两个钩子名称改变,beforeDestroy 变成 onBeforeUnmountdestroyed 变成 onUnmounted

vue2说明vue3说明
beforeCreate在实例初始化之后、进行数据侦听和事件/侦听器的配置之前同步调用。setup()setup 函数会比 beforeCreatecreated 先执行,记住它是入口,会最先跑。
created在实例创建完成后被立即同步调用,这个阶段挂载还没开始,所以 $el 还不能使用。setup()同上
beforeMount在挂载开始之前被调用,相关的 render 函数首次被调用,$el 依旧还不能用。onBeforeMount相同
mounted在实例挂载完成后被调用,这个时候 $el 能使用,但是这个阶段不能保证所有的子组件都挂载完成,如果你希望等待整个视图渲染完毕,可以使用 $nextTickonMounted相同
beforeUpdate在数据发生改变后,DOM 被更新之前被调用(使用该钩子要注意死循环)。onBeforeUpdate相同
updated在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用(使用该钩子要注意死循环)。onUpdated相同
beforeDestroy实例销毁之前调用,但在这一步,实例仍然完全可用。onBeforeUnmount相同
destroyed实例销毁后调用,包括子实例也会被销毁。onUnmounted相同

最后我们放一张钩子执行的对比图,可以发现 Vue3 的生命周期钩子是比 Vue2 优先执行的。

在这里插入图片描述

四、ref与reactive

refreactiveVue3 新推出的主要 API 之一,它们主要用于响应式数据的创建。

既然它们俩都是用于创建响应式数据,那么它们又有什么区别呢?

我们带着这个疑问往下看,先来看看它们俩的基本使用:

<template>
  <div>
    <div>count1: {{count1}}</div>
    <div>count2: {{count2.val}}</div>
    <div>count3: {{count3.val}}</div>
    <button @click="add">按钮</button>
  </div>
</template>

<script>
import { ref, reactive } from 'vue';
export default {
  setup() {
    const count1 = ref(0);
    const count2 = ref({ val: 0 });
    const count3 = reactive({ val: 0 }); // reactive只能接收引用类型, Object、Array...

    function add() {
      count1.value++;
      count2.value.val++;
      count3.val++;
    }

    return { count1, count2, count3, add }
  }
}
</script>

上面写了一个很简单的计数叠加例子,从中可以得到一些结论:

  • template 模板中使用的数据和方法,都需要通过 setup 函数 return 出去才可以被使用。
  • ref 函数创建的响应式数据,在模板中可以直接被使用,在 JS 中需要通过 .value 的形式才能使用。
  • ref 函数可以接收原始数据类型引用数据类型
  • reactive 函数只能接收引用数据类型

这里我们就知道它们俩的区别了!但是既然这样为啥还需要 reactive ,全部使用 ref 一把梭不就行了吗?难道是存在什么情况是 reactive 能做,但是 ref 做不了的?

答案:没有这种情况,反而是 reactive 能做的,ref 都能胜任,并且 ref 底层还是使用 reactive 来做的!!!

在这里插入图片描述

通过 Vue3 的源码是可以证实这个结论的,具体过程感谢兴趣的小伙伴可以自行查阅,这里就不做过多阐述了。

反正简单来说就是, ref 是在 reactive 上在进行了封装,增强了其能力,使它支持了对原始数据类型的处理,所以在 Vue3reactive 能做的,ref 也能做,reactive 不能做的,ref 也能做。

五、toRef与toRefs

toRef

官网对它的解释是可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。 按我的理解就是把 reactive 创建的响应对象中的某个属性变成一个单独的 ref ,但这个新 ref 与原对象是保持响应式连接的。

概念不理解没关系,我们直接来看看例子:

<template>
  <div>
    <div>姓名: {{person.name}}</div>
    <div>年龄: {{person.age}}</div>
    <div>新年龄: {{newAge}}</div>
    <button @click="update">按钮</button>
  </div>
</template>

<script>
import { reactive, toRef } from 'vue';
export default {
  setup() {
    const person = reactive({
      name: '橙某人',
      age: 18
    });
    const newAge = toRef(person, 'age');
    
    function update() {
      // person.age = 20 一样会同步响应
      newAge.value = 20
    }

    return { person, newAge, add }
  }
}
</script>

在这里插入图片描述

从上面效果图可以看到,通过 toRef 创建的变量是具有响应式的并与原对象是保持响应式连接的。

toRefs

与前者相比多了一个 “s”,你可以认为它是 toRef 的批量操作,它的主要作用,小编觉得有这两方面:

  • reactive 创建的响应对象中每个属性变成单独的 ref
  • 结合 ES6 的解构,可以使 reactive 对象的属性在模板中直接被使用,再也不需要通过 xx.属性 的形式。

需要注意的是,若直接对 reactive 创建的对象解构,会失去响应式!

<template>
  <div>昵称: {{name}}</div>
  <div>年龄: {{age}}</div>
  <button @click="update">按钮</button>
</template>
<script>
import { reactive, toRef, toRefs } from 'vue';
export default {
  setup() {
    const person1 = reactive({
      name: '橙某人',
    });
    const person2 = reactive({
      age: 18,
    });

    function update() {
      person1.name = 'YDYDYDQ'
      person2.age = 20
    }

    return { 
      ...toRefs(person1), 
      ...person2, // 直接解构, 会失去响应式
      add 
    };
  }
}
</script>

在这里插入图片描述

从效果图可以看到 person2.age 是没有响应变化的。

person1 的类型:
{
  name: Ref<string>,
}
person2 的类型:
{
  age: number,
}

六、响应式对象判断

isRef

从名称可以直接看出这个方法的作用,它用于检查值是否为一个 ref 对象。

<script>
import { ref, isRef } from 'vue'
export default {
  setup() {
    const name = ref('橙某人');
    const age = 18;
    console.log(isRef(name)); // true
    console.log(isRef(age)); // false
  }
}
</script>

unref

如果参数是一个 ref,则返回内部值,否则返回参数本身。这个方法可能平常使用到的几率会比较少,它本质是 isRef 的三元判断语法糖,等同 val = isRef(val) ? val.value : val

<script>
import { ref, unref } from 'vue'
export default {
  setup() {
    const name = ref('橙某人');
    const age = 18;
    console.log(unref(name)); // 橙某人
    console.log(unref(age)); // 18
  }
}
</script>

你可以认为 name.value === unref(name)

isReactive

检查一个对象是否是由 reactiveshallowReactive 创建的代理。

<script>
import { ref, reactive, isReactive } from 'vue'
export default {
  setup() {
    const name = ref('橙某人');
    const person = reactive({});
    console.log(isReactive(name)); // false
    console.log(isReactive(person)); // true
  }
}
</script>

isProxy

检查一个对象是否是由 reactivereadonlyshallowReactiveshallowReadonly 创建的代理。

<script>
import { ref, reactive, isProxy } from 'vue'
export default {
  setup() {
    const name = ref('橙某人');
    const person = reactive({});
    const age = 18;

    console.log(isProxy(name)); // false
    console.log(isProxy(person)); // true
    console.log(isProxy(age)); // false
  }
}
</script>

isReadonly

检查一个对象是否是由 readonlyshallowReadonly 创建的代理。

<script>
import { ref, reactive, isProxy } from 'vue'
export default {
  setup() {
    const name = ref('橙某人');
    const person = reactive({});
    const student = readonly({}); // 接受一个对象(不论是响应式还是普通的)或是一个 ref, 返回一个原值的只读代理.
    const age = 18;

    console.log(isReadonly(name)); // false
    console.log(isReadonly(person)); // false
    console.log(isReadonly(student)); // true
    console.log(isReadonly(age)); // false
  }
}
</script>

七、computed

computed()Vue2 中的 computed 作用基本一致,它可以接收一个函数对象作为参数,会返回一个只读ref 对象。

<template>
  <div>{{info}}</div>
</template>

<script>
import { computed, reactive } from 'vue'
export default {
  setup() {
    const person = reactive({
      name: '橙某人',
      age: 18
    });
    const info = computed(() => `姓名:${person.name},年龄${person.age}`)

    return { info }
  }
}
</script>

在这里插入图片描述

修改计算属性

如果你尝试修改计算属性的返回值,控制台会有出现提示信息并且修改不会成功。

info.value = '修改computed()返回值'

在这里插入图片描述

但假如你一定要修改 computed() 的返回值,我们可以使用它的对象参数来间接修改,该对象需要提供 setget 方法。

<template>
  <div>{{info}}</div>
</template>

<script>
import { computed, reactive } from 'vue'
export default {
  setup() {
    const person = reactive({
      name: '橙某人',
      age: 18
    });
    let info = computed({
      set: newValue => {
        person.name = newValue
      },
      get: () => `姓名:${person.name},年龄${person.age}`
    });
   
    info.value = '修改computed()返回值'

    return { info }
  }
}
</script>

在这里插入图片描述

八、watch

Vue3watch()Vue2watch 选项和 this.$watch API 完全等效。

我们先来看看它的基本两种基本用法:

监听单一源

<template>
  <div>昵称: {{name}}</div>
  <div>年龄: {{person.age}}</div>
  <button @click="update">按钮</button>
</template>

<script>
import { ref, reactive, watch } from 'vue';
export default {
  setup() {
    // 直接监听一个ref
    const name = ref('橙某人');
    watch(name, (newValue, oldValue) => {
      console.log('新name:', newValue, oldValue);
    });

    // 监听对象单个属性
    const person = reactive({
      age: 18
    })
    watch(() => person.age, (newValue, oldValue) => {
      console.log('新age:', newValue, oldValue);
    });
    
    function update() {
      name.value = 'YDYDYDQ'
      person.age = 20
    }

    return { name, person, add }
  }
}
</script>

在这里插入图片描述


监听多个源

<template>
  <div>昵称: {{name}}</div>
  <div>年龄: {{age}}</div>
  <button @click="update">按钮</button>
</template>

<script>
import { ref, watch } from 'vue';
export default {
  setup() {
    const name = ref('橙某人');
    const age = ref(18);

    watch([name, age], (newValue, oldValue) => {
      console.log(newValue, oldValue)
    });

    function update() {
      name.value = 'YDYDYDQ';
      age.value = 20;
    }

    return { name, age, add }
  }
}
</script>

在这里插入图片描述

watch() 可以使用数组形式同时监听多个源,并且得到的 newValueoldValue 也是以数组的形式返回的。

当我们只修改 name 得到的结果:
在这里插入图片描述

如果是想监听 reactive 对象的多个属性,可以这么写:

const person = reactive({
  name: '橙某人',
  age: 18
});

watch([() => person.name, () => person.age], (newValue, oldValue) => {
  console.log(newValue, oldValue); // [...], [...]
});

immediate与deep参数

Vuewatch 选项还有 immediatedeep 参数,在 Vue3 中一样还能继续使用。

import { reactive, watch } from 'vue';
export default {
  setup() {
    const person = reactive({
        name: '橙某人',
        age: 18
    });
    
    watch(() => person, (newValue, oldValue) => {
      console.log(newValue, oldValue)
    }, {
      deep: true,
      immediate: true
    });
    
    function add() {
      person.value.name = 'YDYDYDQ';
      person.value.age = 20;
    }
    
    return { add };
  }
}

在这里插入图片描述

但是这里有两个小坑需要小伙伴们注意,都是针对 oldValue 参数:

  • 开启 immediate 参数后,初次监听的 oldValue 值是 undefined
  • 监听一整个 引用类型 变化时,oldValue 值与 newValue 值一样 (用 ref 来定义也是一样)。

停止监听

当我们在 setup 函数内创建 watch 监听,它会在当前组件被销毁的时候自动停止。但如果你想要手动地停止某个 watch,可以调用 watch 函数的返回值即可。

<script>
import { ref, watch } from 'vue';
export default {
  setup() {
    const name = ref('橙某人');
    
    const stopWatch = watch(name, (newValue, oldValue) => {});
    
    // 停止watch
    stopWatch();
    
    stopWatch
  }
}
</script>

九、watchEffect

watchEffectVue3 的新函数,你可以认为它是 watch 的升级版或加强版,它功能和 watch 相似,但却更加强大。

它接收一个函数作为参数,并会立即执行传入的函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

定义看不懂-.-,没关系,我们一样先来看看它的使用方式:

<template>
  <button @click="update1">按钮1</button>
  <button @click="update2">按钮2</button>
</template>

<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    const name = ref('橙某人');
    const age = ref(18);

    watchEffect(() => {
      console.log('**********');
      console.log(name.value);
      console.log(age.value);
    });

    function update1() {
      name.value = 'YDYDYDQ';
    }
    function update2() {
      name.value = 20;
    }

    return { add1, add2 }
  }
}
</script>

在这里插入图片描述

从上图中可以看到,watchEffect 会被立即执行(可以认为 immediate: true),并且不管是修改 name 还是 age 都会触发 watchEffect 执行。

你可以认为 watchEffect 内部使用到什么响应数据,就监听了什么响应数据,只要这些响应数据发生变化,就会触发 watchEffect

停止监听

watchEffect 在组件的 setup 函数或 生命周期钩子 被调用时,监听器会被链接到该组件的生命周期,并在组件卸载时自动停止。当然它和 watch 一样,也能被手动停止。

const stopWatchEffect = watchEffect(() => {});

// 停止WatchEffect
stopWatchEffect();

清除副作用

首先,什么是副作用呢?

在纯函数中,副作用指的是如果一个函数在输入和输出之外还做了其他的事情,那么这个函数额外做的事情就被称为 副作用,而产生副作用的函数被称为 副作用函数

纯函数:你可以简单认为,我调用一个函数,给它传入参数,会得到一个结果,但这个函数在调用过程中不会对程序中的其他变量进行修改,这种函数即可称之为纯函数。

watchEffect(effect) 的回调函数(effect)就是一个副作用函数,因为我们使用watchEffect就是为了监听响应数据变化后做一些其他操作。

一旦副作用函数被执行时,它势必会对程序带来一些影响。有时副作用函数会执行一些异步的副作用,而异步则会带来一些响应(副作用)是"失效"的,我们需要及时清除这些响应。

watchEffect((onInvalidate) => {}) 监听器可以接收一个 onInvalidate 函数作为入参,用来注册清理失效时的回调。

当以下情况发生时,这个失效回调会被触发:

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

在这里插入图片描述

看不懂!!!Em…那我们继续来看例子:

<template>
  <div>count: {{count}}</div>
  <button @click="update">按钮</button>
</template>

<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    const count = ref(1);
    watchEffect(onInvalidate => {
      const timer = setTimeout(() => {
        console.log('发请网络请求')
      }, 1000);
      onInvalidate(() => {
        clearTimeout(timer)
      });
      console.log(count.value);
    });

    function update() {
      count.value++;
    }

    return { count, update }
  }
}
</script>

在这里插入图片描述

从上面效果图可以看到,当我们不断点击去修改 count 的值时(修改过程时间超过 1s),会频繁触发 watchEffect,但是其中的 setTimeout 却不会在规定时间内被执行,而是当停止更改后 1s 后才被执行。

从这个现象有没有让你想起什么呢?没错,就是它,节流函数

我们把 DEMO 修改修改:

<template>
  <input v-model="inputValue" />
</template>

<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    const inputValue = ref('');
    watchEffect(onInvalidate => {
      const timer = setTimeout(() => {
        console.log('输入结束200ms才去请求接口')
      }, 200);
      onInvalidate(() => {
        clearTimeout(timer)
      });
      console.log('输入框的值:', inputValue.value)
    })

    return { inputValue }
  }
}
</script>

在这里插入图片描述

从这个例子里有没有让你悟出些什么呢?

十、provide与inject

provideinject 是一种跨层级组件(祖孙)通信方式。当组件多层嵌套时,不需要将数据一层一层的向下传递,通过它俩可以实现跨层级组件通信。

provide

注入一个值,可以被后代组件接收。

它接受两个参数:

  • 第一个参数是要注入的 key,可以是一个字符串或者一个 symbol
  • 第二个参数是要注入的值。
<template>
  <button @click="update">按钮</button>
  <PChild />
</template>

<script>
import { ref, provide, reactive } from 'vue'
import PChild from './PChild.vue';

export default {
  components: { PChild },
  setup() {
    // 静态值
    let name = '橙某人';
    provide('nameKey', name);

    // 响应式值
    let age = ref(18);
    provide('ageKey', age);

    // 响应式值
    let person = reactive({
      sex: '男'
    });
    provide('personKey', person);

    function update() {
      name = 'YDYDYDQ';
      age.value = 20;
      person.sex = '女'
    }

    return { update };
  }
}
</script>

inject

接收一个由祖先组件或整个应用 (通过 app.provide()) 注入的值。

它接受两个参数:

  • 第一个参数是注入的 key,找不到对应的 key,则返回 undefined 或默认值。

    Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会 “覆盖” 链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject 将返回 undefined,除非提供了一个默认值。

  • 第二个参数是默认值,非必填,也可以是一个工厂函数。

    如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

    const fn = inject('function', () => {}, false)

<template>
  <h3>第二层子组件</h3>
  <div>昵称: {{name}}</div>
  <div>年龄: {{age}}</div>
  <div>性别:{{person.sex}}</div>
</template>

<script>
import { inject } from 'vue';
export default {
  setup() {
    const name = inject('nameKey');
    const age = inject('ageKey');
    const person = inject('personKey');

    return { name, age, person };
  }
}
</script>

在这里插入图片描述

从效果图可以看到,修改静态值视图是不更新的,而修改响应式值视图是可以同步更新的。

后代组件修改注入的值

Vue 是单向数据流,子组件直接修改父组件传递的数据是不被允许的,这样容易造成数据混乱,来源不明等问题。

然而,有时我们确实需要在子组件修改注入的数据,这种情况下我们需要怎么做呢?我们可以在父组件再注入一个方法,提供给后代组件们调用。

父组件:

<script>
import { ref, provide } from 'vue'
export default {
  setup() {
    let name = ref('橙某人');
    provide('nameKey', name);
    
    function updateName(newName) {
      name.value = newName;
    }
    provide('updateNameKey', updateName);
  }
}
</script>

后代组件:

<template>
  <div>昵称: {{name}}</div>
  <button @click="updateName('YDYDYDQ')">按钮</button>
</template>

<script>
import { inject } from 'vue';
export default {
  setup() {
    const name = inject('nameKey');
    const updateName = inject('updateNameKey');

    return { name, updateName }
  }
}
</script>

禁止后代组件修改注入的值

如果要确保注入的数据不会被后代组件更改,我们可以使用 readonly 来加上这个保证。

<script>
import { ref, provide, readonly } from 'vue'
export default {
  setup() {
    let name = ref('橙某人');
    provide('nameKey', readonly(name)); // readonly
    
    function updateName(newName) {
      name.value = newName;
    }
    provide('updateNameKey', updateName);
  }
}
</script>

增加 readonly 后,如果你尝试去修改 inject 接收的值,则控制台会报出提醒。

在这里插入图片描述

当然,即使增加了 readonlyname 的响应式可不会消失哦,一样还是响应式的!

十一、全局API

2.x 全局 API(Vue3.x 实例 API (app)
Vue.config.xxxapp.config.xxx
Vue.config.productionTip移除
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.$mountapp.mount
Vue.prototypeapp.config.globalProperties
app.provide
unmount

十二、其余不常用API

readonly

readonly 接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

<script>
import { readonly } from 'vue'
export default {
  setup() {
    const student = readonly({
       name: '橙某人'
    });
    student.name = 'YDYDYDQ'; // 不可修改
  }
}
</script>

当你尝试修改 readonly 创建的对象时,控制台会有提示:

在这里插入图片描述

shallowRef与shallowReactive

shallowRefref()浅层作用形式。说白了就是把对象的第一层数据变成响应式的,深层的数据不会变成响应式的。

shallowRef 如果用来定义原始数据类型,那么它和 ref 是等同的。

<template>
  <div>{{person}}</div>
  <button @click="update1">按钮一</button>
  <button @click="update2">按钮二</button>
</template>

<script>
import { shallowRef, shallowReactive } from 'vue'
export default {
  setup() {
    const person = shallowRef({
      name: '橙某人',
    });
    
    function update1() {
      person.value.name = 'YDYDYDQ';
      console.log(person.value); // {name: 'YDYDYDQ'}
    }

    function update2() {
      person.value = {
        name: 'ydydydq'
      };
      console.log(person.value); // {name: 'ydydydq'}
    }

    return { person, update1, update2 };
  }
}
</script>

点击按钮一:

在这里插入图片描述

点击按钮二:

在这里插入图片描述

你会发现点击 “按钮一” 数据虽然更改了,但是视图却没有更新,这就是 shallowRef 的作用。

shallowReactiveshallowRef 类似,就不多说了。

shallowRef() 的作用常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

triggerRef

triggerRef 用于强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。说人话就是替 shallowRef 去更新视图。

<template>
  <div>{{person}}</div>
  <button @click="update1">按钮一</button>
  <button @click="update2">按钮二</button>
</template>

<script>
import { shallowRef, triggerRef } from 'vue';
export default {
  setup() {
    const person = shallowRef({
       name: '橙某人'
    });
    function update1() {
      person.value.name = 'YDYDYDQ';
    }
    function update2() {
      triggerRef(person)
    }
    return { person, update1, update2 }
  }
}
</script>

在这里插入图片描述

你是否在想有 triggerRef 那应该就有 triggerReactive 吧?还真没有。

在这里插入图片描述

customRef

customRef 用于创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

可能前半句你看懂了,但后半句直接就懵逼了吧(-.-)。没事,概念这种东西也不是非记住不可,只要知道有那么一回事(可以自定义一个 ref)就可以啦(✪ω✪)。

来看看它具体是如何使用的:

<template>
 <div>姓名:{{ name }}</div>
 <button @click="update">按钮</button>
</template>

<script>
import { customRef } from 'vue';

export default {
  setup() {
    function myRef(value) {
      return customRef((track, trigger) => ({
        get() {
          track();
          return value;
        },
        set(newVal) {
          value = newVal;
          trigger();
        }
      }))
    }
    
    const name = myRef('橙某人');
    function update() { 
      name.value = 'YDYDYDQ';
    }
    return { name, update };
  }
}
</script>

上面的例子中,myRef() 方法的效果就等同于 Vue 里面的 ref。而其中的关键代码应该是 track()trigger() 方法了,可惜在 文档 中只有寥寥数语的介绍。

一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。

不过,问题不大,猜,我们都能大概猜出,执行这两个方法是要干些什么事情了(^ω^) 。

  • track()跟踪数据,收集数据被使用的情况(依赖),后续数据被更改了才知道去更新哪里的视图。
  • trigger()更新视图,当数据被更改了,需要手动通知 Vue 去更新视图。

shallowReadonly

shallowReadonlyreadonly 的浅层作用形式。

这个API很简单,我们直接看个例子就算了。

<template>
 <button @click="update">按钮</button>
</template>

<script>
import { shallowReadonly } from 'vue';

export default {
  setup() {
    const person = shallowReadonly({
      name: '橙某人',
      other: {
        age: 18
      },
      arr: [1, 2, 3]
    })

    function update() {
      person.name = 'YDYDYDQ';
      person.other.age = 20;
      person.arr = [];

      console.log(person); // { name: 'YDYDYDQ', other: { age: 20 }, arr: [1, 2, 3] }
    }

    return { update }
  }
}
</script>

需要注意的是,Vue 并不希望我们把它用在嵌套的响应式对象中。

谨慎使用

浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。

toRaw

toRaw 根据一个 Vue 创建的代理返回其原始对象。

toRaw() 可以返回由 reactive()readonly()shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。

以上是官方文档的解释,看着有点懵,我们直接来看个例子:

<template>
  <div>{{ reactivePerson }}</div>
  <div>{{ toRawPerson }}</div>
  <button @click="update">按钮</button>
</template>
<script>
import { reactive, toRaw } from 'vue';

export default {
  setup() {
    const person = { name: '橙某人', age: 18 };
    const reactivePerson = reactive(person);
    const toRawPerson =  toRaw(reactivePerson);
    
    console.log(person === toRawPerson); // true
    
    function update() {
      toRawPerson.name = 'YDYDYDQ';
      console.log(person, reactivePerson, toRawPerson);
    }

    return { reactivePerson, toRawPerson, update };
  }
}
</script>

点击按钮后,可以发现数据被更改了,但是视图并没有更新。

在这里插入图片描述

这就是 toRaw 想要表达的一个作用。

因为平常我们修改 refreactive 等的数据,每次都会自动同步去更新视图,这在某些场景下是对性能的损耗。有时,我们单纯只希望修改数据而已,并不希望视图同样也跟着去更新,这个时候你就可以使用 toRaw 方法拿到它的原生数据,对原生数据进行修改,这个操作过程数据不会被追踪,不会去更新视图,这对性能有一定的优化作用。

markRaw

markRaw 将一个对象标记为不可被转为代理。返回该对象本身。

说人话就是标记一个对象,使其永远不会再成为响应式对象。

<template>
  <div>{{ reactivePerson }}</div>
  <button @click="update">按钮</button>
</template>

<script>
import { markRaw, reactive } from 'vue';
export default {
  setup() {
    let person = { name: '橙某人' };
    person = markRaw(person);
    const reactivePerson = reactive(person);

    function update() {
      reactivePerson.name = 'YDYDYDQ'; // 视图不会更新
      console.log(reactivePerson);
    }

    return { reactivePerson, update }
  }
}
</script>

在这里插入图片描述

它和 toRaw 相似,但也不全一样,可不要搞混了哦。

十三、多根性质-Fragment

<!-- Vue2这么写会报错 -->
<template>
  <div>昵称: </div>
  <div>年龄: </div>
  <div>性别:</div>
</template>

相信应该有小伙伴在 Vue2 中会遇到这么一个报错:

在这里插入图片描述

这报错提示大概的意思是说组件只能有一个根节点,很多时候我们都会手动包裹一个 div 作为根节点。

<template>
  <div>
    <div>昵称: </div>
    <div>年龄: </div>
    <div>性别:</div>
  </div>
</template>

虽然这可能不是一个多大的问题,但这并不友好,大多数情况下我们可能并不需要这个根节点。

Vue2 为什么不引入 Fragments

Vue2 限制组件只能有一个根节点, 主要原因是虚拟 DOM Diff 算法依赖于具有单个根的组件,如果允许 Fragments 需要对该算法进行重大更改,不仅要使其正常工作,而且必须使其具有高性能,这是一项非常艰巨的任务。

Vue3 中解决了这个问题,它新增了一个类似 DOM 的标签元素 <Fragment />。如果在组件中出现多元素节点,那么在编译时 Vue 会自动为这些元素节点包裹一个<Fragment /> 标签,并且该标签不会出现在 DOM 树中

<!-- Vue3允许这么写 -->
<template>
  <div>昵称: </div>
  <div>年龄: </div>
  <div>性别:</div>
</template>

Vue2 中可以使用 vue-fragments 库来实现 <Fragment />

十四、Teleport-传送组件

TeleportVue3 新增的一个内置组件,它能将其插槽内容渲染到 DOM 中的另一个位置。因为它的性质,我们也常常称呼它为瞬移组件、传送组件、传送门等等。

我们举个栗子来说明(✪ω✪):

<template>
  <div id="box">
    <p>id选择器</p>
  </div>
  <div class="box">
    <p>class选择器</p>
  </div>
  <div data-box>
    <p>class选择器</p>
  </div>
  <!--teleport-->
  <teleport :to="target">
    <h2>橙某人</h2>
  </teleport>
  <div>
    <button @click="update('body')">传送到body中</button>
    <button @click="update('#box')">传送到id选择器中</button>
    <button @click="update('.box')">传送到class选择器中</button>
    <button @click="update('[data-box]')">传送到属性选择器中</button>
  </div>
</template>

<script>
import { ref } from 'vue';
export default {
  setup() {
    const target = ref('#app'); // 初始化渲染的位置

    function update(selector) {
      target.value = selector;
    }

    return { target, update }
  }
}
</script>

在这里插入图片描述

通过上面的例子,可以看到使用 teleportto 属性,我们可以很方便、随意的控制其插槽内容渲染的位置,to 属性可以接收各种 选择器或实际元素

<teleport /> 挂载时,传送的 to 目标必须已经存在于 DOM。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。如果目标元素也是由 Vue 渲染的,你需要确保在挂载 <Teleport> 之前先挂载该元素。

借用 teleport 组件的这种性质,现在就能很容易解决 Vue2 中组件内部嵌套 ModalToast 组件的定位、 z-index 层级的问题了。

禁用 Teleport

<teleport /> 标签一共接收两个属性,除了 to 属性,还有一个 disabled 属性,顾名思义,用于禁用功能的。

还是上面的例子,我们给它添加 disabled 属性:

<template>
  ...
  <!--teleport-->
  <teleport :to="target" :disabled="true">
    <h2>橙某人</h2>
  </teleport>
  ...
</template>

在这里插入图片描述

可以发现,<teleport /> 会直接就渲染插槽的内容,并且点击任何按钮的切换是没有效果的,也就是相当于 to 属性完全不生效了。

十五、Suspense组件

Vue3 内置组件一共增加了两个,除了 <teleport /> 组件,另一个就是 <suspense /> 组件了。

Suspense 它用于协调对组件树中嵌套的异步依赖的处理。看定义应该有点懵,我们还是一样,实践出真理,举个栗子先。

在这里插入图片描述

当然,官网说这还是一个实验性功能!!!

<Suspense> 是一项实验性功能,它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。

新建 AsyncCom.vue 组件:

<template>
  <h1>我是一个异步组件</h1>
</template>

<script>
export default {
  async setup() {
    const res = new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, 3000);
    });
    return await res;
  }
}
</script>

App.vue 中使用:

<template>
  <h1>Suspense</h1>
  <Suspense>
    <template v-slot:default>
      <AsyncCom />
    </template>
    <template v-slot:fallback>
      <h3>Loading...</h3>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'
const AsyncCom = defineAsyncComponent(() => import('./AsyncCom.vue'));

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

在这里插入图片描述

<Suspense /> 组件实际上是一个提升用户友好度的组件,当如果你在渲染时遇到异步依赖项 (异步组件或者具有 async setup() 的组件),它将等到所有异步依赖项解析完成时再显示默认插槽。

可以先详细看看下面的异步组件内容,再回过头来看 <Suspense /> 组件,会有更好的理解。

十六、异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

这是 Vue2 中对于异步组件的定义介绍,看了这个定义可能你和小编一样懵,那么我们就先来看看如何定义、使用一个异步组件。

我们先在 main.js 中定义异步组件:

import Vue from 'vue';
import App from './App.vue';

// 定义一个异步组件
Vue.component('first-async-com', (resolve, reject) => {
  setTimeout(() => {
    const comConfig = {
      render: h => h('p', '我是第一个异步组件,我五秒后才加载!!!')
    };
    resolve(comConfig);
  }, 5000)
});

Vue.config.productionTip = false;

new Vue({
  render: h => h(App)
}).$mount('#app');

然后在 App.vue 中使用:

<template>
  <div>
    <h1>异步组件</h1>
    <first-async-com />
  </div>
</template>

在这里插入图片描述

从效果图中可以看到,我们的异步组件会在五秒后才被挂载到 DOM 树上,这对页面性能来说是一个非常好的优化。当然,我们也可以把 comConfig 单独写在一个文件中,丢到服务器上,再通过请求去获取,这也是一个优化项目体积的手段。

总得来说,异步组件的作用是对于性能的优化。

在这里插入图片描述

如果你注册子组件模板想使用 template 配置项,那么你可能会遇到上方的报错。

Vue.component('first-async-com', (resolve, reject) => {
  setTimeout(() => {
    const comConfig = {
      template: '<p>我是第一个异步组件,我五秒后才加载!!!</p>', // 报错
    };
    resolve(comConfig);
  }, 1000)
});

这是因为 Vue 有两种形式的代码 compiler 模式和 runtime 模式,Vue 模块的 package.jsonmain 字段默认为 runtime 模式,指向了 dist/vue.runtime.common.js 位置。

解决方式:

import Vue from 'vue/dist/vue.js';

new Vue({
  el: '#app',
  template: '<App/>',
  components: { App },
})

我们重新引入 Vuecompiler 模式的包,再修改一下 Vue 初始化的形式,那么子组件的注册即可使用 template 配置项了。

配合webpack实现异步组件

Vue.component('async-webpack-example', (resolve, reject) => {
  // webpack会内置实现 require() 方法
  require(['@/components/my-async-component'], resolve)
})

上面这个例子是 官方 介绍的另一种结合 webpack 实现异步组件的方式。

通过这个特殊的 require 语法将会告诉 webpack 自动将你的构建代码切割成多个包,这些包会通过 Ajax 请求加载。

局部注册异步组件

前面我们介绍的是全局注册异步组件,接下来就看看如何在局部中注册异步组件。

先创建一个新组件:

<template>
  <div>我是第二个异步组件</div>
</template>

App.vue 中局部注册异步组件:

<template>
<div>
  <second-async-com />
</div>
</template>

<script>
export default{
  components: {
    'SecondAsyncCom': () => import('@/components/SecondAsyncCom.vue')
  }
}
</script>

在这里插入图片描述

从上图中,可以看到该组件会被单独异步加载获取。

这是借用 import() 语法来实现的导入,你也可以直接提供一个返回 Promise 的函数,例如:

<script>
export default {
  components: {
    'SecondAsyncCom': () => new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({
          template: '<div>我是第二个异步组件</div>'
        })
      }, 1000)
    })
  }
}
</script>

最后,我们把例子改成同步的形式来进行对比:

<script>
import SecondAsyncCom from '@/components/SecondAsyncCom.vue';
export default {
  components: {
    SecondAsyncCom
  }
}
</script>

在这里插入图片描述

可以发现同步形式并没有单独加载组件,它会被打包到一起,并同步加载、顺序渲染。

Vue3的异步组件

上面罗里吧嗦回顾了一下 Vue2 异步组件的内容,那么 Vue3 中的异步组件又是如何的呢?

整体来说,没啥改变,可能更多的是在语法方面的改变而已。

我们直接来看看,在 Vue3 项目的 App.vue 文件中:

<template>
  <AsyncCom />
</template>

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

export default {
  components: {
    'AsyncCom': defineAsyncComponent(() => import('./components/AsyncCom.vue'))
  },
}
</script>

同样,你也可以直接提供一个返回 Promise 的函数即可:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

十七、Vue3响应式原理

文章字数有点多了,滚动起来总是一卡顿一卡顿的,原理这块只能单独再写一篇文章了,写完会回来补上链接。

在这里插入图片描述

十八、路由

vue-router 相信使用过 Vue 的小伙伴都不陌生了,Vue 通过它来实现对路由的管理,而在 Vue3 中使用 vue-router 的版本至少需要为 4.x.x 以上,下面我们就来看看它的具体使用过程。

安装

npm install vue-router@4 --save

基本使用

基础配置与使用

创建 router/index.js 文件:

import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';

const routes = [
  { path: '/', name: 'Main', component: ()=> import('../views/main.vue')  },
  { path: '/login', name: 'login', component: ()=> import('../views/login.vue')  },
];

const router = createRouter({
  history: createWebHashHistory(), // createWebHashHistory() || createWebHistory()
  routes
})

export default router;

main.js 中引入:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)
app.use(router)
app.mount('#app')

App.vue 中使用:

<template>
  <router-link to="/">主页</router-link> | 
  <router-link to="/login">登陆页</router-link>
  <router-view />
</template>
具体业务使用router
<script>
import { useRouter, useRoute } from 'vue-router';
export default {
  setup() {
    const router = useRouter();
    const route = useRoute();
    // 获取路由携带的参数信息
    console.log(route.query)
    
    // 路由跳转
    function toMainPage() {
      router.push({
        path: '/',
        query: {
          a: 1
        }
      })
    }
    
    return { toMainPage }
  }
}
</script>

十九、EventBus与mitt

Vue2 中我们使用 EventBus 来实现跨组件之间的一些通信,它依赖于 Vue 自带的 $on/$emit/$off 等方法,这种方式使用非常简单方便,但如果使用不当也会带来难以维护的毁灭灾难。

Vue3 中移除了这些相关方法,这意味着 EventBus 这种方式我们使用不了, Vue3 推荐尽可能使用 props/emitsprovide/injectvuex 等其他方式来替代。

当然,如果 Vue3 内部的方式无法满足你,官方建议使用一些外部的辅助库,例如:mitt

优点

  • 非常小,压缩后仅有 200 bytes
  • 完整 TS 支持,源码由 TS 编码。
  • 跨框架,它并不是只能用在 Vue 中,ReactJQ 等框架中也可以使用。
  • 使用简单,仅有 onemitoff 等少量实用API。

安装

npm install mitt --save

基本使用

新建 bus.js 文件:

import mitt from 'mitt';

const emiter = mitt();

export default emiter;

main.vue 组件中引入并监听:

<script>
import emitter from '../utils/bus.js'

export default {
  setup() {
    emitter.on('EventName', res => {
      console.log(res)
    });
  }
}
</script>

login.vue 组件中引入并发送信息:

<template>
  <button @click="send">点击发送信息</button>
</template>

<script>
import { onBeforeUnmount } from 'vue';
import emitter from '../utils/bus.js'

export default {
  setup() {
    function send() {
       emitter.emit('EventName', '我发送个信息')
    }
    
    onBeforeUnmount(() => { 
        emitter.off('EventName'); 
    });
    
    return { send }
  }
}
</script>

源码

export default function mitt(all) {
  all = all || new Map();
  return {
    all: all,
    on: function (type, handler) {
      var handlers = all.get(type);
      if (handlers) {
        handlers.push(handler);
      } else {
        all.set(type, [handler]);
      }
    },
    off: function (type, handler) {
      var handlers = all.get(type);
      if (handlers) {
        if (handler) {
          handlers.splice(handlers.indexOf(handler) >>> 0, 1); // 无符号右移运算符
        } else {
          all.set(type, []);
        }
      }
    },
    emit: function (type, evt) {
      var handlers = all.get(type);
      if (handlers) {
        handlers.slice().map(function (handler) {
          handler(evt);
        });
      }
      handlers = all.get("*");
      if (handlers) {
        handlers.slice().map(function (handler) {
          handler(type, evt);
        });
      }
    },
  };
}

以上是精简之后的 JS 核心源码,整体来说并不是很难吧(✪ω✪),它围绕着发布订阅者模式而展开。

可能唯一让人感到陌生的代码是无符号右移运算符(>>>),关于它的作用小伙伴们就要自行去了解啦。

二十、Pinia

PiniaVue 官方团队成员专门开发的一个全新状态管理库,并且 Vue 的官方状态管理库已经更改为了 Pinia。在 Vuex 官方仓库中也介绍说可以把 Pinia 当成是不同名称的 Vuex 5,这也意味不会再出 5 版本了。

当然,Pinia 可不是 Vue3 专属,它同样也是适用于 Vue2 的。

中文文档

优点

  • 更加轻量级,压缩后提交只有1.6kb
  • 完整的 TS 的支持,Pinia 源码完全由 TS 编码完成。
  • 移除 mutations,只剩下 stateactionsgetters
  • 没有了像 Vuex 那样的模块镶嵌结构,它只有 store 概念,并支持多个 store,且都是互相独立隔离的。当然,你也可以手动从一个模块中导入另一个模块,来实现模块的镶嵌结构。
  • 无需手动添加每个 store,它的模块默认情况下创建就自动注册。
  • 支持服务端渲染(SSR)。
  • 支持 Vue DevTools
  • 更友好的代码分割机制,传送门

安装

npm install pinia --save
// or
npm i pinia -S

基本使用

初始化并挂载Pinia

我们还是根据老传统,在src 文件夹下,新建 store/index.js 文件:

import { createPinia } from 'pinia';

const pinia = createPinia();

export default pinia;

main.js 文件中进行全局挂载:

import { createApp } from 'vue';
import App from './App.vue';
import pinia from './store';

const app = createApp(App);

app.use(pinia);
app.mount('#app');
创建store

完成 Pinia 的初始化后,就可以来定义 store 进入具体使用了,而定义 store 一共有三种不同形式的写法,下面我们一一列举。

创建 src/store/modules/user.js 文件:

import { defineStore } from 'pinia';

// options API模式
export const usePersonStore = defineStore('person', {
  state: () => {
    return {
      name: '人类'
    }
  },
  actions: {
    updateName(newName) {
      this.name = newName;
    }
  },
  getters: { // getters基本与Vue的计算属性一致
    getFullName() {
      return 'Full' + this.name;
    }
  }
})

// 对象形式
export const useTeacherStore = defineStore({
  id: 'teacher',
  state: () => {
    return {
      name: '老师'
    }
  },
  actions: {
    updateName(newName) {
      this.name = newName;
    }
  },
  getters: {
    getFullName() {
      return 'Full' + this.name;
    }
  }
})

// setup模式
import { computed, ref } from 'vue';
export const useStudentStore = defineStore('student', () => {
  const name = ref('学生');

  function updateName(newName) {
    name.value = newName;
  }

  const getFullName = computed(() => 'Full' + name.value);

  return { name, updateName, getFullName }
})

上面例子中,我们列举了同个 DEMO 不同的三种写法,它们效果都一样,第一和第二种类似,比较简单;第三种适合 Vue3script setup 的形式。

需要我们注意的是其中的 id 是必填且需要唯一的。

具体业务使用store

App.vue 中使用:

<template>
  <div>
    <h1>Pinia</h1>
    <div>state: {{personStore.name}} ****** getter: {{personStore.getFullName}}</div>
    <div>state: {{studentStore.name}} ****** getter: {{studentStore.getFullName}}</div>
    <div>state: {{teacherStore.name}} ****** getter: {{teacherStore.getFullName}}</div>
    <button @click="updateName">点击</button>
  </div>
</template>

<script>
import { usePersonStore, useStudentStore, useTeacherStore } from '@/store/modules/user.js';

export default {
  setup() {
    const personStore = usePersonStore();
    const studentStore = useStudentStore();
    const teacherStore = useTeacherStore();

    function updateName() {
      personStore.updateName('人类-updated')
      studentStore.updateName('学生-updated')
      teacherStore.updateName('老师-updated')
    }

    return {
      personStore,
      studentStore,
      teacherStore,
      updateName
    }
  }
}
</script>

在这里插入图片描述

上面我们通过 actions 来修改 state ,当然你也可以直接就去 state ,但是更推荐使用前者。actions 支持同步与异步,也支持 await 形式。

第三种修改state方法-$patch()

Pinia 中,一共有三种修改 state 的方式:

  • 直接修改 state
  • 通过 actions 修改。
  • 通过 $patch 批量修改。

新建 src/store/modules/count.js 文件:

import { defineStore } from 'pinia';

export const useCountStore = defineStore('count', {
  state: () => {
    return {
      count1: 0,
      count2: 0,
      count3: 0,
    }
  },
})

App.vue 中使用:

<template>
  <div>
    <h1>Pinia</h1>
    <div>count1: {{countStore.count1}}</div>
    <div>count2: {{countStore.count2}}</div>
    <div>count3: {{countStore.count3}}</div>
    <button @click="updateName">点击</button>
  </div>
</template>

<script>
import { useCountStore } from '@/store/modules/count.js';

export default {
  setup() {
    const countStore = useCountStore();

    function updateName() {
      // 直接修改
      // countStore.count1++;
      // countStore.count2++;
      // countStore.count3++;
      
      // 通过$patch批量修改
      countStore.$patch(state => {
        state.count1++;
        state.count2++;
        state.count3++;
      });
      // $patch的另外一种形式
      // countStore.$patch({
      //   count1: 2,
      //   count2: 2,
      //   count3: 2
      // })
    }

    return { countStore, updateName }
  }
}
</script>

上面我们展示了直接修改与通过 $patch() 方法进行批量修改的两种形式,它们效果一样。那它们有些什么更具体的区别呢?小编从网上的一些文章看到说通过 $patch() 修改 state 更具性能优势,也就是$patch() 方法是经过性能调优的,但是在 官网 却好像并没有找到相关介绍。如果小伙伴们有找到这方面的东西,欢迎给我留留言告知,感谢感谢。

在这里插入图片描述

Pinia解构方法-storeToRefs

上面的例子中,我们都是通过 .xxx 来访问数据,但是当很多数据被使用时,为了更简洁的使用这些数据,我们都会采用解构的方式来优化。

ES6 中普通的解构形式虽然能获取到值,但是却会失去响应式。

Pinia 为我们提供了 storetorefs API 方便一次性获取所有的数据,并且它们依旧是具备响应式的。

新建 src/store/modules/info.js 文件:

import { defineStore } from 'pinia';

export const useInfoStore = defineStore('info', {
  state: () => {
    return {
      name: '橙某人',
      age: 18
    }
  }
})

App.vue 文件中使用:

<template>
  <div>
    <h1>Pinia</h1>
    <div>{{ name }}</div>
    <div>{{ age }}</div>
    <button @click="update">点击</button>
  </div>
</template>

<script>
import { useInfoStore } from '@/store/modules/info.js';
import { storeToRefs } from 'pinia';

export default {
  setup() {
    const infoStore = useInfoStore();
    const { name, age } = storeToRefs(infoStore); // 解构

    function update() {
      infoStore.name = 'YDYDYDQ';
      infoStore.age = 20;
    }

    return { name, age, update }
  }
}
</script>

在这里插入图片描述

store互相调用

新建 src/store/modules/other.js 文件:

import { defineStore } from 'pinia';

export const useOtherStore = defineStore('other', {
  state: () => {
    return {
      hobby: ['吃饭', '睡觉', '打豆豆']
    }
  }
})

src/store/modules/info.js 文件中引入:

import { defineStore } from 'pinia';
import { useOtherStore } from './other';

export const useInfoStore = defineStore('info', {
  state: () => {
    return {
      name: '橙某人',
      age: 18
    }
  },
  getters: {
    getHobby() {
      return useOtherStore().hobby // 获取爱好列表
    }
  }
})

数据持久化

项目中的状态管理一般我们需要把它进行数据持久化,否则一刷新就会造成数据丢失。而 Pinia 配套有个插件 pinia-plugin-persist 可以帮助我们来做这件事情。

安装

npm i pinia-plugin-persist -S

初始化

import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';

const pinia = createPinia();
pinia.use(piniaPluginPersist);

export default pinia;

在对应的 store 中开启缓存:

// info.js
import { defineStore } from 'pinia';
import { useOtherStore } from './other';

export const useInfoStore = defineStore('info', {
  state: () => {
    return {
      name: '橙某人',
      age: 18
    }
  },
  getters: {
    getHobby() {
      return useOtherStore().hobby
    }
  },
  persist: {
    enabled: true, // 开启缓存
  }
})

开启缓存后,默认数据是缓存在 sessionStorage 里面的。

在这里插入图片描述

也可以指定成 localStorage 作为缓存:

// info.js
import { defineStore } from 'pinia';
import { useOtherStore } from './other';

export const useInfoStore = defineStore('info', {
  state: () => {
    return {
      name: '橙某人',
      age: 18
    }
  },
  getters: {
    getHobby() {
      return useOtherStore().hobby
    }
  },
  persist: {
    enabled: true, // 开启缓存
    strategies: [
      {
        key: 'infoStore', // 修改缓存的key
        storage: localStorage,    // 指定localStorage作为缓存
        paths: ['name']  // 只缓存name
      }
    ]
  }
})

在这里插入图片描述

更多细节就自行查阅文档了(✪ω✪),传送门




至此,本篇文章就写完啦,撒花撒花。

在这里插入图片描述

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

本文首发于掘金社区,如希望有更好的阅读效果,欢迎前去查看,也期望着你的点赞。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值