Skip to content

派发器思想的初衷

首先要明白,“状态”和“数据”分别表示什么?

“数据”: 数据是提供给我们视图的一个显示出来的依据,数据是可以被改变的,在Vue的响应式程序中,当响应式数据发生改变的时候,视图也会随着改变。

“状态”:Vuex中提出状态的概念,Vuex就是基于Redux重新发展出来的一个库,它的核心理念并没有逃脱Redux的思想,Redux中将统一管理的这部分数据称为”状态“。

它为什么要叫做”状态“呢?是因为,这个数据综合的管理,它其实反应的是我们视图到底处于一个什么样的静态状况。而我们每一个所谓数据的状态发生改变以后,实际上我们是要使视图发生改变。其次,状态的改变,不仅仅是跟视图相关,它还可能跟其它的状态或者数据相关,例如计算属性等。也就是说状态的改变,它会使其它的数据,包括视图也发生改变,所以在一个综合管理的这个情况下,我们把综合管理的东西叫做”状态“。状态其实是高于数据的,Vuex叫做”中央状态管理器“,其实它就是把各个组件所需要的一个状态集中到一起管理,有统一的状态触发、视图改变。所以Vuex叫做中央状态管理器,而组件中的data叫做数据。

Vue中我们通常以组件的形式去开发视图模板,组件会以组件树的形式存在。也就是说,一个组件通常会被我们拆分为不同的子孙组件,多个子孙组件将会成为组件树的不同分支。那么此时就出现一个问题:比如说,我现在有一个组件TODOLIST,这个组件被我拆分出去两个子组件FormListList子组件又被我拆分出去一个ListItem组件。如果说Form子组件和List子组件中存在共用一个状态的情况时,我们该如何去设计呢?

很简单,有些人会说:我们可以使用Vuex来解决这个问题。使用Vuex来解决这个问题,当然是可以的。但是很不合理,Vuex叫做中央状态管理器,目的是:为了能够集中管理多组件中共用的状态。而现在的情况是只存在一个单组件TODOLIST,如果你把TODOLIST中的所有状态都放在Vuex中,很显然是不合理的。为什么不合理呢?因为这些状态只有TODOLIST能够用到,其它的组件都用不到,自然这种设计是不符合Vuex设计的初衷的。

还有一个问题,在开发Vue组件时,我们通常在组件中设置一个块级组件,块级组件也被称为出口组件。出口组件是整个组件的出口,组件的所有逻辑都会在该组件内部实现。当然,组件逻辑一旦复杂起来,那么出口组件中的代码维护就会繁琐,所以我们还要解决出口组件中逻辑繁重的问题,要将出口组件中的逻辑抽离出来,因为组件本身就是处理视图的。

派发器思想的设计

Vuex中借鉴了派发器的思想,所以我们也可以借鉴Vuex去实现一个属于我们的小型store。那么我们预期的结果是什么样子的呢?也就是说,在单组件TODOLIST中,我们存在一个store,这个store不仅仅帮我们存放单组件中需要的状态,而且能够通过不同的需求去驱动不同类型的任务执行。换句话讲,这个驱动能够帮我们派发任务,按照不同的需求去调用不同类型的任务,从而去改变状态。

我们从下图中可以看到整个store运行的过程:

首先按照设置不同的,当接收到不同任务的类型时,将会去调用不同的执行。任务执行的调用将会改变,当状态发生改变的时候,那么就会随之改变。

对于TODOLIST组件中不同子组件共用同一状态的问题,我们就可以利用这个微型的store进行解决,并且我们可以将出口文件中的逻辑看做任务,将任务抽离到store中进行实现。

画板

派发器代码的实现结构

我希望的是派发器文件中给我导出一个useReducer方法,调用useReducer方法返回[ state, dispatch ]的结构。其中state表示当前组件内部需要用到的状态;dispatch表示派发器驱动方法,并且dispatch()方法接收配置参数,例如:dispatch({ type: 'addTodo', pyload: xxx }),其中type表示需要执行的任务,pyload表示需要传递的参数。

当我调用dispatch方法之后,派发器驱动就能够对应的派发任务执行,然后任务的执行影响状态的变化,状态变化之后,视图将随之更新。

javascript
import useReducer from '../index'
const [state, dispatch] = useReducer()
dispatch({
    type: 'addTodo',
    pyload: 2
})

最基本TODOLIST组件的实现

TODOLIST组件的实现我们已经做过好多次了,所以这次我们主要针对派发器思想,对于TODOLIST组件的实现就不多介绍了,但是对于一些组件上的设计,我们会强调指出。

块级组件、出口组件创建

根据目录结构来看,TodoList文件夹下的index.vue就是TodoList组件的出口文件,关于TodoList组件的逻辑都要汇总到该组件内部实现,但是之后我们会将出口文件中的逻辑进行抽离。List文件作为TodoList单组件下面的子组件文件,它应该也具有出口组件,因为List组件内部存在子组件ListItem,所以关于List组件的内部逻辑都要放在List出口组件中进行处理。由于Form组件并不存在子组件,所以不需要设置出口组件。

Vue组件开发中的单项数据流、Props、Emit传递问题

其实这个问题已经很简单了,我们学过Vue都知道:在Vue组件开发中,父组件通过Props给子组件传递属性,但是子组件不能够修改父组件传递的Props属性,这也是单项数据流的特点。我们可以让子组件通过Emit向父组件传递自定义事件,父组件接收到自定义事件后做出相关逻辑处理。

举个例子:父组件TodoList要向Form子组件传递addCount状态,而Form子组件需要通过Emit向父组件传递自定义addTodo事件,所以我们的代码就可以写成下面样子:

这是最基本的Vue组件开发流程。

javascript
<form-comp
	@add-todo="addTodo"
	:add-count="addCount"
></form-comp>

// addTodo逻辑处理
const addTodo = todo => {};
javascript
<button @click="onAddTodo">ADDTODO</button>

const props = defineProps({
	addCount: Number
});
const emit = defineEmits(['addTodo']);

const onAddTodo = () => {
	emit('addTodo', 'pyload');
};

Provide、InjectVue组件中的开发使用

既然Vue组件开发流程是严格按照PropsEmits传递的形式,那么我现在就有一个问题:如果TodoList组件的孙子组件ListItem需要TodoList入口文件中的某个方法处理逻辑,那么ListItem组件还需要一层一层通过emit进行传递的形式交由TodoList入口文件进行处理吗?

其实我们可以使用Provide/Inject去直接向组件树下的子组件注入数据,但是Provide/Inject也不能够乱用。因为Provide/Inject注入的数据是没有办法知道来源的,也就是说:父组件不知道谁注入的数据,而子组件也不知道是谁提供了数据。

什么情况下使用Provide/Inject呢?

  1. 在一个组件体系下,在一个功能整体中,如果深入嵌套。

这是什么意思呢?也就是说,如果我有一个todoList的组件,todoList组件有多个子组件,此时组件还有多层的嵌套,那么此时我就可以使用provide、inject进行处理props逐层传透的问题。为什么呢?因为所有的子组件都存在于todoList组件下,而todoList是一个组件体系,所有关于todoList功能都在这一个todoList组件下,后期维护的时候,我们也只是在这个todoList下面去寻找provide、inject。这样后期维护就相对于简单了一些。

  1. 在一个组件体系下,多层级多个组件使用的时候。

TodoList --> TodoFooter --> TodoStatics

TodoList --> todos --> item

如果在同一个组件体系下,多层级多个组件使用同样的数据的时候,这时候就可以使用provide、inject进行处理。例如TodoStatics、item组件都需要使用TodoList中的部分数据,因为它们现在都处于一个组件体系下,所以可以通过provide、inject进行处理,利于后期维护。对于这种场景,我们也可以使用将要实现的store思想来解决。

ProvideInject在组件中使用时的伪代码:

javascript
provide('TodoListMethod', {
    addTodo
})
javascript
const { addTodo } = inject('TodoListMethod')

:::color1 Note:

一个组件树下的子组件,可以使用Provide、Inject去传递属性或者方法。但是Provide的组件尽量是最顶端的父组件,不要到处都在提供,尽可能把所有数据都几种在父组件管理。

派发器相对来说就大一些,还是看设计。派发器要做的话,就得整个项目都要用这种思想,如果多个组件树都要用同一个数据,最好使用Vuex

其实Vuex就是一个大的派发器,只是它集成了管理状态的功能,对于任务并没有集成。对于任务的集成是指:实现对异步同步任务进行自动派发的机制,我们下章就会介绍。

:::

Vue中封装useState Hooks

为什么要在Vue中借鉴useState Hooks的思想呢?主要还是为了能够有更好的语义化,比如说:我现在对data进行获取值的操作或者是重新赋值的操作,那么此时你可能会说:为什么你不直接对数据进行操作呢?而非要利用Hooks的思想去操作数据呢?主要是因为直接操作数据的语义化并不友好,当一个组件内部中存在多处直接操作数据的形式时,那么整体的逻辑就会变得臃肿,我们为了能够让组件逻辑中具有更好的语义化,所以通过借鉴useState Hooks的思想进行设计与处理。

从下面的例子中,我们能够看出Hooks思想的好处,无论是赋值操作还是获取值的操作,在Hooks思想中都是利用函数来处理的,函数不仅仅能够易于扩展,而且还有很好的语义化。所以,在实现TODOLIST时,我们建议使用Hooks的思想去封装这些数据的操作逻辑。

javascript
const addCount = 0
// 获取值的操作
console.log(addCount)
// 重新赋值的操作
addCount = 100
console.log(addCount) // 100
javascript
const [addCount, setAddCount] = useState(0)
// 获取值的操作
console.log(addCount)
// 重新赋值的操作
setAddCount(100)
console.log(addCount) // 100

使用派发器思想的原因

首先看TodoList组件中出口组件的逻辑,我们能够看到useState Hooks的优点,在toggleTodo/addTodo这些方法内部操作addCount/removeCount这些数据时,都是通过函数的方式进行操作,此时你会发现代码的语义化非常友好。

  1. 组件本质上是为了开发视图,但是现在出口组件中存在非常多的组件逻辑。我们想的是能够让组件纯粹的为视图服务,对于组件中的逻辑我们将其抽离集中维护。
  2. 单组件组件树中,不同的孙子组件共用同一状态。如果使用Vuex的话,很显然是不合理的;如果使用Provide/Inject的话,能够解决问题,但不是最理想的解决方式。最理想的解决方式就是:我们上面说的在单组件内部中集成一个微型store,这个微型store的驱动不仅能够帮助我们解决子组件共用状态的问题,还能够按照需求去调用不同任务的类型,驱动状态的改变。

所以说,有了上面优化的需求,我们就要去集成一个微型store,帮我们解决遇到的问题。

javascript
const [todoList, setTodoList] = useState([])
const [addCount, setAddCount] = useState(0)
const [removeCount, setRemoveCount] = useState(0)

const toggleTodo = (id) => {
    setTodoList((todoList) =>
        todoList.map((todo) => {
            if (todo.id === id) {
                todo.completed = !todo.completed
            }
            return todo
        })
    )
}

const removeTodo = (id) => {
    setTodoList((todoList) => todoList.filter((todo) => todo.id !== id))
    setRemoveCount((count) => count + 1)
}

const addTodo = (todoText) => {
    const todo = {
        id: new Date().getTime(),
        title: todoText,
        completed: false
    }
    setTodoList((todoList) => [...todoList, todo])
    setAddCount((count) => count + 1)
}

provide('TodoMethods', {
    addTodo,
    removeTodo
})

派发器思想设计,微型store的开发与设计

按照派发器的流程图,我想的是:派发器驱动是一个函数useReducer,调用派发器驱动函数useReducer将返回[state, dispatch]。其中dispatch以函数的形式出现,按照当前需求调用dispatch函数,传入需要执行的任务类型type,派发器驱动将自动调用相应的任务,任务执行后将修改当前状态,状态发生变化时,视图也会随之改变。

画板

state状态创建与声明

state其实很简单,就是组件中需要用到的一些状态。那么你可能会问,为什么你不给它设置为响应式对象呢?当响应式数据发生改变的时候,视图也会发生改变,这是Vue的特点。

对于state来说,它只是保存当前组件内部共用的一些状态而已,我们没有必要在state中操作数据。

javascript
export default {
    todoList: [],
    addCount: 0,
    removeCount: 0
}

actionType任务类型的声明与设计

actionType任务类型代表出口组件内部需要执行的任务类型,比如说:TodoList出口组件中需要调用执行addTodo/removeTodo/ToggleTodo等方法,那么这些方法都可以看做是任务。那么不同的任务自然是要对应着不同的任务类型,而且派发器会按照不同任务的类型来调用不同的任务执行。

javascript
export const ADD_TODO = 'ADD_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'

action任务处理

action的作用是什么呢?其实action也很简单,action主要的功能就是实现每个任务中需要处理的逻辑。比如说:TodoList组件中的addTodo任务,addTodo任务的逻辑本质上就是增加一条新的todo列表,所以action中的addTodo任务逻辑就是增加一条todo列表。

在对action设计上需要我们注意:

  1. action默认是以函数的形式出现,因为调用action函数之后,还需要给外界提供相应的接口。
  2. 调用action时,将传递参数statestate表示的就是微型store中的state对象,因为调用任务的目的就是修改状态,所以任务要接收state状态对象。
  3. 利用闭包函数的特点,在执行调用action之后,action需要给派发器驱动提供公开的接口。
javascript
export default function (state) {
    function addTodo(todoText) {
        const todo = {
            id: new Date().getTime(),
            title: todoText,
            completed: false
        }
        state.todoList = [...state.todoList, todo]
        state.addCount += 1
        console.log('state: =>', state)
    }

    function toggleTodo(id) {
        state.todoList = state.todoList.map((todo) => {
            if (todo.id === id) {
                todo.completed = !todo.completed
            }
            return todo
        })
    }

    function removeTodo(id) {
        state.todoList = state.todoList.filter((todo) => todo.id !== id)
        state.removeCount += 1
    }

    return {
        addTodo,
        toggleTodo,
        removeTodo
    }
}

派发器驱动useReducer的设计与实现

派发器驱动的本质是函数形式,为什么是函数的形式呢?因为我们在设计的时候,需要派发器驱动给外界提供一个公开的接口useReducer函数,当useReducer函数调用执行后,将返回[ state, dispatch ]的形式。所以吖,派发器本质上就是一个函数。

实现派发器驱动的注意事项:

从形式上来说,我们希望派发器驱动是一个函数的形式。因为当外界调用派发器驱动的时候,派发器驱动能够直接执行驱动内部的逻辑。所以,实际上export default function() {}函数内部调用的是useReducer派发器驱动入口函数。当外界调用执行模块化导出的函数时,函数内部的useReducer派发器驱动入口函数也会执行,这是设计的一种方式。

  1. <font style="color:#DF2A3F;">reducer、useReducer</font>

从派发器驱动的形式上来看,useReducer函数只是派发器驱动的入口函数,而reducer函数是派发器驱动的主要驱动函数。因为useReducer函数执行的目的,就是为了能够给外界提供state数据、dispatch公共接口。但是reducer函数是根据任务的类型去驱动不同任务的调用执行,很显然reducer函数是派发器处理逻辑的主要函数,而useReducer只是派发器的驱动入口函数。

  1. <font style="color:#DF2A3F;">initialValue</font><font style="color:#DF2A3F;">reactive</font>

initialValue实际上就是store/state状态对象,因为调用任务的目的就是操作状态。所以在reducer函数执行的时候,我们要把state状态对象以参数的形式进行传递。但是为什么要用reactive包装为响应式对象呢?因为store/state本质上就是一个普通对象,它不具有响应式。我们在Vue中说的响应式数据是指什么意思呢?也就是说,当数据发生改变的时候,视图也会随着发生改变。那么对于普通的对象来说,并不存在这种功能,所以要想实现当数据发生改变的时候,视图也随之更新的效果,我们就要将state这个普通的对象包装为响应式数据。

这样的话,当任务被调用时修改state状态,身为响应式数据的state对象,在内部状态发生改变的时候,视图将会随之发生更新。

  1. ``

实际上useReducer函数执行的过程非常简单:从下面外界使用派发器的形式上来说,当useReducer派发器入口函数被调用时,useReducer函数将组装以状态statedispatch公共接口为元素的数组。外界调用dispatch公共接口时,将会调用派发器驱动函数reducer的执行。随着reducer函数的执行,reducer函数根据不同任务的类型执行任务,从而修改状态。由于状态是响应式数据,所以视图也会发生更新。

javascript
import useReducer from './store/reducer'
// 外界使用派发器
const [state, dispatch] = useReducer()
// 驱动任务执行
dispatch({
    type: 'ADD_TODO',
    pyload: { name: 'jack', age: '30' }
})
javascript
import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from './actionType'

import initialValue from './state'

import { reactive } from 'vue'

import action from './action'

function reducer(state, { type, pyload }) {
    const { addTodo, removeTodo, toggleTodo } = action(state)

    switch (type) {
        case ADD_TODO:
            addTodo(pyload)
            break
        case REMOVE_TODO:
            removeTodo(pyload)
            break
        case TOGGLE_TODO:
            toggleTodo(pyload)
            break
        default:
            break
    }
}

function useReducer(reducer, initialValue) {
    const state = reactive(initialValue)
    const dispatch = (pyload) => {
        reducer(state, pyload)
    }
    return [state, dispatch]
}

export default function () {
    return useReducer(reducer, initialValue)
}

TodoList出口组件优化完成后的代码

````````````

现在你再看看TodoList出口组件中的逻辑,是不是少很多。并且组件中的逻辑都不需要你在出口组件中维护,而是到我们集成的微型store维护。并且呢,如果后期组件需要逻辑扩展,我们依旧可以在store中继续扩展任务。

javascript
<template>
  <div class="todo-container">
    <form-comp
      @add-todo="addTodo"
    ></form-comp>
    <list-comp
      :todo-list="todoList"
      :add-count="addCount"
      :remove-count="removeCount"
    ></list-comp>
  </div>
</template>

<script setup>
  import { reactive, provide, toRefs } from 'vue';
  import { useState } from '../../Hooks';
  import FormComp from './Form';
  import ListComp from './List';

  import {
    ADD_TODO,
    useReducer
  } from './store/index';

  const [ state , dispatch ] = useReducer();

  const {
    todoList,
    addCount,
    removeCount
  } = toRefs(state);

  const addTodo = todoText => {
    dispatch({
      type: ADD_TODO,
      pyload: todoText
    });
  };