组件通讯

Vuex|Redux|Pinia

Road In City During Sunset
Road In City During Sunset

就地取材

全局事件总线 EventBus

将 Vue 实例赋值给 Vue 显式原型上增加的 $EventBus 属性,后续所有的通信数据和事件监听都存储在这个属性上。在范围较小时可以直接定义变量作为事件中心。

Vue.prototype.$eventBus = new Vue() // main.js
this.$eventBus.$emit('eventTarget','eventTargetVal') // 传值组件
this.$eventBus.$on('eventTarget', (val) => { ...val })// 接收组件
this.$eventBus.$off('eventTarget') // 销毁事件

选项和实例 Property

  • inheritAttrs => 抹去直接绑定子组件根元素上未作为 props 的 attribute
export default {
  inheritAttrs: false, ...
}
  • vm.$attrs => 父作用域中不被 prop 获取的 attribute 绑定

  • vm.$listeners => 包含父作用域中不含 .native 修饰器的 v-on 事件监听器

vm.$attrs 与 vm.$listeners 可分别通过 v-bind="$attrs" 和 v-on="$listeners" 传入内部组件。内部组件可通过 this.$emit('XXX') 直接触发传入的事件。

<template>
  <div>
    SupComponent
    <AttrsListenerTest ok="ok" okk="okk" okkk="okkk" @supHandler01="supHandler01" v-on:supHandler02="supHandler02"></AttrsListenerTest>
  </div>
</template>
<script>
import AttrsListenerTest from '@/components/AttrsListenerTest.vue'
export default {
  components: { AttrsListenerTest },
  methods: {
    supHandler01 () { console.log('super func 01') },
    supHandler02 () { console.log('super func 02') }
  }
}
</script>
<template>
  <div>
    AttrsListenerTest
    <SubAttrsListenerTest v-bind="$attrs" v-on="$listeners"/>
  </div>
</template>
<script>
import SubAttrsListenerTest from '@/components/SubAttrsListenerTest.vue'
export default {
  inheritAttrs: false, // 抹去绑定在根元素上的 $attrs 属性
  name: 'AttrsListenerTest',
  props: { ok: { type: String } },
  mounted () {
    console.log(this.$attrs) // {okk: 'okk', okkk: 'okkk'}
    console.log(this.$listeners) // {supHandler01: ƒ, supHandler02: ƒ}
  },
  components: { SubAttrsListenerTest }
}
</script>
<template>
  <div>SubAttrsListenerTest</div>
</template>
<script>
export default {
  name: 'SubAttrsListenerTest',
  props: { okk: { type: String } },
  mounted () {
    console.log(this.$attrs) // {okkk: 'okkk'}
    console.log(this.$listeners) // {supHandler01: ƒ, supHandler02: ƒ}
  }
}
</script>

选项和实例

  • vm.$parent => 指定已创建的实例的父实例

子组件在挂载完成后可通过 this.$parent 拿到其父组件实例,和父组件实例上的属性和方法。

父组件在挂载完成后可通过 this.$children 拿到一级子组件的属性和方法,那么就可以直接改变 data 或调用 methods 方法。

  • vm.$refs => 含注册过 ref attribute 的所有 DOM 元素组件实例的对象
  • vm.$root => 当前组件树的根 Vue 实例

所有组件最终都会挂载到根实例上,可通过根实例的 $children 获取子组件。

this.$root // 根实例
this.$root.$children[0] // 根实例的一级子组件
this.$root.$children[0].$children[0] // 根实例的二级子组件

pubsub-js

同样适用于任意组件间的通讯方式,消息订阅与发布。由于铺天盖地的相关库,这里主要说 PubSubJS

# 安装 pubsub
npm i pubsub-js

Vuex

快速上手

  • 安装与使用

VueCLI 勾选 Vuex 选项后,创建项目时会在 src 目录下生成 store 文件夹,其中包含 index.ts 文件。main.js|ts 会引入 store 进行全局注测,方便通过 this.$store 访问。项目阶段需要引入也可使用 npm install vuex@next --save 安装。

// src/store/index.js => vue2
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({ state: {}, mutations: {}, actions: {}, modules: {} })

// src/store/index.ts => vue3
import { createStore } from 'vuex'
export default createStore({ state: {}, mutations: {}, actions: {}, modules: {} })
// vue2 => main.js
import store from './store'
new Vue({
  el: '#app', router,
  store, // 使用 store
  template: '<App/>', components: { App }
})
// vue3 => main.ts
import store from './store'
...
createApp(App).use(store).use(router).mount('#app')
  • State => 唯一数据源 -> SSOT

Vuex 使用单一状态树,默认应用仅包含一个 Store 实例,即用一个对象包含全部的应用层级状态。需要注意的是,存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则,即状态对象必须是纯粹 plain 的(普通对象)。

组件访问 State 中的数据可通过 this.$store.state.全局数据名称 或 mapState 辅助函数。后者是在 Vue 组件读取 Vuex 数据状态时,避免重复和冗余的声明计算属性,能接收对象或数组作为参数。

  • Getter => store 中的 getters => 派生 state 却不修改原 state

对于 store 的 state 来说,是多个组件需要的数据才会放入。那么同样的 state 派生状态也适合进行抽取封装。store 中定义 getters 可以认为是 store 的计算属性。

const store = createStore({
  state: {
    administrative_staff: [
      { id: 1, name: '...', del_flag: true }, { id: 2, name: '...', del_flag: false }
    ]
  },
  getters: {
    delAccountGet (state) {
      return state.administrative_staff.filter(p => p.done)
    }
  }
})

getters 会默认暴露为 store.getters 对象,可通过属性形式访问;且 getters 允许互相使用,getters 可以作为具体 getters 方法,即 getter 的第二个参数传入。

getters: {
  // ...
  delAccountCount (state, getters) { return getters.delAccountGet.length }
}

interface StoreOptions<S> 定义 getter 只接受两个参数,若需给 getters 传参,那么应该让 getter 返回一个函数来实现。但是这种形式不会进行结果的缓存。

getters: {
  // ...
  getASInfoById: (state) => (id) => { return state.administrative_staff.find(p => p.id === id) }
}
store.getters.getASInfoById(2) // -> { id: 2, name: '...', del_flag: false }

同样地,为简化将操作属性映射为组件中的方法,可通过传入对象(方法名不同于属性映射)或数组的 mapGetters() 辅助函数进行操作。

import { mapGetters } from 'vuex'
export default {
  ...,
  computed: {
    ...mapGetters([ // 使用对象展开运算符将 getter 混入 computed 对象中
      'delAccountCount', 'anotherGetter',
    ])
  }
}
  • Mutation => 必须是同步函数

变更 store 数据时不推荐直接对 this.$store.state.xx 赋值或运算,而是要唯一的通过提交 mutation 来改变 store 的状态。每个 mutation 都有一个字符串的事件类型 type,以及一个回调函数 handler。这个回调函数是实际对状态进行更改的位置,且接受 state 作为第一个参数。在 store 中定义的 mutation 更像是事件的注册,只能通过 store.commit('mutationName') 触发,mutationName 对应 type。

// store/xxx.js|ts 文件
const store = new Vuex.Store({
  state: { 数据名:数据值, ... },
  mutations: {
    func01(state, args) {...}, func02(state, args) {...}, func03(state, args) {...},
  }
})
// .vue 组件触发 mutation
import { mapMutations } from 'vuex'
methods: {
  eventfunc() { this.$store.commit('func01') },
  ...mapMutations(['func02','func03'])
}

可以向 store.commit 传递参数,即向 mutation 提交载荷 Payload。载荷更多时候是传入一个包含多个字段的对象。

// ...
mutations: {
  increment (state, payload) { state.count += payload.amount }
}
store.commit('increment', { amount: 10 })

对象风格的提交方式 => store.commit 的参数可以逐个指定,也可以通过对象的形式指定。后者包含对应 mutation 的 type 字段以及其他载荷字段。

mutations: {
  increment (state, payload) { state.count += payload.amount }
}
store.commit({ type: 'increment', amount: 10 })
  • Action => 提交 mutation 而不是直接变更状态 => 可异步

Mutation 中不能写异步代码,Action 可以处理异步任务。开发中通常以 Action 间接地触发 Mutation 达到变更数据的目的。

函数 Action 接受一个与 store 实例具有相同方法和属性的 context 对象,因此可以调用 context.commit 提交 mutation,或通过 context.state 和 context.getters 来获取 state 和 getters。开发中可用参数解构来简化多次调用 commit 的情况。

/* ActionContext 源码 */
export interface ActionContext<S, R> {
  dispatch: Dispatch;
  commit: Commit;
  state: S;
  getters: any;
  rootState: R;
  rootGetters: any;
}
// store/xxx.js|ts
const store = new Vuex Store({
  ...
  mutations: { func01(state) { ... }, func02(state, args) { ... }, ... },
  actions: {
    func01Sync({ commit }) { // ES2015 参数解构
      commit('func01')
    },
	func02Async(context, payload) {
	  setTimeout(() => { context.commit('func02', payload.args01) }, 1000)
	},
    incrementAsync(context, acc) { ... },...
  }
})

Action 通过 store.dispatch 方法触发。若在组件中涉及到异步操作,那么还是得分派 Action 再提交通知给必须同步执行的 Mutation 改变数据。此外 Actions 支持同样的载荷方式和对象方式进行分发。

// 组件分发 Actions
import { mapActions } from 'vuex'
methods: {
  ...mapActions(['func01Sync', '...']), // 映射导入后可以直接绑定到事件
  eventfunc() { this.$store.dispatch('func02ASync', {args01:'zs'}) }, // 载荷方式分发
  patchEventByObject() { this.$store.dispatch({ type: 'incrementAsync', amount: 10 }) // 对象方式分发
  }
}
// Action 进行异步操作
const module_user = {
  namespaced: true,
  state: () => ({...}), mutations: {...}, getters: {...},
  actions: {
    ...
    addUserByServer (context) {
      axios.get("").then(response => { // 默认引入 axios 与 nanoid
        context.commit(ADD_USER, {id:nanoid(),username:response.data})
      },error => {...})
    }
  }
}
  • module

虽然单一状态树会使应用的所有状态集中,但是当应用变得复杂时,代码就会变得臃肿。为此 Vuex 允许将 store 分割成模块 module。每个模块 module 拥有各自的 state、mutation、action、getter、甚至是嵌套子模块。

const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } }
const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } }
// Vuex3
const store = new Vuex.Store({
  modules: { a: moduleA, b: moduleB }
})
// Vuex4
// const store = createStore({ modules: { a: moduleA, b: moduleB } })
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

因 store 被分割成模块 module,那么每个模块的 state 都是局部状态。模块内部的 mutation 和 getter 接收的首个参数是模块的局部状态对象,后者所需的 rootState 根节点状态会作为第三个参数暴露出来。同样对于模块内部的 action,局部状态以及根节点状态都是通过 context.??? 暴露。

/* 源码分析 */
export type Mutation<S> = (state: S, payload?: any) => any;
export type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any;
export interface ActionContext<S, R> { dispatch: Dispatch; commit: Commit; state: S; getters: any; rootState: R; rootGetters: any; }
  • 命名空间 => namespaced: true

模块内部的 action、mutation、getter 默认注册在全局命名空间,这种方式可以让多个模块对其进行操作,但是如果在不同的、无命名空间的模块中定义两个相同的 action、mutation 或 getter,那么会出现覆盖的情况,主要表现是后定义的覆盖前定义的。

在模块对象中设置字段 namespaced: true,会使其成为带命名空间的模块。这类模块注册后,getter、action 及 mutation 都会自动据模块注册路径调整命名。

开启命名空间后组件读取 State 数据 开启命名空间后组件读取 Getter 数据 开启命名空间后组件调用 Dispatch 开启命名空间后组件调用 Commit
直接读取 this.$store.state.moduleA.data01 this.$store.getters['moduleA/getter01'] this.$store.dispatch('moduleA/action01',args) this.$store.commit('moduleA/mutation01',args)
借助 mapXxx 读取 ...mapState('moduleA', ['data01',...]) ...mapGetters('moduleA',['getter01',...]) ...mapActions('moduleA',{methodName01:'action01',...}) ...mapMutations('moduleA',{method01:'mutation01',...})

简记:无效忠、忠效忠、笑笑打

默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。点此

模块内未指明 namespaced: true 时,除 state 外,getters & mutation & action 默认注册在全局。

// 使用 A 模块内的 getters 数据 -> 未指明命名空间为真
$store.getters.getter01 => yes
$store.getters.moduleA.getter01 => no

小结

  • 和 getters 配置项功能类似的 computed 属性虽然可以完成运算功能,但却仅能在当前组件中生效,不能复用

  • 开发中最好使用简单的模板语法而不要过长的插值语法,所以说从 vuex 引入的 mapXxx 的意义是帮助简化取值的过程

Redux

Concepts & API

  • Flux 是强制单向数据流的架构

该模式控制派生的数据,通过中央存储来支持组件间的通信。整个应用中的数据更新在此完成。

Redux 是由 Flux 衍生的架构模式,用于集中管理多个组件所共享的状态。

Redux 中只能定义一个可以更新状态的 store,Flux 中可以定义多个 store。

  • Redux 和 React-redux 并非同一种东西

React-redux 就是把 Redux 架构模式和 Reactjs 结合起来的官方库,即 Redux 架构在 React 中的体现。

  • action => 具有 type 和 data 属性的一般对象

Actions are plain JavaScript objects that have a type field, which normally put any extra data needed to describe what's happening into the action.payload field. This could be a number, a string, or an object with multiple fields inside.

唯一改变状态树的方法是创建 Actions,一个描述发生了什么的对象,并将其 dispatch 给 store。

  • store => getState、dispatch、subscribe API

Reducers are functions that take the current state and an action as arguments, and return a new state result. In other words, (state, action) => newState.

在纯函数 reducer 中应该创建包含更新后字段的新 state 对象,而不是在其中直接修改 state 的值。

首次 reducer 调用是由 store 自动触发的,传递的先前状态是 undefined,且 action.type 通常是 @@INIT@@redux/INIT。随后的 reducer 执行会由 store.dispatch(action) 方法来触发。

返回更改的状态默认不会引起页面的更新。可在对应组件的 componentDidMount 中设置监听函数。

export default class Xxx extends Component {
  ...
  conponentDidMount(){
    store.subscribe(() => this.setState({})) // 触发页面更新
  }
}

subscribe 是一个用于注册回调函数的方法,可以监听 Store 中的状态变化,并在状态发生变化时触发回调函数。此外,subscribe 方法也会返回一个函数,可以用来取消对状态变化的监听。

// 优化触发更新 => 应用的 index.js 文件中包裹应用的渲染
store.subscribe(() => {ReactDOM.render(<App/>, document.getElementById("root"))})

Quick Start

createStore 函数用于生成整个应用唯一保存数据的容器 Store。

/* 
  store.js 用于暴露 store 对象,整个应用只有一个 store 对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import { createStore, applyMiddleware } from "redux";
//引入为Count组件服务的reducer
import countReducer from "./count_reducer";
//引入redux-thunk,用于支持异步action
import thunk from "redux-thunk";
//暴露store
export default createStore(countReducer, applyMiddleware(thunk));

store.getState() 对 Store 生成快照,可以得到某个节点的数据集合 State。

状态变化时,应在组件中发出通知。接受 action 作为参数的 store.dispatch() 是 View 发出 action 的唯一方法。

// Xx组件.js
store.dispatch(xxxAction(value))
// Xx组件_action.js
export const xxxOperateAction = (data) => ({ type: xxxOperate, data });

Store 接受 Action 后生成新 State 的计算过程叫做 Reducer。

// Xx组件_reducer.js
const initState = 0; // 初始化状态
export default function xxReducer(preState = initState, action) {
  // redux会在开始时调用
  // 没有传递preState或者默认值为undefined则修改为0
  // console.log(preState, action); // {type: '@@redux/INIT1.6.3.4.l.f'}
  if (preState === undefined) preState = 0;
  const { type, data } = action;
  switch (type) {
    case "operate1":
      return preState ??? data;
    case "operate2":
      return preState ??? data;
    default:
      // 初始化 Store -> reducer 唤醒初始化
      return preState;
  }
}
  • reducer 纯函数无需手动调用,会被 store.dispatch() 自动触发执行

生成 store 时将 reducer 作为参数传入 createStore(xxxReducer) 方法。

纯函数 reducer 在相同输入的情况下,必定得到同样的输出。此函数中不能调用系统 IO 和 Date.now()Math.random() 等方法。

大型应用的 state 和 reducer 体量庞大,Redux 提供 combineReducers 方法,用于将 reducer 拆分。定义的各个子 reducer 可以用这个方法合成为总 reducer 函数。

import { combineReducers } from 'redux';
...
const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})

// 等同于
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}
  • 异步 action => 除开对象类型,action 的值也可以是函数

异步操作不在组件中指明,而是在 Action Creators。异步 action 中一般会调用同步 action,前者直接被 Store 执行,后者传递给 Reducers 执行。

// 组件
handleAsync = () => {
  const {value} = this.handleNumber;
  store.dispatch(createHandleAsyncAction(value*1, 500))
}
// xxx_action.js
export const createHandleAsyncAction = (data, time) => {
  return (dispatch) => { // 异步 action 即返回的不再是对象而是函数
    setTimeout(()=>{
      dispatch(createHandleSyncAction(data))
    }, time)
  }
}

"Error: Actions must be plain objects.Use custom middleware for async actions."

默认传递给 Store 的 action 应该是一个一般对象,否则识别不了。

此时需引入 redux-thunk 中间件,以让 Store 执行函数,不再交给 Reducers。

多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

import {createStore, applyMiddleware} from 'redux'

react-redux

使用 facebook 官方的插件 react-redux 可以在 React 中更加方便的使用 redux。

  • 连接容器组件和 UI 组件 => react-redux 中的 connect 函数

UI 组件负责页面的呈现和事件的绑定,通常存放于 components 目录。

容器组件负责传递状态以及状态操作的方法,通常存放于 containers 目录。

容器组件并非直接写成一般组件的形式,而是需要借助 connect 函数产生。

/* containers/Xxx */
// 引入 UI 组件
import XxxUI from "../../component/Xxx"
// 引入 connect 连接 UI 组件和 redux
import {connect} from "react-redux"
// 使用 connect 创建并暴露容器组件
export default connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(XxxUI)

连接函数的返回值中传入需要包裹的组件执行时,若没有在引入容器组件的位置给容器组件标签传入 store,则会出现 "Error: Could not find 'store' in the context of 'Connect(Xxx)'" 的报错。

ReactDOM.render(
  <ContainerComponent store={store}>
    <App />
  </ContainerComponent>,
  document.getElementById("root")
);
  • react-redux 中的 connect 作为高阶组件可以传入四个参数
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

容器组件与 UI 组件无法直接通过 props 的形式进行数据的注入(非标签嵌套)。

mapStateToProps => Function 类型的参数,本质是将状态带入视图组件。该函数返回对象中的键值对将分别作为传递给 UI 组件 props 的 key 与 value。

mapStateToProps?: (state, ownProps?) => Object

mapDispatchToProps => Object 或 Function 类型的参数,本质是将操作状态的方法带入 UI 组件。该函数返回对象中的键值对作用同上。

mapDispatchToProps?: Object | (dispatch, ownProps?) => Object

mapDispatchToProps 指定为对象时,其每个字段都是一个 Action Creators。

React-Redux binds the dispatch of your store to each of the action creators using bindActionCreators. The result will be regarded as dispatchProps, which will be either directly merged to connected components, or supplied to mergeProps as the second argument. here.

bindActionCreators(mapDispatchToProps, dispatch)

mapStateToProps 和 mapDispatchToProps 的返回值在内部分别称为 stateProps 和 dispatchProps。

mergeProps => Function 类型的参数,将定义的 stateProps、dispatchProps 和组件自身的 props 传入回调函数,返回的对象将作为 props 传递到被包装的组件。

该属性用于根据组件的 props 对 state 进行筛选,或将 props 中特定数据与 Action Creator 捆绑。

省略该属性时,connect 默认返回 Object.assign({}, stateProps, dispatchProps)。

mergeProps?: (stateProps, dispatchProps, ownProps) => Object

React Redux 相较于 Redux,容器组件默认拥有监测 redux 状态改变的能力,无需在 index 中使用 store.subscribe。

Provider 会自动将 store 注入应用中的容器组件

store 以标签属性的方式添加到容器组件,会存在代码冗余的问题 => 可通过 react-redux 提供的 <Provider /> 组件进行批量注入。

// 优化前
// App.jsx
import React, { Component } from "react";
import {store} from "./redux/store"
import Xxx from "./containers/Xxx/index.jsx";

export default class App extends Component {
  render() {
    return (
      <div>
        <Xxx state={state}/>
        ...
      </div>
    );
  }
}
// 优化后
// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./redux/store";
import { Provider } from "react-redux";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

整合 UI 组件与容器组件 => 放入一个 jsx

combineReducers 传入的对象就是 Redux 保存的总状态对象。

reducer 中的 preState 会进行浅比较,所以用 push 之流方式不可进行更新页面。此外对数组的改变使用 [newData, ...oldData] 不会破坏 reducer 纯函数的原则。

纯函数:相同输入必定得到相同的输出。必须遵守以下约束:不的改写参数数据;不会产生副作用;不可调用 random now 之流的不纯方法。

有些数据是需要通过异步操作来获取的,但是 Redux 中 dispatch 函数是同步的,也就是说该函数不能直接处理异步操作。此时可以考虑使用 react-redux 中间件,这样就可以在 actionCreator 中编写异步代码,并且返回一个函数而不是一个 action 对象。

The actual implementation of the thunk middleware is very short - only about 10 lines. If you pass a function into dispatch, the thunk middleware sees that it's a function instead of an action object, intercepts it, and calls that function with (dispatch, getState) as its arguments. If it's a normal action object (or anything else), it's forwarded to the next middleware in the chain

redux devtools 需配合项目安装 redux-devtools-extension 库使用

@redux-devtools/extension

import {composeWithDevTools} from 'redux-devtools-extension'
export default createStore(rootReducer, composeWithDevTools(applyMiddleWare(thunk)))

Pinia

Vuex 开发团队的新作品 Pinia,对 Vue2|3 都有很好的支持,也是强推的状态管理库。注意 Pinia 也支持在 Vue-devtools 中进行调试,但需 Vue Devtools v6 版本。

Pinia v2 no longer hijacks Vue Devtools v5, it requires Vue Devtools v6. Find the download link on the Vue Devtools documentation for the beta channel of the extension.

快速开始

  • Advantages

较 Vuex 相比,Pinia 抛弃了 Mutations 操作,保留 State、Getters 和 Actions;不需要嵌套模块,符合 Composition API 和代码扁平化;完整支持 Typescript;可实现代码自动分割。

  • Vite & Pinia & TS
# 搭建项目
npm create vite@latest # vue + ts
# 安装 pinia
npm install pinia
import { createPinia } from 'pinia'
...
app.use(createPinia())
  • API Documentation / pinia / _StoreWithState => Methods

Store 仓库

确保在项目 /src/main.ts 里引入 pinia 后,直接在 /src 目录下新建 store 目录作为状态管理库。

Store 是使用 defineStore() 定义的,需要一个唯一名称作为第一个参数传递,第二个参数是包含 state、getters 和 actions 的对象。

// ?.ts
import { defineStore } from 'pinia'
// useStore => useUser、useCart
export const useStore = defineStore('main',{ // 容器起名 => main
  state:()=>{ return {} }, // SPA 的全局状态
  getters:{}, // 监视或计算状态的变化 => 有缓存的功能
  actions:{} // 操作 state 里数据的业务逻辑
})

store 是在组件的 setup() 中通过调用 xxStore() 进行的实例化,这个对象可以直接访问 state、getters 和 actions 中定义的任何属性与方法。

// ?.vue
import { useStore } from '@/stores/counter' // 接收暴露的 xxxStore
export default {
  setup() {
    const store = useStore()
    ...
    return {
      store, // 可返回整个 store 实例以在模板中使用
    }
  },
}

store 是用 reactive 包裹的对象,故不需要在 getter 之后写 .value;其次是和 setup 中的 props 一样,直接解构失去响应式。

若需在提取属性的同时保持响应式 => 通过 storeToRefs() 传入 store 对象再进行解构。

const store = useStore();
// const {helloPinia,count} = store; // 解构使用失去响应式 -> 放入 reactive({}) 也不行
const {[state01],[state02],[getters01]} = storeToRefs(store);

State 状态

state 被定义为返回初始状态的函数,可通过 store 实例直接读取和写入状态,也可调用 store 上的 $reset() 方法将状态重置到其初始值。

import { defineStore } from 'pinia'
const xxxStore = defineStore('storeId', {
  state: () => { // 推荐使用完整类型推断的箭头函数
    return { // 所有这些属性都将自动推断其类型
      [attribute01]: 0, [attribute02]: 'zs', [attribute03]: true,
    }
  },
})
const store = xxxStore()
store.[attribute01]++
store.$reset()

改变状态除了直接用 store[attribute] 修改,还可以调用 $patch() 方法传入对象或函数。前者适合修改单条数据,后者适合修改多条数据。建议传入函数,因为在使用对象作参数时修改集合(从数组中推送、删除、拼接元素)都需要创建一个新集合。若是业务逻辑导致状态的改变,也可以使用 actions。

// 传入对象 改变 state
store.$patch({ arr: [..., itemNew], ... })
// 传入函数 改变 state
xxxstore.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 });
  state.helloWorld = state.helloWorld === "HelloPinia" ? "HelloWorld" : "HelloPinia";
});
// actions 改变 state
actions: {
  changeState() {
    this.count++,
    (this.helloPinia = this.helloPinia === "helloPinia" ? "Hello World" : "helloPinia");
  }, ...
},

注意在使用 actions 时,不能用箭头函数,因为箭头函数绑定是外部的 this。

若有替换 store 整个状态的需求,可以将 store.$state 属性直接设置为新对象,或通过更改 pinia 实例的 state 来替换应用程序的整个状态,后者在 SSR 期间使用。

store.$state = { counter: 666, name: 'Paimon' }
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.state.value = {}

可以通过 store 的 $subscribe() 方法查看状态及其变化,类似于 Vuex 的 subscribe 方法。与常规的 watch() 相比,subscriptions 只会在 patches 之后触发一次。

Getters 接收器

Getter 用法类似于 Vue 的计算属性,通常在获取 State 值时进行相应的处理。可以用 defineStore() 中的 getters 属性定义。可接收状态 state 作为首个参数。

export const useStore = defineStore("main", {
  state: () => {
    return {
      ...,
      phone: "12345678910",
    };
  },
  getters: {
    /* 手机中间四位隐藏 */
    // 传入参数可以进行类型推倒出结果 => 写法一 => 不使用箭头函数
    // handleHiddenPhone(state) {
    //   // getters 具有缓存性
    //   console.log("getters");
    //   return state.phone
    //     .toString()
    //     .replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
    // },
    // 写法二 => 使用 this => 定义常规函数时
    // handleHiddenPhone(): string {
    //   // getters 具有缓存性
    //   console.log("getters");
    //   return this.phone.toString().replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
    // },
    // 写法三 => 接收状态作为第一个参数 -> 使用箭头函数
    handleHiddenPhone:(state) => state.phone.toString().replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2"),
  },
});

getters 有缓存性,虽被多次调用,但是在值不发生改变时只进行读取。在 getters 里可以用 this 进行操作,但项目若使用 TS,那么会因为不传 state 导致 TS 无法自动推断出返回的数据类型,所以要显示标记返回的类型,否则会提示错误。

getters 无法直接被传递任何参数。但可以从 getters 返回一个函数以接受所需的参数。在执行此操作时,getters 不再缓存,只是调用的函数。

import { defineStore } from "pinia";
import { jsokStore } from "./jsok";
export const useStore = defineStore("main", {
  state: () => {
    return {
      ...,
      users: [ { name: "gz", id: 1, }, { name: "hz", id:2 } ],
    };
  },
  getters: {
    ...
    getUserById: (state) => {
      return (userId:number) => state.users.find((user) => user.id === userId);
    },
  },
  actions: {},
});
<template>
  ...
  <div>user 1 => {{getUserById(1)}}</div>
</template>
<script lang="ts">
import { reactive,... } from 'vue'
import { useStore } from '../store/index'
import { storeToRefs } from 'pinia'
export default {
  name: '',
  setup() {
    const store = useStore();
    const {..., getUserById} = storeToRefs(store);
    return {
      ...toRefs(data),
      store,
      ...,getUserById
    }
  },
}
</script>
<style scoped lang='less'></style>

Actions 操作

Actions 相当于组件中的 methods,适合操作业务逻辑,可以通过 defineStore() 中的 actions 属性定义。

和 getters 一样,actions 也能通过 this 访问 store 实例。除了支持异步操作,调用 actions 时,会自动进行类型推断。

import { defineStore } from "pinia";
export const useStore = defineStore("main", {
  state: () => {
    return { helloPinia: "helloPinia", ... };
  },
  getters: {...},
  actions: {
    changeState() { this.helloPinia = this.helloPinia === "helloPinia" ? "Hello World" : "helloPinia"; },
  },
});

xxStore.$onAction() 方法可以订阅 actions 及其结果。传入的参数回调在 actions 之前执行,其中 after() 是在 actions 完成后处理 Promise 的执行函数;onError() 在处理中抛出错误。

// useMain.ts
import { defineStore } from "pinia";
const useMainStore = defineStore("useMainUniqueId", {
  state: () => ({
    user: { name: "zs", age: 23, },
  }),
  actions: {
    subscribeAction(name: string, age: number, manualError?: boolean) {
      return new Promise((resolve, reject) => {
        console.log("subscribeAction Function Performs");
        if (manualError) {
          reject("manualError !");
        } else {
          this.user.name = name;
          this.user.age = age;
          resolve(`${this.user.name} => ${this.user.age}`);
        }
      });
    }
  },
});
export default useMainStore;
<!-- UseMain.vue -->
<template>
  <div>
    <button @click="subscribeNormal">click me show $onAction Ok</button>
    <button @click="subscribeError">click me show $onAction Error</button>
  </div>
</template>
<script lang="ts">
import useMainStore from "../store/useMain";
import { ref, defineComponent, computed } from "vue";
export default defineComponent({
  setup() {
    const useMainUniqueId = useMainStore();
    function subscribeNormal() { useMainUniqueId.subscribeAction(useMainUniqueId.user.name, useMainUniqueId.user.age, false); }
    function subscribeError() { useMainUniqueId.subscribeAction("ErrorError", 97, true); }
    const unsubscribe = useMainUniqueId.$onAction(
      ({
        name, // action 函数的名称
        store, // store 实例 => useMainUniqueId
        args, // action 函数参数数组
        after, // 钩子函数 => 在 action 函数执行完成返回或者 resolves 后执行
        onError, // 钩子函数 => 在 action 函数报错或者 rejects 后执行
      }) => {
        console.log("action func name => ", name);
        console.log("args array => ", args);
        console.log("store instance => ", store);
        after((result) => { console.log("$onAction after func => ", result); });
        onError((error) => { console.log("Error Catch => ", error); });
      },
      false // 卸载组件后不保留
    );
    return { subscribeNormal, subscribeError, };
  },
});
</script>
<style scoped lang="less"></style>

Bugs 解决

  • xxxStore.<function> is not a function => 标识冲突时产生

显示不出组件的情况 => defineStore() 要求首个参数是不重复的字符串唯一标识。

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!