Vue3+TS踩坑日记(一):pinia store怎么丢失响应了?

259 阅读3分钟

背景

鉴于最近发现自己学艺不精,因此打算好好深造一下Vue3相关的技术,现有的工作经验中使用Vue3的地方都太过简单,导致在最近的一次面试中发现自己缺失太多需要学习的东西,因此痛定思痛,打算做一个共享画布的项目来从中学习,并且将项目进度和学习到的东西做下记录并和大家分享,希望和我一样境地的同学能一起学到东西,同时因为第一次写博客,也希望各位大佬尽量地批评和建议,感谢。

项目简介

pixel-craft,用户可以在工具栏中选择需要画的形状/笔,在画布区域能监听用户的鼠标操作完成绘制,后续加入多人共享画布一起绘制的功能。

当前进度

已搭建好Vue3+TS+pinia+tailwind的项目框架。 实现了简单的工具栏展示和画布区域。

image.png

今日学习(Store和Vue监听底层原理)

@/views/IndexView.vue 主页面,包括工具栏和画布区域,还有一个用于测试store是否有效改变的内容。

<script setup lang="ts">
import CanvasArea from '@/components/common/CanvasArea.vue'
import ToolBar from '@/components/common/ToolBar.vue'
import { useToolStore } from '@/stores/tool'
const { chosedTool } = useToolStore()
</script>
<template>
  <div style="position: fixed; left: 100px; top: 100px"> {{ chosedTool }}</div>
  <ToolBar />
  <CanvasArea />
</template>
<style scoped></style>

@/stores/tool.ts 简单定义了一个全局的当前选中工具变量,并提供了修改这个变量的方法。

import { ToolTypes } from '@/types/tool'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useToolStore = defineStore('tool', () => {
  const chosedTool = ref(ToolTypes.SELECT)
  function setChosedTool(tool: ToolTypes) {
    chosedTool.value = tool
  }

  return { chosedTool, setChosedTool }
})

@/component/common/ToolBar.vue 两个形状,暂时用样式实现了,矩形和圆形,点击形状时调用setChosedTool

<script setup lang="ts">
import { useToolStore } from '@/stores/tool'
import { ToolTypes } from '@/types/tool'

const { setChosedTool } = useToolStore()
const handleToolClick = (toolType: ToolTypes) => {
  setChosedTool(toolType)
}
</script>
<template>
  <div class="container mx-auto fixed top-4 left-0 right-0 flex rounded-md p-2 shadow-sm">
    <div
      @click="handleToolClick(ToolTypes.RECTANGLE)"
      class="rounded-xs size-8 hover:bg-sky-200 flex items-center justify-center"
    >
      <div class="size-4 border-2 rounded-xs"></div>
    </div>
    <div
      @click="handleToolClick(ToolTypes.CIRCLE)"
      class="rounded-xs size-8 hover:bg-sky-200 flex items-center justify-center"
    >
      <div class="size-4 border-2 rounded-lg"></div>
    </div>
  </div>
</template>

<style scoped></style>

因为工具栏和画布区域是兄弟组件,因此决定通过一个useToolStore来管理全局的当前选中工具,此时就开始遇到了我的第一个问题:

问题一:为什么我的chosedTool不响应?

无论我点击圆形还是矩形,会发现IndexView中的{{ chosedTool }}都是默认值没有任何变化,这就暴露了我的第一个缺陷:我不明白store的响应式是整体的,而不是对其中的一个属性(store.chosedTool),当我在IndexView中用解构的方式取出chosedTool时,它只是在当前组件中保存下了当时chosedTool的值,而Vue中响应的是store这个整体,因此当触发setChosedTool时,store.chosedTool确实改变了,但是IndexView一开始定义的chosedTool没有发生改变,所以不会响应更改。

解决方法

得知只有整个store才会响应监听的情况下,就可以将代码修改为如下方式:

(1)直接在组件中读取整个store

<script setup lang="ts">
import CanvasArea from '@/components/common/CanvasArea.vue'
import ToolBar from '@/components/common/ToolBar.vue'
import { useToolStore } from '@/stores/tool'
const toolStore = useToolStore()
</script>
<template>
  <div style="position: fixed; left: 100px; top: 100px"> {{ toolStore.chosedTool }}</div>
  <ToolBar />
  <CanvasArea />
</template>
<style scoped></style>

(2)也通过storeToRefs实现解构也能响应,这是pinia提供的方法,能将store中的属性转为响应式的

<script setup lang="ts">
import CanvasArea from '@/components/common/CanvasArea.vue'
import ToolBar from '@/components/common/ToolBar.vue'
import { useToolStore } from '@/stores/tool'
import { storeToRefs } from 'pinia'
const toolStore = useToolStore()
const { chosedTool } = storeToRefs(toolStore)
</script>
<template>
  <div style="position: fixed; left: 100px; top: 100px"> {{ chosedTool }}</div>
  <ToolBar />
  <CanvasArea />
</template>
<style scoped></style>

问题二:既然使用整个Store就不会丢失响应,那chosedTool能否不用ref定义?

import { ToolTypes } from '@/types/tool'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useToolStore = defineStore('tool', () => {
  let chosedTool = ToolTypes.SELECT
  function setChosedTool(tool: ToolTypes) {
    chosedTool = tool
  }

  return { chosedTool, setChosedTool }
})

直接说结果:不能,因为Vue不会无缘无故地收集监听,必须得通过ref或者reactive“告知”Vue,当前变量需要进行响应式监听,Vue才会去收集依赖并且在它变更时通知所有组件。上述写法相当于:

<script setup lang="ts">
let counter = 1;

function increment(){
    counter++
}
</script>
<template>
  <div @click="increment"> {{ counter }}</div>
</template>
<style scoped></style>

总结

  1. pinia store的响应式是改变store整个对象,直接取出其中的属性会丢失响应式;
  2. store中的属性如果需要响应,需要通过ref/reactive包裹,告知Vue我需要监听这个变量。