Pinia小抄

Pinia (/piːnjʌ/)是一款类型安全、可扩展以及模块化设计的 Vue.js 状态管理库。

Pinia 应用示例

通过 defineStore 先创建一个 Store:

stores/counter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 };
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++;
}
}
});

在组件中引入该 Store 并使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
// setup() 函数
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
counter.count++;
// 自动补全
counter.$patch({ count: counter.count + 1 });
// 或使用 action 代替
counter.increment();
</script>
<template>
<!-- 直接从 store 中访问 state -->
<div>Current Count: {{ counter.count }}</div>
</template>

Pinia 也提供了一组类似 Vuex 的映射 state 的辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: state => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});

const useUserStore = defineStore('user', {
// ...
});

export default defineComponent({
computed: {
// 其他计算属性
// ...
// 允许访问 this.counterStore 和 this.userStore
...mapStores(useCounterStore, useUserStore),
// 允许读取 this.count 和 this.double
...mapState(useCounterStore, ['count', 'double'])
},
methods: {
// 允许读取 this.increment()
...mapActions(useCounterStore, ['increment'])
}
});

Pinia 应用方式

使用包管理器安装 Pinia:

  • Yarn: yarn add pinia
  • NPM: npm install pinia

Pinia 使用到了组合式 API,如果你是用的 Vue<2.7版本,还需要安装组合式API包: @vue/composition-api

在 Vue3 中,创建一个 pinia 实例 并将其传递给应用:

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const pinia = createPinia();
const app = createApp(App);

app.use(pinia);
app.mount('#app');

在 Vue2 中,还需要安装一个插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createPinia, PiniaVuePlugin } from 'pinia';

Vue.use(PiniaVuePlugin);
const pinia = createPinia();

new Vue({
el: '#app',
// 其他配置...
// ...
// 请注意,同一个`pinia'实例
// 可以在同一个页面的多个 Vue 应用中使用。
pinia
});

Pinia 概念

Store

Store 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定,承载着全局状态。一个 Store 应该包含可以在整个应用中访问的数据,你应该避免在 Store 中引入那些原本可以在组件中保存的本地数据。

Store 是用 defineStore() 定义的,它的第一个参数要求是一个唯一的名字,用作id,返回一个函数,为了符合组合式函数风格,通常命名为 use<id>StoredefineStore() 的第二个参数可接受两类值:Setup 函数或 Option 对象

当第二个参数使用 Option 对象时,可以传入一个带有 stateactiongetters 属性的 Option 对象。

1
2
3
4
5
6
7
8
9
10
11
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: state => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});

第二个参数使用 Setup 函数时,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象:

1
2
3
4
5
6
7
8
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
function increment() {
count.value++;
}

return { count, increment };
});

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

Store 在使用 <script setup> 调用 useStore() (或者调用 setup() 函数)时,会被创建。

1
2
3
4
5
<script setup>
import { useCounterStore } from '@/stores/counter';
// 可以在组件中的任意位置访问 `store` 变量
const store = useCounterStore();
</script>

为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs(),它将为每一个响应式属性创建引用。

1
2
3
4
5
6
7
8
9
10
<script setup>
import { storeToRefs } from 'pinia';
const store = useCounterStore();
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store);
// 作为 action 的 increment 可以直接解构
const { increment } = store;
</script>

State

state 被定义为一个返回初始状态的函数,代表了应用中的状态。

默认情况下,可以通过 store 实例访问 state,并进行读写。

1
2
const store = useStore();
store.count++;

使用选项式 API 时,可以通过调用 $reset 方法将 state 重置。

1
2
const store = useStore();
store.$reset();

可以通过调用 $patch 方法同时修改多个属性:

1
2
3
4
5
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO'
});

$patch 方法也接受一个函数来对集合进行修改:

1
2
3
4
store.$patch(state => {
state.items.push({ name: 'shoes', quantity: 1 });
state.hasChanged = true;
});

你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以通过 $patch 方法替换它:

1
2
3
4
// 这实际上并没有替换`$state`
store.$state = { count: 24 };
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 });

你可以通过 store 的 $subscribe 方法侦听 state 及其变化,相当于组件中的 watch(), 比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次 (例如,当使用上面的函数版本时)。

Getter

Getter 完全等同于 store 的 state 的计算值(computed value)。推荐使用箭头函数,并且它将接收 state 作为第一个参数。在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例,从而访问其他 getter。

你可以访问其他 store 的 getter:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useOtherStore } from './other-store';

export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore();
return state.localData + otherStore.data;
}
}
});

Action

Action 相当于组件中的 method,它们定义了业务逻辑。action 也可通过 this 访问整个 store 实例。action 也可以是异步的,可以像函数或方法一样被调用,也可以在另一个 store 的 action 中被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useAuthStore } from './auth-store';

export const useSettingsStore = defineStore('settings', {
state: () => ({
preferences: null
// ...
}),
actions: {
async fetchUserPreferences() {
const auth = useAuthStore();
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences();
} else {
throw new Error('User must be authenticated');
}
}
}
});

你可以通过 store.$onAction() 来监听 action 和他们的结果。传递给它的回调函数会在 action 本身之前执行。after 允许在 promise 解决之后执行回调函数。onError 允许在 action 抛出错误或 reject 时执行回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const unsubscribe = someStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError // action 抛出或拒绝的钩子
}) => {
// 为这个特定的 action 调用提供一个共享变量
const startTime = Date.now();
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${name}" with params [${args.join(', ')}].`);

// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after(result => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`);
});

// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError(error => {
console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`);
});
}
);

// 手动删除监听器
unsubscribe();

Pinia 插件

Pinia 支持通过 pinia.use() 添加插件,Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createPinia } from 'pinia';

// 创建的每个 store 中都会添加一个名为 `secret` 的属性。
// 在安装此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin(context) {
// context.pinia // 用 `createPinia()` 创建的 pinia。
// context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
// context.store // 该插件想扩展的 store
// context.options // 定义传给 `defineStore()` 的 store 的可选对象。
return { secret: 'the cake is a lie' };
}

const pinia = createPinia();
// 将该插件交给 Pinia
pinia.use(SecretPiniaPlugin);

// 在另一个文件中
const store = useStore();
store.secret; // 'the cake is a lie'

以下为插件可扩展的内容:

  • 为 store 添加新的属性
  • 定义 store 时增加新的选项
  • 为 store 增加新的方法
  • 包装现有的方法
  • 改变甚至取消 action
  • 实现副作用,如本地存储
  • 仅应用插件于特定 store

当添加外部属性、第三方库的类实例或非响应式的简单值时,你应该先用 markRaw() 来包装一下它,再将它传给 pinia。下面是一个在每个 store 中添加路由器的例子:

1
2
3
4
5
6
7
import { markRaw } from 'vue';
// 根据你的路由器的位置来调整
import { router } from './router';

pinia.use(({ store }) => {
store.router = markRaw(router);
});

Pinia 与 Vuex 区别

Pinia 是 Vue3 推荐的状态管理库,而 Vuex 将不再更迭,从事件顺序上看,Pinia 可以看做是 Vuex5 。

  1. Pinia 提供了更简单的 API,也提供了符合组合式 API 风格的 API,搭配 Typescript 一起使用时有可靠的类型推断支持。
  2. Pinia API 已经稳定,新功能需要经过 RFC 流程。Vuex 不再更新。
  3. Vuex3.x 只适配 Vue2,Vuex 4.x 适配 Vue3, Pinia 适配 Vue3 和 Vue2。
  4. Pinia 没有 mutation,Vuex 中使用 mutation 记录数据的更新。
    Pinia 在 action 执行完成后会自动发布给订阅者,所以不需要 mutation。
  5. Pinia 无需要创建自定义的复杂包装器来支持 TypeScript。
  6. Pinia 无过多的魔法字符串注入,只需要导入函数并调用它们。
  7. Pinia 无需动态添加 Store,默认即是动态的。
  8. Pinia 不再有嵌套结构的模块,提供扁平的 Store 结构。
  9. Pinia 不再有可命名的模块。

Vue面试手册

Vue 是什么

Vue是一个构建用户界面的渐进式 MVVM 框架。它的核心在于数据驱动和组件化的思想。

Vue的优点主要有:

  • 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;
  • 简单易学:中文文档丰富,易于理解和学习;
  • 双向数据绑定:保留了 Angular 的特点,在数据操作方面更为方便;
  • 组件化:保留了 React 的优点,通过单页面组件实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
  • 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
  • 虚拟DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作;
  • 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。

为什么说Vue是一个渐进式框架

Vue 实现了构建 web 应用最核心的 mvvm 绑定部分,用户可以选择使用其他库(例如vuex、axios、vue-router)来搭配使用构建最适合自己的应用。

v-model 双向绑定的原理是什么

v-model 实际上是语法糖,以 input 元素为例:

1
2
3
<input v-model="inputV" />
// 等同于
<input :value="inputV" @input="inputV = $event.target.value"/>
  • input 组件的 value 属性绑定于 inputV 变量上,也就是当 inputV 变化时,input 组件的 value 也会跟着变化;
  • 监听 input 事件,该事件在 input 的值改变时触发,事件触发时给 inputV 重新赋值,所赋的值是 $event.target.value,也就是当前触发 input 事件时的 value 值,也就是该 input 组件的值。

Vue 2.0 响应式数据的原理

Vue.js 是采用 数据劫持 结合 发布者-订阅者模式 的方式,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

  • 对需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter,这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化;
  • Compile 负责解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;
  • Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
    • 在自身实例化时往属性订阅器(dep)里面添加自己
    • 自身必须有一个 update() 方法
    • 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调
  • MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到 数据变化 -> 视图更新;视图交互变化(input) -> 数据(model)变更的双向绑定效果。

vue2_inner

Vue3 中为什么使用 Proxy 来替代 Object.defineProperty() 实现数据劫持?

Object.defineProperty 无法监听 下标方式修改数组数据 或者 给对象新增属性 的操作,Proxy 可以监听。

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。

Vue2 监控不了数组类型 data 的变化,有什么解决办法

  • 使用 this.$set(obj,key,value) 更新数组
  • 调用以下几个数组的方法 splice()、 push()、pop()、shift()、unshift()、sort()、reverse()

Vue2 中何时使用 Vue.$nextTick()?

  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的 DOM 结构的时候,这个操作就需要方法在 nextTick() 的回调函数中。
  • 在 vue 生命周期中,如果在 created() 钩子进行DOM操作,也一定要放在 nextTick() 的回调函数中。因为在 created() 钩子函数中,页面的DOM还未渲染,这时候没办法操作DOM。

什么是 Vue.mixin

混入(mixin)是指当多个组件共享同样的功能(生命周期hook、方法等)时,将同样的功能剥离开来并在多个组件中引入该部分实现从而代码重用的目的。

vue-router有多少种模式?

  • hash模式:兼容所有浏览器,包括不支持 HTML5 History Api 的浏览器。例如 https://example.com/#index hash 的值为 #index, hash 的改变会触发 hashchange 事件,我们可以通过监听 hashchange 事件来完成操作实现前端路由。

  • history模式:能支持 HTML5 History Api 的浏览器,依赖 HTML5 History API 来实现前端路由。例如 https://example.com/index 。为防止服务器请求不到资源时返回 404,需要在服务器端设置,匹配不到资源时返回 index.html ,同时由前端来控制 404 页面的显示。例如,nginx 需配置

1
2
3
4
5
6
location / {
try_files $uri $uri/ @router index index.html;
}
location @router {
rewrite ^.*$ /index.html last;
}
  • abstract模式:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。

hash和history模式实现vue-router跳转api的区别

api hash history
push window.location.assign window.history.pushState
replace window.location.replace window.history.replaceState`
go window.history.go window.history.go
back window.history.go(-1) window.history.go(-1)
forward window.history.go(1) window.history.go(1)

vue 指令有哪些?

v-show、v-if、v-else-if、v-else、v-for、v-on、v-bind、v-model、v-once、v-slot、v-html、v-text

对比Vue和React

Vue3 和 React16.8 引入了较大的变更,所以 Vue 和 React 的对比一般指 Vue2 和 React16.7 的对比。

相同点

  • 使用 Virtual DOM
  • 提供了响应式(Reactive)和组件化(Composable)的视图组件
  • 核心库与路由(react-router、vue-router)和状态管理(redux、vuex)分离
  • 支持 JSX,移动端都支持原生渲染
  • 提供了命令行工具(create-react-app、vue-cli)
  • 提供了跨端解决方案(React Native、weex)

不同点

  • 预编译
    • React 可以通过 Prepack 优化 JavaScript 源代码,在编译时执行原本在运行时的计算过程,通过简单的赋值序列提高 JavaScript 代码的执行效率,消除中间计算过程及分配对象操作。缓存 JavaScript 解析结果,优化效果最佳
    • Vue 可以静态分析 template,构造 AST 树,通过 PatchFlags 标记节点变化类型
  • 渲染
    • React
      • 通过 shouldComponentUpdate / setState,使用 PureCompoent 等对比前后状态和属性,手动决定是否渲染来优化
      • 推荐 jsx 语法,可扩展性好,可以渐进式应用
      • CSS in JS
    • Vue
      • 推荐 template 语法,自动追踪组件依赖,精确渲染状态改变的组件
      • 支持并且默认单文件组件,样式仍旧是 CSS 语法,迁移方便
  • 事件处理
    • React
      • 事件委托到 document,之后委托到 根节点
      • 所有事件被合并为合成事件并兼容不同浏览器
      • 事件处理函数中的 this 需要手动绑定或使用箭头函数声明
    • Vue
      • 支持原生事件
      • this 自动绑定执行上下文

前端技术选型

本文主要面向前端基础框架和库的选型,不包括构建工具、 UI 库和框架配套解决方案的选择。由于前端发展速度很快,框架层出不穷,特标明本文发布时间为 2021年6月6日,更新时间 2022年2月21日,更新时间 2022年3月14日,只适用于2021年前端基础框架技术选型。只选择了 web 应用开发时使用的框架,桌面端框架如 Electron、Tauri、Flutter,或者跨平台框架例如 React Native、Weex 或者众多的小程序框架本质上和如下列出的框架不是解决同一类问题所以不在对比之列。

阅读更多

Vue最佳实践

使用 vue 周边

vue-devtools

浏览器插件,让你能够实时编辑数据 property 并立即看到其反映出来的变化。另一个主要的好处是能够为 Vuex 提供时间旅行式的调试体验。

vue-performance-devtool

一个用于检查vue组件性能的浏览器插件。

vue-cli

vue-cli 是 vue 开发的标准工具,提供了交互式的项目脚手架,集成了 webpack 和丰富的官方插件集合。

vue-router

vue 官方路由管理器,支持嵌套的路由/视图表以及模块化的、基于组件的路由配置。

vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

axios

Axios是一款 promise 化的HTTP客户端,支持在浏览器和 Nodejs 环境中使用。

element-ui

elementUI 是一款基于 Vue 2.0 桌面端中后台组件库。

iview

iview 是一套基于 Vue 的高质量 UI 组件库,主要服务于 PC 界面的中后台产品。

Ant Design Vue

Ant Design Vue 是 Ant Design 的 Vue 实现,开发和服务于企业级后台产品。

阅读更多

Vue-keep-alive实现原理

一、前言

本文介绍的内容包括:

  • keep-alive用法:动态组件&vue-router
  • keep-alive源码解析
  • keep-alive组件及其包裹组件的钩子
  • keep-alive组件及其包裹组件的渲染

阅读更多

Vue3新功能和作用

2020 年 9 月 18 日 Vue3.0 正式发布。

Composition API

新增 setup 组件选项,是一个接受 props 和 context 的函数,使用 toRefs 对 props 进行响应式解构,可以通过 context 获取组件的 attrs slots emit 属性。返回一个可以在组件模板中使用其属性的对象,或者返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态。

通过一个新的 ref/reactive 函数使任何响应式变量在任何地方起作用。

在 setup 中注册生命周期钩子,因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们,在vue3中只有如下钩子:onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmountedonErrorCapturedonRenderTrackedonRenderTriggered

从 Vue 导入的 watch 函数执行相同的操作

从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性

从 Vue 导入的 provide/inject 函数进行深度传值

阅读更多

读Vue源码所能想到的面试问题

指令v-if v-show有什么不同?

v-if 和 v-show都是Vue中的指令而且都可以用来控制模板的渲染。

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS( display:none) 进行切换。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

阅读更多

ionic4+vue搭建跨平台移动应用框架

ionic是一款混合移动应用开发框架,可以用来构建跨平台(Android & iOS)的移动应用程序,而且还可以基于Sass和AngularJS进行构建,是目前跨平台移动应用开发的一个翘楚。这个框架的目的是从web的角度开发手机应用,基于PhoneGap的编译平台,可以实现编译成各个平台的应用程序。
Vue.js是一个MVVM驱动的 web 界面的渐进式框架,学习简单并且十分灵活。ionic3之前一般默认和Angular“绑定”在了一起,从ionic4开始,Ionic Team宣布将会逐渐采用Stencil来实现标准Web Components,使用ionic将不再必须使用Angular,可以使用React,Vue,Jquery或者什么框架都不使用。

阅读更多