Vue3中双向数据绑定与Pinia实践+JS数据引用的循环修改问题
Vue3 + Pinia
VUE3虽然出了很久了,但是很少深入研究,目前项目上遇到了一些问题,所以做个Note解决一下疑问:
- v-bind/v-model怎么与Pinia进行结合
- Object/Array数据大量处理时,为何有的修改不生效
- 组合API与选项API选择 (TS不考虑)
- This指针问题
生命周期
Vue3基础语法
不熟悉情况下直接选择选项式API。对TS和组合有需求的再选组合式API。
语法风格
注意,生成的项目中的示例组件使用的是组合式 API 和 <script setup>
,而非选项式 API。下面是一些补充提示:
- 推荐的 IDE 配置是 Visual Studio Code + Volar 扩展。如果使用其他编辑器,参考 IDE 支持章节。
- 更多工具细节,包括与后端框架的整合,我们会在工具链指南进行讨论。
- 要了解构建工具 Vite 更多背后的细节,请查看 Vite 文档。
- 如果你选择使用 TypeScript,请阅读 TypeScript 使用指南。
选项式 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 变量 */ }">
特殊数据的监听
- 数组
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
- 替换一个数组
this.items = this.items.filter((item) => item.message.match(/Foo/))
- 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与双向绑定
- 对于value绑定的类型(input, select, options, textarea, radio, checkbox)直接使用
v-model
绑定 - 对于自定义类型,需要手动处理
v-bind
与click/change
一类的事件,借用$event.target.value
处理。 - 如果使用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'],
},
})