Appearance
派发器思想的初衷
首先要明白,“状态”和“数据”分别表示什么?
“数据”: 数据是提供给我们视图的一个显示出来的依据,数据是可以被改变的,在Vue
的响应式程序中,当响应式数据发生改变的时候,视图也会随着改变。
“状态”:Vuex
中提出状态的概念,Vuex
就是基于Redux
重新发展出来的一个库,它的核心理念并没有逃脱Redux
的思想,Redux
中将统一管理的这部分数据称为”状态“。
它为什么要叫做”状态“呢?是因为,这个数据综合的管理,它其实反应的是我们视图到底处于一个什么样的静态状况。而我们每一个所谓数据的状态发生改变以后,实际上我们是要使视图发生改变。其次,状态的改变,不仅仅是跟视图相关,它还可能跟其它的状态或者数据相关,例如计算属性等。也就是说状态的改变,它会使其它的数据,包括视图也发生改变,所以在一个综合管理的这个情况下,我们把综合管理的东西叫做”状态“。状态其实是高于数据的,Vuex
叫做”中央状态管理器“,其实它就是把各个组件所需要的一个状态集中到一起管理,有统一的状态触发、视图改变。所以Vuex
叫做中央状态管理器,而组件中的data
叫做数据。
在Vue
中我们通常以组件的形式去开发视图模板,组件会以组件树的形式存在。也就是说,一个组件通常会被我们拆分为不同的子孙组件,多个子孙组件将会成为组件树的不同分支。那么此时就出现一个问题:比如说,我现在有一个组件TODOLIST
,这个组件被我拆分出去两个子组件Form
、List
,List
子组件又被我拆分出去一个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、Inject
在Vue
组件中的开发使用
既然Vue
组件开发流程是严格按照Props
和Emits
传递的形式,那么我现在就有一个问题:如果TodoList
组件的孙子组件ListItem
需要TodoList
入口文件中的某个方法处理逻辑,那么ListItem
组件还需要一层一层通过emit
进行传递的形式交由TodoList
入口文件进行处理吗?
其实我们可以使用Provide/Inject
去直接向组件树下的子组件注入数据,但是Provide/Inject
也不能够乱用。因为Provide/Inject
注入的数据是没有办法知道来源的,也就是说:父组件不知道谁注入的数据,而子组件也不知道是谁提供了数据。
什么情况下使用Provide/Inject
呢?
- 在一个组件体系下,在一个功能整体中,如果深入嵌套。
这是什么意思呢?也就是说,如果我有一个todoList
的组件,todoList
组件有多个子组件,此时组件还有多层的嵌套,那么此时我就可以使用provide、inject
进行处理props
逐层传透的问题。为什么呢?因为所有的子组件都存在于todoList
组件下,而todoList
是一个组件体系,所有关于todoList
功能都在这一个todoList
组件下,后期维护的时候,我们也只是在这个todoList
下面去寻找provide、inject
。这样后期维护就相对于简单了一些。
- 在一个组件体系下,多层级多个组件使用的时候。
TodoList --> TodoFooter --> TodoStatics
TodoList --> todos --> item
如果在同一个组件体系下,多层级多个组件使用同样的数据的时候,这时候就可以使用provide、inject
进行处理。例如TodoStatics、item
组件都需要使用TodoList
中的部分数据,因为它们现在都处于一个组件体系下,所以可以通过provide、inject
进行处理,利于后期维护。对于这种场景,我们也可以使用将要实现的store
思想来解决。
Provide
与Inject
在组件中使用时的伪代码:
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
这些数据时,都是通过函数的方式进行操作,此时你会发现代码的语义化非常友好。
- 组件本质上是为了开发视图,但是现在出口组件中存在非常多的组件逻辑。我们想的是能够让组件纯粹的为视图服务,对于组件中的逻辑我们将其抽离集中维护。
- 单组件组件树中,不同的孙子组件共用同一状态。如果使用
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
设计上需要我们注意:
action
默认是以函数的形式出现,因为调用action
函数之后,还需要给外界提供相应的接口。- 调用
action
时,将传递参数state
。state
表示的就是微型store
中的state
对象,因为调用任务的目的就是修改状态,所以任务要接收state
状态对象。 - 利用闭包函数的特点,在执行调用
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
派发器驱动入口函数也会执行,这是设计的一种方式。
<font style="color:#DF2A3F;">reducer、useReducer</font>
从派发器驱动的形式上来看,useReducer
函数只是派发器驱动的入口函数,而reducer
函数是派发器驱动的主要驱动函数。因为useReducer
函数执行的目的,就是为了能够给外界提供state
数据、dispatch
公共接口。但是reducer
函数是根据任务的类型去驱动不同任务的调用执行,很显然reducer
函数是派发器处理逻辑的主要函数,而useReducer
只是派发器的驱动入口函数。
<font style="color:#DF2A3F;">initialValue</font>
<font style="color:#DF2A3F;">reactive</font>
initialValue
实际上就是store/state
状态对象,因为调用任务的目的就是操作状态。所以在reducer
函数执行的时候,我们要把state
状态对象以参数的形式进行传递。但是为什么要用reactive
包装为响应式对象呢?因为store/state
本质上就是一个普通对象,它不具有响应式。我们在Vue
中说的响应式数据是指什么意思呢?也就是说,当数据发生改变的时候,视图也会随着发生改变。那么对于普通的对象来说,并不存在这种功能,所以要想实现当数据发生改变的时候,视图也随之更新的效果,我们就要将state
这个普通的对象包装为响应式数据。
这样的话,当任务被调用时修改state
状态,身为响应式数据的state
对象,在内部状态发生改变的时候,视图将会随之发生更新。
- ``
实际上useReducer
函数执行的过程非常简单:从下面外界使用派发器的形式上来说,当useReducer
派发器入口函数被调用时,useReducer
函数将组装以状态state
和dispatch
公共接口为元素的数组。外界调用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
});
};