Vue3 + Pinia

VUE3虽然出了很久了,但是很少深入研究,目前项目上遇到了一些问题,所以做个Note解决一下疑问:

  1. v-bind/v-model怎么与Pinia进行结合
  2. Object/Array数据大量处理时,为何有的修改不生效
  3. 组合API与选项API选择 (TS不考虑)
  4. This指针问题

生命周期
https://cn.vuejs.org/assets/lifecycle.16e4c08e.png

Vue3基础语法

不熟悉情况下直接选择选项式API。对TS和组合有需求的再选组合式API。

语法风格

注意,生成的项目中的示例组件使用的是组合式 API<script setup>,而非选项式 API。下面是一些补充提示:

选项式 API (Options API)

选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

<script>
export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 `this` 上
  data() {
    return {
      count: 0
    }
  },

  // methods 是一些用来更改状态与触发更新的函数
  // 它们可以在模板中作为事件监听器绑定
  methods: {
    increment() {
      this.count++
    }
  },

  // 生命周期钩子会在组件生命周期的各个不同阶段被调用
  // 例如这个函数就会在组件挂载完成后被调用
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

组合式 API (Composition API)

这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。
选项式 API 是在组合式 API 的基础上实现的.

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

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Demo

全局引入网页:

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">{{ message }}</div>

<script>
  const { createApp } = Vue
  
  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

模块化ES开发:

<!-- index.html -->
<div id="app"></div>

<script type="module">
  import { createApp } from 'vue'
  import MyComponent from './my-component.js'

  createApp(MyComponent).mount('#app')
</script>
// my-component.js
export default {
  data() {
    return { count: 0 }
  },
  template: `<div>count is {{ count }}</div>`
}

组件化开发:

// ButtonCounter.vue
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text'],
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <h4>{{ title }}</h4>
  <button @click="count++">You clicked me {{ count }} times.</button>
  <button @click="$emit('enlarge-text')">Enlarge text</button>
  <slot /> <!-- 这是一个插槽占位符,通过父组件传递给子组件 -->
</template>


// main.vue
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  },
  methods:{
    btn_click(){},
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter title="My journey with Vue" @enlarge-text="btn_click" />
  <ButtonCounter title="abc" @enlarge-text="btn_click" />
</template>

所有 prop 默认都是可选的,除非声明了 required: true。
props的类型校验参考 https://cn.vuejs.org/guide/components/props.html#prop-validation

v-slot

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

几个重要语法

没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。


// 默认的绑定都是单向的: vue to html
<span>数据绑定: {{ msg }}</span>

<p>渲染原始HTML: <span v-html="rawHtml"></span></p>

<div v-bind:id="dynamicId">属性绑定(v-bind:可以省略为:)</div>

<div class="static" :class="{ active: isActive, 'text-danger': hasError }" ></div>

objectOfAttrs: {
    id: 'container',
    class: 'wrapper',
    href: 'href'
}
<a v-bind="objectOfAttrs">同时绑定一个元素的多个属性</a>

// 每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码
<span>绑定支持表达式: {{ msg?:'abc':'123' + id }}</span>  

// 动态attribute bind
<a v-bind:[attributeName]="url"> ... </a>
<a :[attributeName]="url"> ... </a>
<!-- 这会触发一个编译器警告, 用计算属性替代 -->
<a :['foo' + bar]="value"> ... </a>


// on 用来监听DOM事件
<a v-on:click="doSomething"> ... </a>
<a @click="doSomething"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>
<a @[eventName]="doSomething">

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

// 修饰符 Modifiers, prevent会调用event.preventDefault() 
<form @submit.prevent="onSubmit">...</form>

<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>



// v-if 可以用在template上
// 当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行。

// items: [{ message: 'Foo' }, { message: 'Bar' }]
<li v-for="item in items">
  {{ item.message }}
</li>
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

// 也可以对一个对象object使用v-for遍历所有属性
<li v-for="value in myObject">
  {{ value }}
</li>
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

表单与双向绑定

<input :value="text" @input="event => text = event.target.value" />
// 与 v-model 等价
<input v-model="text" />

// 双向绑定数组 checkedNames: []
// 选中后 checkedNames = ['jack','john','mike']
<div>Checked names: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>

// 如果是单选radio, 则只会存储一个值: picked = 'one' or 'two'
<div>Picked: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>

// select 单选时 等同 radio效果
// <select v-model="selected" multiple> => 多选等同于checkbox效果
<div>Selected: {{ selected }}</div>

<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option value="1">A</option>
  <option>B</option>
  <option>C</option>
</select>

// 动态绑定,pick被自动设置为first/second
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

// v-model的修饰符:.lazy/.number/.trim 

DOM元素绑定 ref
ref 是一个特殊的 attribute,它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。

<script>
export default {
  mounted() {
    this.$refs.input.focus()
  }
}
</script>

<template>
  <input ref="input" />
</template>

当在 v-for 中使用模板引用时,相应的引用中包含的值是一个数组:<li v-for="item in list" ref="items">

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

特殊数据的监听

  1. 数组
    Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()
  • 替换一个数组this.items = this.items.filter((item) => item.message.match(/Foo/))
  1. watch
    参考vue进行。默认是浅层watch.

JS实用小技巧

Vue3中的this

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
      // 在Dom更新后被调用
      nextTick(() => {
        // 访问更新后的 DOM
      })
    }
  },
  mounted() {
    // 在其他方法或是生命周期中也可以调用方法
    this.increment()
  }
}

**Vue 自动为 methods 中的方法绑定了永远指向组件实例的 this。**这确保了方法在作为事件监听器或回调函数时始终保持正确的 this

你不应该在定义 methods 时使用箭头函数,因为箭头函数没有自己的 this 上下文。

箭头函数没有自己的this值,箭头函数中所使用的this来自于函数作用域链(上下文)。

const obj = {
  name: 'this test',

  // 普通函数/匿名函数
  func1: function(){
    console.log(this) // 这个this指向对象obj本身, vue帮忙做的bind
  },
  func2(){
    console.log(this) // 这个this指向对象obj本身
  }

  // 指针/箭头函数
  // 错误做法,VUE不支持
  // 语法不对,仅示意
  func3: ()=>{
    console.log(this) // 这个this指向父对象,windows,不是obj
  },

  // 组合
  func4: function(){
    console.log(this) // 这个this指向对象obj本身
    func4_1: ()=>{
      console.log(this) // 这个this指向父对象,func4,但func4的this指向obj,所以这里this也是obj
    }
  }

  // 组合+推荐方案
  func5: function(){  // 这里VUE做了bind处理
    console.log(this) // 这个this指向对象obj本身, vue帮忙做的bind(this)
    setTimeout(function(){
      console.log(this) // 这里this有问题,不指向obj
      // solution:
      // 1. 手动 setTimeout().bind(this)
      // 2. let self = this; 然后 self.xxxx
      // 3. 使用 `()=>{}` 箭头函数,不使用匿名函数
    }, 1000);
  },
}

JS foreach 陷阱

在JS中,除了基本数据类型(number,string, boolean),其他都是引用类型。所以,对object赋值,都是引用的对方,进行的浅拷贝。

所以,我们在对array进行forEach并且修改值的时候,可能发生修改了没有效果的情况。

case 1:

let a = [1, 10, 21, 33];

// 错误做法
a.forEach((item, index, arr)={
  item += 100;  // 这种修改是无效的,因为每次的item都是一个全新的,与原来的item没关系
});

// 正确做法
a.forEach( (val, index)=> a[index]+=100 );
a.forEach( (val, index, arr)=>{
  arr[index] += 1000; // a[index] += 1000; 也可以
});

case 2:

let a = [
  {b1: 1001, b2: 'xxxx', b3:[1,2,3]},
  {b1: 1002, b2: '123111', b3:[1,2,3]},
  {b1: 1003, b2: 'aaaaaaaaaa', b3:[1,2,3]},
];

// forEach与手写for循环效果一样

// 针对数组a
a.forEach((val, index)=>{
  val.b2 += "_method1";  // 因为val是object(非基本数据类型),对原来item的引用,所以可以修改
});

// 针对数组b3
// 建议的做法
a.forEach((val, index)=>{
  val.b2 += "_method2";
  val.b3.forEach((v2, i2, arr)=>{
    // 正确做法, 2种都可以
    val.b3[i2] += 100;
    arr[i2] += 1000;

    // 错误做法
    v2 += 10000; // v2是number,不能这样修改
  });
});

数据扩展

注意: 数据只会展开一层,内部数据不会展开。

let a = [1,2,3];
let b = [3,4,5];

console.log([...a, ...b])  // [1,2,3,3,4,5]

let x = {a:1, b:true, c: '123'};
let y = {a:88, d: [1,2,3]};

console.log({...a, ...b}) // {a:88, b:true, c: '123', d:[1,2,3]}
console.log({...b, ...a}) // {a:1, d:[1,2,3], b:true, c: '123'}

Pinia与双向绑定

  1. 对于value绑定的类型(input, select, options, textarea, radio, checkbox)直接使用v-model绑定
  2. 对于自定义类型,需要手动处理v-bindclick/change一类的事件,借用$event.target.value处理。
  3. 如果使用Pinia则考虑storeToRefs映射变量,或者通过action/$patch功能手动设定。

参考: https://www.45fan.com/article.php?aid=1D0cTL9M3N582fPx

import { defineStore } from 'pinia'
// 创建store,命名规则: useXxxxStore
// 参数1:store的唯一表示
// 参数2:对象,可以提供state actions getters
const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
   double() {
      return this.count * 2
    },
  },
  actions: {
    increment() {
      this.count++
    },
    incrementAsync() {
      setTimeout(() => {
        this.count++
      }, 1000)
    },
  },
})

export default useCounterStore
<script setup>
import useCounterStore from './store/counter'

const counter = useCounterStore()

// 如果直接从pinia中解构数据,会丢失响应式, 使用storeToRefs可以保证解构出来的数据也是响应式的
//const { count, double } = counter  // 错误,这个没有响应性,不可以这样
const { count, double } = storeToRefs(counter)  // 这个可以

</script>

<template>
  <h1>根组件---{{ counter.count }}</h1>  <!-- 这个也具有响应性 -->
  <h1>响应测试: {{ count +' - '+ double }}</h1>
  <button @click="counter.increment">加1</button>
  <button @click="counter.incrementAsync">异步加1</button>
</template>

<style></style>

如果想做pinia数据持久化, 使用插件pinia-plugin-persistedstate

import { createApp } from "vue";
import App from "./App.vue";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
createApp(App).use(pinia);

自定义选项:

import { defineStore } from 'pinia'

export const useStore = defineStore('main', s{
  state: () => {
    return {
      someState: 'hello pinia',
      nested: {
        data: 'nested pinia',
      },
    }
  },
  // 所有数据持久化
  // persist: true,
  // 持久化存储插件其他配置
  persist: {
    // 修改存储中使用的键名称,默认为当前 Store的 id
    key: 'storekey',
    // 修改为 sessionStorage,默认为 localStorage
    storage: window.sessionStorage,
    // 部分持久化状态的点符号路径数组,[]意味着没有状态被持久化(默认为undefined,持久化整个状态)
    paths: ['nested.data'],
  },
})
Logo

前往低代码交流专区

更多推荐