在 TypeScript 中实现模型-视图-更新(MVU)模式





0/5 (0投票)
在本文中,我们将看到如何以函数式方式构建一个小型应用程序。
应用程序开发的世界广阔而丰富。有小块,有更大的复合抽象,不同的编程语言,编程语言的库,新趋势,时尚,被遗忘的宝藏,以及未被注意的沉默组件。
通过本文,我们将以函数式方式构建一个小型应用程序。可预测的状态、选择器、动作、高阶组件、副作用将是主要角色。本文的材料可用于学习MVU。它可以用作实验场所,并将挑战那些寻求函数式开发知识的人。它概述了函数式方法对所实现应用程序形态的影响。
我们仍在发明,再发明。发现并重新发现。对我来说,一切都始于WndProc的辉煌。Windows API中的消息分发系统。现在时尚正逐渐转向函数式开发。消息分发队列、可预测状态容器、单子函数是对时尚的回应。
当我知道我不知道我不知道什么时。
模型-视图-更新(MVU) - 现有、再发明、再发现
MVU是一种逐渐从函数式编程语言中演变出来的设计模式。在早期阶段,面向对象编程(OOP)迈出了第一步。现有开发人员在解决需求时使用了函数式范式、单子运算符抽象、函数式map
和reduce
运算符。让我们尝试遵循函数式软件路径,并像使用现代技术一样解决任务。有哪些限制?这些限制是真的吗?也许这是为了OOP时尚而做出的权衡。
MVU草案中的子层
由于MVU是另一种设计模式。与MVC、MVVM类似,它将拥有类似的高层生态系统,如数据传输、模型、视图层。关于这些层的更多信息可以在文章介绍MVVM架构中找到。在本文中,我们重点关注差异。视图层有不同的形态。因为我们的责任只是函数。应用程序的状态将频繁更新。只有部分状态更新与UI更改相关。应该有一个工具,只选择并更新实际更改的UI部分。其余的UI将保持不变。有一个现代概念来解决这个问题。它就是虚拟DOM。由于完整的vDom是一个巨大的任务。这里将只介绍一个小型版本。足以展示其通用思想。
虚拟DOM
它分为三个部分
- 模板 - 将保留描述视图形状的标记
- DOM - 用于处理浏览器元素
- 虚拟DOM - 用于生成初始视图和执行小型更新
它将提供在UI上定位元素的方法。这些元素绑定到状态的更新部分。从一开始,这个任务听起来有点奇怪。然而,它有一个巧妙的解决方案。让我们检查一下实现,并尝试理解最重要的部分。
这是可以用DOM实体对UI进行的操作
const dom = {
eventHandlers: [],
closest(el, selector) {
return el.closest(selector);
},
attach(inst, selector, eventName, fn) {
const pair = dom.eventHandlers.find(([key]) => fn === key);
const index = dom.eventHandlers.indexOf(pair);
if (index >= 0) {
throw new Error(`Event handler ${fn} for event ${eventName} already attached`);
}
let detach;
const handler = function (evnt) {
if (selector && dom.closest(evnt.target, selector)) {
fn(evnt);
} else if (!selector) {
fn(evnt);
}
};
inst.addEventListener(eventName, handler);
dom.eventHandlers.push([fn, handler]);
return detach = function () {
const pair = dom.eventHandlers.find(([, h]) => handler === h);
const index = dom.eventHandlers.indexOf(pair);
if (index >= 0) {
inst.removeEventListener(eventName, handler);
} else {
throw new Error(`Error in detach result.
Can't detach unexisting ${eventName} handler`);
}
};
},
detach(inst, eventName, fn) {
const pair = dom.eventHandlers.find(([key]) => fn === key) || [];
const [, handler] = pair;
const index = dom.eventHandlers.indexOf(pair);
if (index >= 0) {
dom.eventHandlers.splice(index, 1);
inst.removeEventListener(eventName, handler);
} else {
setTimeout(() => {
throw new Error(`Error in detach method.
Can't detach unexisting ${eventName} handler`);
});
}
},
el(type, attrs, ...children) {
attrs = attrs || {};
children = [].concat(...children);
const el = document.createElement(type);
const attrNames = Object.keys(attrs);
attrNames.forEach(attrName => {
if (attrName in EVENT_NAMES) {
const eventName = EVENT_NAMES[attrName];
dom.attach(el, '', eventName, attrs[attrName]);
} else if (attrName in el) {
el[attrName] = attrs[attrName];
} else {
el.setAttribute(attrName, attrs[attrName]);
}
});
for (const child of [...children]) {
el.append(child);
}
return el;
}
};
乍一看感觉很奇怪。所有真正需要的只是附加/分离事件和创建元素。尽管工具列表很小。这里的所有内容都非常重要。事件将非常频繁地附加和分离。在最坏的情况下,每次状态更新都会发生。听起来相当可怕。它可以优化。优化部分将取决于实现。
第二个子层更有趣。它将包含更多DOM操作。这些操作与比较虚拟DOM上的更改有关。在需要时执行UI更新。当状态更新有内容可显示时,UI将更新。
虚拟DOM - DOM操作的第二部分。
import { dom } from './dom';
import { arrayMerge } from './utils';
export const EVENT_NAMES = {
onClick: 'click',
onInput: 'input',
onKeyPress: 'keypress',
onChange: 'change',
onSubmit: 'submit',
onKeyDown: 'keydown',
onKeyUp: 'keyup',
onBlur: 'blur',
onMouseDown: 'mousedown'
};
export const propConverters = {
contentEditable: function (value) {
return !!value;
},
convert(propName, value) {
if (propName in propConverters) {
return propConverters[propName](value);
}
return value;
}
};
export function el(type, attrs = {}, ...children) {
children = [].concat(...children)
.filter(a => [undefined, true, false].indexOf(a) === -1)
.map(item => (['object', 'function'].indexOf(typeof item) === -1 ? '' + item : item));
if (typeof type === 'function') {
return type({ ...attrs, store: currentStore,
children: children.length > 1 ? children : children[0] }, children);
}
return {
type,
attrs,
children
};
}
export let currentStore = null;
export function makeVdom(oldDom, store) {
currentStore = store;
function createElement(node) {
if (node === undefined) {
return document.createTextNode('');
}
if (['object', 'function'].indexOf(typeof node) === -1) {
return document.createTextNode(node);
}
if (typeof node.type === 'function') {
const { children = [] } = node;
const [first] = children;
const $el = createElement(first);
patch($el, node.type, { ...node.attrs, store });
return $el;
}
const { type, attrs = {}, children } = node;
const { ref, ...attributes } = (attrs || {});
const el = dom.el(type, attributes,
...[].concat(children).map(child => createElement(child)));
return el;
}
function compare($el, newNode, oldNode) {
if (typeof newNode !== typeof oldNode) {
return true;
}
if (['object', 'function'].indexOf(typeof newNode) === -1) {
if (newNode !== oldNode) {
const oldValue = $el.textContent;
if (oldValue !== newNode) {
return true;
}
}
return false;
}
return newNode.type !== oldNode.type
}
function updateAttribute($el, newValue, oldValue, key) {
if (oldValue === undefined) {
$el.setAttribute(key, newValue);
} else if (newValue === undefined) {
$el.removeAttribute(key);
} else if (newValue !== oldValue) {
$el.setAttribute(key, newValue);
}
}
function updateProperty($el, newValue, oldValue, key) {
const oldElValue = $el[key];
if (oldValue === undefined) {
if (oldElValue !== newValue) {
$el[key] = propConverters.convert(key, newValue);
}
} else if (newValue === undefined) {
if (oldElValue !== newValue) {
$el[key] = propConverters.convert(key, newValue);
}
} else if (newValue !== oldValue) {
if (oldElValue !== newValue) {
$el[key] = propConverters.convert(key, newValue);
}
}
}
function updateEvent($el, newHandler, oldHandler, key) {
const eventName = EVENT_NAMES[key];
if (!oldHandler) {
dom.attach($el, '', eventName, newHandler);
} else if (!newHandler) {
dom.detach($el, eventName, oldHandler);
} else {
dom.detach($el, eventName, oldHandler);
dom.attach($el, '', eventName, newHandler);
}
}
function updateAttributes($el, newAttrs, oldAttrs) {
newAttrs = newAttrs || {};
oldAttrs = oldAttrs || {}
const newKeys = Object.keys(newAttrs);
const oldKeys = Object.keys(oldAttrs);
const allKeys = arrayMerge(newKeys, oldKeys);
for (const key of allKeys) {
if (key in EVENT_NAMES) {
updateEvent($el, newAttrs[key], oldAttrs[key], key);
} else if (key in $el) {
updateProperty($el, newAttrs[key], oldAttrs[key], key);
} else {
updateAttribute($el, newAttrs[key], oldAttrs[key], key);
}
}
}
function detachEvents($el, oldAttrs) {
oldAttrs = oldAttrs || {}
const oldKeys = Object.keys(oldAttrs);
for (const key of oldKeys) {
if (key in EVENT_NAMES) {
updateEvent($el, undefined, oldAttrs[key], key);
}
}
}
function updateElement($parent, newNode, oldNode, index = 0) {
let nodesToRemove = [];
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
detachEvents($parent.childNodes[index], oldNode.attrs);
nodesToRemove.push($parent.childNodes[index]);
} else if (compare($parent.childNodes[index], newNode, oldNode)) {
detachEvents($parent.childNodes[index], oldNode.attrs);
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
updateAttributes($parent.childNodes[index], newNode.attrs, oldNode.attrs);
const length = Math.max(newNode.children.length, oldNode.children.length);
for (let i = 0; i < length; i++) {
nodesToRemove = [
...nodesToRemove,
...updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
)];
}
}
if (newNode && newNode.attrs && newNode.attrs.ref) {
newNode.attrs.ref($parent.childNodes[index]);
}
return nodesToRemove;
}
function patch($el, view, props = {}) {
const newDom = view({
...props,
store,
render(props) { patch($el, view, props); }
});
const removedNodes = updateElement($el, newDom, oldDom);
removedNodes.map(node => node.parentElement.removeChild(node));
oldDom = newDom;
}
return patch;
}
在这里,我将省略描述如何查找差异并应用更改的主要方法的部分。这在本文中有所描述:如何编写自己的虚拟DOM。在这里,我将专注于与集成相关的新部分。
这里的主要函数是makeVdom
。这是初始化方法。它创建一个初始的虚拟DOM状态。它接受两个参数:oldDom
和state
。OldDom
默认为null
。它将保留虚拟DOM的先前版本。用于比较更改并在UI上生成更新。state
用于向每个组件提供应用程序的全局状态。全局状态将被注入到每个渲染组件方法中。makeVdom
函数返回patch
函数。patch
函数将被非常频繁地调用。在渲染初始UI、状态更新时刷新UI、渲染/重新渲染子组件时。el
函数将用于JSX/TSX模板。
全局可预测状态
如果在OOP中,应用程序的状态高度分散。作为类的成员。在函数式范式中,状态的划分程度较低。理想情况下,它只是一个巨大的分层数据结构。将所有状态保存在一个地方。好处是,全局状态易于管理。读取可以并行执行。最棘手的部分是更新全局状态。顺序更新是修改全局状态的理想方式。当多个函数要更新状态时。只有一个函数执行更新。接下来的函数将排队,等待未来的执行。
存储
export function createStore(reducer, initialState, enhancer = null) {
const currentReducer = reducer;
let currentState = initialState;
const listeners = [];
if (enhancer) {
return enhancer(createStore)(
reducer,
initialState
);
}
return {
getState() {
return currentState;
},
dispatch(action) {
currentState = currentReducer(currentState, action);
listeners.map(listener => listener(currentState));
return action;
},
subscribe(newListener) {
listeners.push(newListener);
return function () {
const index = listeners.indexOf(newListener);
if (index >= 0) {
listeners.splice(index, 1);
}
};
}
};
}
一个getState
方法 - 提供请求全局状态的方式。一个dispatch方法
- 提供用于顺序状态更新的队列。subscribe
- 用于监听存储更新并刷新视图的方法。有关创建存储的更多信息,请参阅本文:通过编写迷你Redux学习Redux。
请注意。它来自一个关于虚拟DOM的类似故事。现在它用于状态。存储的第二部分是创建状态函数的组合。这里是组织存储最终状态的规则。更多内容在文章自己编写Redux第二部分:connect函数中有详细描述。
import { createStore } from './createStore';
import { reducer } from './reducers';
import { rootEffect } from './components';
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <t>(arg: T) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}
// Shamelessly stolen from this place:
// https://github.com/reduxjs/redux/blob/master/src/applyMiddleware.ts
// and slightly adjusted
export function applyMiddleware(...middlewares) {
return function (createStoreFn: typeof createStore) {
return function (reducer, initialState) {
const store = createStoreFn(reducer, initialState);
let dispatch = (action, ...args) => { throw Error
(`Dispatching {${JSON.stringify(action)}} while creating middleware`) };
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
};
}
}
function effectsMiddleware(store) {
setTimeout(() => {
store.dispatch({ type: '@INIT' });
});
let handler = rootEffect();
return function (dispatch) {
return function (action) {
const ret = dispatch(action);
for (const a of [].concat(...handler(action, store.getState(),
(a) => store.dispatch(a)))) {
store.dispatch(a);
}
return ret;
};
};
}
const createThunkMiddleware = (extraArgument?) =>
({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
var thunk = createThunkMiddleware();
export const store = createStore(reducer, { items: [], itemName: '' }, applyMiddleware(
thunk,
effectsMiddleware
));
</t>
这个复杂的层负责状态的形态。update
之后状态的自定义处理。当一个存储消息被分发到存储时。在OOP中,它可以按类摄取。在观察者模式的帮助下。最终,如果仔细查看effectsMiddleware
函数。可以看到OOP中出现的另一种设计模式 - 发布订阅。
函数式范式的另一个遗留问题是,没有什么新的东西可以创造。每个解决方案都看起来彼此相似。整个应用程序看起来像一个复合函数。它就像一个发射波的恒星。衡量它的唯一方法是发送输入数据并观察输出。
存储是更新状态系统,它是一个消息分发中心。每当有新消息分发到存储时,它都会根据在reducer中定义的规则更新状态。刷新在模板中定义的视图。然后,该消息以及更新后的状态将传递给rootEffect
例程进行额外处理。
为了更好地理解存储和全局状态,让我们尝试寻找DOM元素、DOM事件和DOM元素属性之间的相似之处。DOM本质上是声明性的。存储本质上是函数式的。进入函数式世界的主要入口。带有视图的存储监听DOM更新。将带有属性的DOM事件转换为消息。观察并同步DOM元素中相关的属性值与全局状态。
完成这张图的最后一个模块是connect
方法。
export function connect(mapStateToProps, mapDispatchToProps) {
let unsubscribe = () => { };
return function (wrappedTemplate) {
return function (p) {
const { render, store = vdom.currentStore, ...props } = p;
unsubscribe();
unsubscribe = store.subscribe((state) => {
render && render(props);
});
return wrappedTemplate({
...props,
...mapStateToProps(store.getState(), props),
...mapDispatchToProps(store.dispatch, props)
});
}
}
}
这个函数“连接”了两种范式,声明式方法和函数式方法。store.subscribe
方法用于声明式部分。在订阅时调用render
方法。整个视图将根据存储状态重新渲染、更新。rootEffect
用于函数式部分。你看到在我们的时代保持连接有多重要了吗? ;)
在这一步,只是为了便于理解。存储可以被视为函数式DOM适配器。每个onclick
、onchange
等事件都是消息。每个DOM元素属性都是状态的一部分。类似地,我们附加监听器到DOM元素并读取,例如,value
属性。现在我们可以在rootEffect
方法的帮助下做同样的事情。这在文章末尾有所描述。这次是关于消息和状态。每个元素都可以有许多事件。而DOM最终可以有许多元素。现在你可以想象通过这种设置可以产生多少种消息。我们可以以同样的方式考虑DOM元素属性。这是一个完美的输入系统。它允许随时以任何方式控制UI的任何部分。具有顺序更新的中央命令分发队列。类似于命令行。函数式宇宙刚刚诞生。它不仅可以显示,还可以监听和交谈。
动作消息和状态片段Reducer
到目前为止,应用程序是静默的。让我们打破沉默。应用程序更新状态和分发事件的形式在本文中得到了很好的描述:减少Redux样板的三种方法(并提高生产力)。简单的想法是将状态更新任务视为简单的update
操作。然后所有reducer可以根据低级任务进行分离。有点像我们如何编程处理数据库表。Insert
、update
、delete
操作。
关于这个想法,它是在declareActions
函数中引入的。
export function createReducer (initialState) {
return (reducerMap) => (state = initialState, action) => {
const reducer = reducerMap[action.type];
return reducer ? reducer(state, action) : state;
}
};
type UnionToIntersection<U> = (U extends any
? (k: U) => void
: any) extends ((k: infer I) => void)
? I
: any;
type ActionArg<Y, T> = T extends (a: keyof Y, b: infer I, c?) => any ? I : any;
export function declareActions<T extends {
[type in keyof T]: {
[name in keyof T[K]]: (type: type, props: P, meta?) => any;
};
}, K extends keyof T, KK extends keyof UnionToIntersection<T[K]>,
P, O extends UnionToIntersection<T[K]>>(
actions: T
): [{ [key in KK]: (args?: ActionArg<T, O[KK]>, meta?) => any }, { [key in K]: K }, any] {
const reducers = {};
const keys = Object.keys(actions);
const resActions = keys.reduce((res, type) => {
const actionDecl = actions[type];
let left = res[0],
right = res[1];
const actionNames = Object.keys(actionDecl);
const reducer = actionDecl.reducer;
if (reducer) {
reducers[type] = reducer;
}
const actionFn = actionDecl[actionNames[0]];
left = { ...left, [actionNames[0]]: (props, meta) => actionFn(type, props, meta) };
right = { ...right, [type]: type };
return [left, right];
}, [{}, {}]);
return [...resActions, createReducer({})(reducers)] as any;
}
export const selectPayload = ({ payload }) => payload;
它是一个构建方法。用于简单的更新状态命令。它允许构建消息处理块(reducer)。以及消息(action)和消息类型。缺点是它规定了以下消息格式。
动作消息的形态。
{
type: "UI_CREATE_TODO",
payload: {
title: "New todo item title"
}
}
下面列出了所有负责全局状态形态的构造。
这就是待办事项列表的更新方式。
export const [MainActions, MainActionTypes, mainReducer] = declareActions({
UI_CREATE_TODO: {
uiCreateTodo: (type, payload) => ({ type, payload })
},
UI_UPDATE_NEW_TITLE: {
uiUpdateNewTodoTitle: (type, payload) => ({ type, payload })
},
UI_TOGGLE_ALL_COMPLETE: {
uiToggleAllComplete: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
toggleAllComplete: payload
};
}
},
UI_SET_ACTIVE_FILTER: {
uiSetActiveFilter: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
activeFilter: payload
};
}
},
UI_CLEAR_COMPLETED: {
uiClearCompleted: (type, payload) => ({ type, payload })
},
UPDATE_MAIN_NEW_TODO_TITLE: {
updateNewTodoTitle: (type, payload: string) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
newTodoTitle: payload
};
}
},
UPDATE_MAIN_ITEMS: {
updateItems: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
items: payload,
activeItems: selectActiveItems(payload),
completeItems: selectCompleteItems(payload)
}
}
}
});
关于消息的命名,有些消息来自UI。这些消息以UI_
为前缀命名。其他可能导致UI更新的消息。用于更新全局状态。触发异步操作的消息没有前缀。这个小约定将有助于关注与UI相关的内容和其余内容。
一个列表项将以这种方式更新。
export const [ItemActions, ItemActionTypes, itemReducer] = declareActions({
UI_UPDATE_CURRENT_TITLE: {
uiUpdateCurrentTitle: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
title: payload
};
}
},
UI_SET_CURRENT_ITEM: {
uiSetCurrentItem: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
id: payload
};
}
},
UI_UPDATE_TODO_TITLE: {
uiUpdateTodoTitle: (type, payload) => ({ type, payload })
},
UI_SET_COMPLETE: {
uiSetComplete: (type, id, complete) => ({ type, payload: { id, complete } })
},
UI_REMOVE_ITEM: {
uiRemoveItem: (type, payload) => ({ type, payload })
},
SET_CURRENT: {
setCurrent: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { payload }) => {
return {
...state,
...payload
};
}
}
});
另一个优点是,这样的声明有助于开始编写动作、动作类型和 reducer。将它们放在一个地方。以便快速检查消息分发时全局状态会发生什么。
下一组消息和reducer。这次是更新状态。数据来自后端请求。
export const [ToDoActions, ToDoActionTypes, toDoReducer] = declareActions({
FETCH_TODOS_ERROR: {
fetchTodosError: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
error: payload
};
}
},
FETCH_TODOS_RESULT: {
fetchTodosResult: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
loading: false,
items: selectItemsById(payload),
order: selectItemsOrder(payload)
};
}
},
FETCH_TODOS: {
fetchItems: (type, payload) => dispatch => {
const adapter = new TodosAdapter();
(async () => {
try {
const items = await adapter.fetchTodos();
dispatch(ToDoActions.fetchTodosResult(items));
} catch (ex) {
dispatch(ToDoActions.fetchTodosError(ex));
}
})();
return {
type,
payload: true
}
},
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
loading: payload
};
}
},
CREATE_TODO_ERROR: {
createTodoError: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
error: payload
};
}
},
CREATE_TODO_RESULT: {
createTodoResult: (type, payload) => ({ type, payload }),
reducer: ({ loading, ...state}: any = {}, { type, payload }) => {
return {
...state,
items: {
...selectItems(state),
[payload.id]: payload
},
order: [payload.id, ...selectOrder(state)]
};
}
},
CREATE_TODO: {
createTodo: (type, title) => dispatch => {
const adapter = new TodosAdapter();
(async () => {
try {
const item = await adapter.createTodo(title);
dispatch(ToDoActions.createTodoResult(item));
} catch (ex) {
dispatch(ToDoActions.createTodoError(ex));
}
})();
return {
type,
payload: true
}
},
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
loading: payload
};
}
},
UPDATE_TODO_ERROR: {
updateTodoError: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
error: payload
};
}
},
UPDATE_TODO_RESULT: {
updateTodoResult: (type, payload) => ({ type, payload }),
reducer: ({ loading, ...state}: any = {}, { type, payload }) => {
return {
...state,
items: {
...selectItems(state),
[payload.id]: payload
}
};
}
},
UPDATE_TODO: {
updateTodo: (type, id, attrs) => dispatch => {
const adapter = new TodosAdapter();
(async () => {
try {
const item = await adapter.updateTodo(id, attrs);
dispatch(ToDoActions.updateTodoResult(item));
} catch (ex) {
dispatch(ToDoActions.updateTodoError(ex));
}
})();
return {
type,
payload: true
}
},
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
loading: payload
};
}
},
DELETE_TODO_ERROR: {
deleteTodoError: (type, payload) => ({ type, payload }),
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
error: payload
};
}
},
DELETE_TODO_RESULT: {
deleteTodoResult: (type, payload) => ({ type, payload }),
reducer: ({ loading, ...state }: any = {}, { type, payload: id }) => {
const { [id]: removed, ...items } = selectItemsInternal(state) as any;
return {
...state,
items,
order: selectOrder(state).filter(pos => pos !== id)
};
}
},
DELETE_TODO: {
deleteTodo: (type, id) => dispatch => {
const adapter = new TodosAdapter();
(async () => {
try {
const item = await adapter.deleteTodo(id);
dispatch(ToDoActions.deleteTodoResult(id));
} catch (ex) {
dispatch(ToDoActions.deleteTodoError(ex));
}
})();
return {
type,
payload: true
}
},
reducer: (state: {} = {}, { type, payload }) => {
return {
...state,
loading: payload
};
}
}
});
这些消息用于后端通信。它们可以分为命令:create
、update
、delete
。并按请求划分。启动远程请求的消息。从后端返回结果的消息。用于错误报告的消息。
所有这些都合并到这个最终的reducer中。
import { mainReducer, selectMain } from './components/main'; import { toDoReducer, selectTodos } from './models/todos'; import { itemReducer, selectCurrent } from './components/todoItem'; export const reducer = (prevState, action) => { const state = { ...prevState, main: mainReducer(selectMain(prevState), action), todos: toDoReducer(selectTodos(prevState), action), current: itemReducer(selectCurrent(prevState), action) }; const { type, payload } = action; switch (type) { default: return state; } };
在OOP中,这类信息通常组织在类方法和类成员中。这与此类似。借助declareActions
方法,所有内容都可以按单独的文件进行分组。这总是提醒我,我离OOP只有一步之遥。也许这是因为我无法足够地从OOP切换到函数式范式。CREATE_TODO
、UPDATE_TODO
和DELETE_TODO
——实际上应该更小。为了简洁起见,(async () => { ... })
已内联到消息中。
视图
全局状态的形状已调整。有关于如何更新状态的规则。现在是读取状态的部分。视图是第一个积极读取状态的参与者。选择要显示的状态的一部分。
模板
模板是在虚拟DOM的帮助下渲染的。由于它是VDOM,我们可以充分利用JSX/TSX模板技术。这将有助于生成虚拟DOM状态。渲染UI。有关JSX的更多信息可以在互联网上找到。在这里,我只列出将用于解决方案的模板。在本文中,“component
”一词用于通过connect
函数连接到全局状态的视图。而“control
”一词用于足以自给自足的简单视图。不连接到全局状态。
这是主组件的模板。
/** @jsx el */
import { el } from '../../virtualDom';
import { connect } from '../../connect';
import { className as cn } from '../../utils';
import { TodoListView } from '../../controls/todoListView';
import { TodoListViewItem } from '../todoItem';
import { selectMain } from '.';
import { Actions } from '../';
import { bindActions } from '../../bindActions';
const ENTER_KEY = 13;
function mapStateToProps(state, props: {
error;
showClearCompleted;
activeFilter;
hasTodos;
items;
completeItems;
activeItems;
totalText;
todoCount;
manyTasks;
}) {
const newState = {
...props,
...selectMain(state)
};
return {
...newState,
errors: {
main: newState.error,
todos: state.todos && state.todos.error
}
};
}
function mapDispatchToProps(dispatch, props) {
const actions = bindActions(Actions, dispatch);
return {
dispatch: dispatch,
onKeypress(evnt) {
if (evnt.which === ENTER_KEY) {
actions.uiCreateTodo();
}
},
...actions
};
}
export const MainView = connect(mapStateToProps, mapDispatchToProps)
(({ dispatch, ...props } = {
} as ReturnType<typeof mapDispatchToProps> & ReturnType<typeof mapStateToProps>) => <main>
<section className="todoapp device-content">
<header className="bar bar-nav">
<button className={cn('btn pull-left ?active', props.toggleAllComplete)}>
<input className="toggle-all hidden" id="toggle-all" type="checkbox"
defaultChecked={!!props.toggleAllComplete}
onClick={e => props.uiToggleAllComplete(e.target['checked'])}
/>
<label htmlFor="toggle-all">Complete All</label>
</button>
<button className={cn('clear-completed btn pull-right ?hidden',
props.showClearCompleted)}
onClick={e => props.uiClearCompleted()}
>Clear completed</button>
<hr/>
<div className="filters segmented-control">
<a className={cn("control-item ?active", !props.activeFilter)} href="#/"
onClick={evnt => props.uiSetActiveFilter('all')}
>All</a>
•
<a className={cn("control-item ?active",
props.activeFilter === 'active')} href="#/active"
onClick={evnt => props.uiSetActiveFilter('active')}
>Active</a>
•
<a className={cn("control-item ?active",
props.activeFilter === 'completed')} href="#/completed"
onClick={evnt => props.uiSetActiveFilter('complete')}
>Completed</a>
</div>
</header>
<hr/>
<section className="bar bar-standard bar-header-secondary">
<form onSubmit={e => e.preventDefault()}>
<input className="new-todo"
type="search" placeholder="What needs to be done?"
value={props.newTodoTitle}
onInput={e => props.uiUpdateNewTodoTitle(e.target['value'])}
onKeyPress={e => props.onKeypress(e)}
/>
</form>
</section>
<hr/>
<footer className={cn('footer bar bar-standard bar-footer ?hidden', props.hasTodos)}>
<span className="todo-count title">
<strong>{props.activeItems ? props.activeItems.length : 0}</strong>
{!(props.activeItems && props.activeItems.length === 1)
? <span className="items-word">items</span>
: <span className="item-word">item</span>
}
left from
<span className="total">{props.items ? props.items.length : 0}</span>
</span>
</footer>
<section className={cn('content ?hidden', props.hasTodos)}>
<TodoListView items={
props.activeFilter === 'complete'
? props.completeItems
: props.activeFilter === 'active'
? props.activeItems
: props.items
}>
{item => TodoListViewItem(item)}
</TodoListView>
<footer className="info content-padded">
<p>Click to edit a todo</p>
</footer>
</section>
</section>
</main>);
列表项组件的模板。
/** @jsx el */
import { el } from '../../virtualDom';
import { connect } from '../../connect';
import { className as cn } from '../../utils';
import { selectCurrent, selectTitle } from './';
import { Actions } from '../';
import { bindActions } from '../../bindActions';
const ENTER_KEY = 13;
export const ESC_KEY = 27;
function mapStateToProps(state, props) {
const newState = {
...props,
current: selectCurrent(state)
};
return {
...newState,
errors: {
main: newState.error,
todos: state.todos && state.todos.error
}
};
}
function mapDispatchToProps(dispatch, props) {
const actions = bindActions(Actions, dispatch);
return {
dispatch: dispatch,
...actions,
updateOnEnter(evnt, itemId) {
if (evnt.which === ENTER_KEY) {
actions.uiUpdateTodoTitle(itemId);
actions.uiSetCurrentItem(null);
}
},
revertOnEscape(e) {
if (e.which === ESC_KEY) {
actions.uiSetCurrentItem(null);
//this.cancelChanges();
}
}
};
}
// this part should be extracted into the separate file
// it has kept here just to make easy understand the implementation
function setCaretAtStartEnd(node, atEnd) {
const sel = document.getSelection();
node = node.firstChild;
if (sel.rangeCount) {
['Start', 'End'].forEach(pos =>
sel.getRangeAt(0)["set" + pos](node, atEnd ? node.length : 0)
)
}
}
// this part should be extracted into the separate file
// it has kept here just to make easy understand the implementation
function focusEditbox(el) {
el.focus();
if (el.textContent) {
setCaretAtStartEnd(el, true);
}
}
export const TodoListViewItem = connect(mapStateToProps, mapDispatchToProps)
(({ dispatch, ...props } = {
} as ReturnType<typeof mapDispatchToProps> &
ReturnType<typeof mapStateToProps>) => <div className={cn(
'table-view-cell', 'media', 'completed?', 'editing?', 'hidden?',
props.completed, props.current.id, props.hidden
)}>
<span className="media-object pull-left" style={"display: inline-block;" as any}>
<input id={`view-${props.id}`} className="hidden" type="checkbox"
checked={props.complete}
onChange={e => props.uiSetComplete(props.id, e.target['checked'])}
/>
</span>
<span className="input-group" style={"display: inline-block; width: 70%;" as any}>
{(props.current && props.current.id === props.id) ||
<label className="view input" style={"padding: 1px 1px 1px 1px;" as any}
onClick={e => props.uiSetCurrentItem(props.id)}
>{props.title}</label>}
{(props.current && props.current.id === props.id) &&
<div className="edit" style={"border: 1px solid grey;outline: none;" as any}
contentEditable={true}
ref={el => focusEditbox(el)}
onInput={e => props.uiUpdateCurrentTitle(e.target['innerText'])}
onKeyPress={e => props.updateOnEnter(e, props.current.id)}
onKeyUp={e => props.revertOnEscape(e)}
onBlur={e =>props.uiSetCurrentItem(null)}
>{props.current.title}</div>}
</span>
<button className="destroy btn icon icon-trash"
onClick={e => props.uiRemoveItem(props.id)}
style={"display: inline-block;" as any}
>Delete</button>
</div>
);
以及列表控件的模板。
/** @jsx el */
import { el } from '../virtualDom';
const map = (items, fn) => {
const res = [];
for (let key in items) {
if (items.hasOwnProperty(key)) {
res.push(fn(items[key], key));
}
}
return res;
}
export const TodoListView = ({ items = [], children = item => '' + item }:
{ items: [] | {}, children?}) => <ul style={"padding: 0px;" as any}>
{map(items, (item, index) => <li style={"list-style: none;" as any}>
{children(item, index)}</li>)}
</ul>;
最后是一个小的。它是列表视图的模板。类似于MVVM。使用相同的方法来构建无状态控件。它们用作简单控件来执行简单任务。在这种情况下,是列出待办事项。
除了JSX标记,模板中的重要部分是这些方法:mapStateToProps
和mapDispatchToProps
。因为它是虚拟DOM。它将频繁重新渲染。mapStateToProps
和mapDispatchToProps
中的所有内容都应谨慎编写。这是可能导致整个应用程序性能下降的第一个地方。即使UI中没有任何内容会更新,它也会在每次调度操作时重新渲染。这里使用的选择器应该快速。
选择器应该提供足够用于视图的状态部分。这很重要。当应用程序增长时。快速找出哪些数据部分被使用以及在哪个视图中会很有用。只需观察模板顶部即可。这里下一个重要的部分是函数bindActions
。该函数用于将每个动作绑定到状态中的dispatch函数。然后可以用于触发相关动作。
这是bindActions
函数的定义方式。
export function bindActions<A, K extends keyof A>(actions: A, dispatch) {
const res = Object.keys(actions).reduce((res, key) => ({
...res,
[key]: (...args) => dispatch(actions[key](...args))
}), {} as { [key in K]: A[K] });
return res;
}
这就是应用程序中所有动作如何合并到Actions
对象中的方式。
import { MainActions } from './main';
import { ItemActions } from './todoItem';
export const Actions = {
...MainActions,
...ItemActions
};
接下来是选择器。
选择符
选择器的概念已在许多文章中解释。我只想提一下,选择器是将全局状态切分成一小部分。选择器可以根据它们切分数据的方式进行组合。有些选择器倾向于深入全局状态并生成子状态片段。而另一些选择器则以组合方式工作。选择必要数据的一部分作为其他选择器的输入。数据片段的一部分将作为参数提供给构建最终sub
片段的其他选择器。
选择器不仅是视图的一部分。它们将在许多地方使用。
这是来自后端模块的选择器列表。
const selectItemsById = (items = []) => items.reduce((res, item) => ({
...res,
[item.id]: item
}), {});
const selectItemsOrder = (items = []) => items.map(({ id }) => id);
const selectOrder = ({ order }) => order;
export const selectTodos = ({ todos }) => todos;
export const selectItemsInternal = ({ items = {} }) => items;
export const selectItems = ({ items = {}, order = [] }) => order.map(id => items[id]);
主模块的选择器列表。
export const selectMain = ({ main = { newTodoTitle: '', toggleAllComplete: false } }) => main;
export const selectItems = ({ items = [] }) => items;
export const selectItemIsComplete = ({ complete }) => complete;
export const selectActiveItems =
(items = []) => pick(items, item => !selectItemIsComplete(item));
export const selectCompleteItems =
(items = []) => pick(items, item => selectItemIsComplete(item));
export const selectNewTodoTitle = ({ newTodoTitle = '' }) => newTodoTitle;
export const selectToggleAllComplete = ({ toggleAllComplete }) => toggleAllComplete;
列表项模块的选择器列表
export const selectCurrent = ({ current = {} }) => current;
export const selectTitle = ({ title }) => title;
export const selectId = ({ id }) => id;
export const selectComplete = ({ complete }) => complete;
总而言之,选择器类似于类中的getter。选择器会被频繁评估。应该快速。理想情况下,用于选择状态的一小部分。而reducer类似于类中的setter。setter应该很小。理想情况下用于一个任务:create
、update
、delete
。
效果
声明式开发中数据绑定的答案。这次来自函数式范式。
就像我们可以将事件附加到DOM文档元素一样。当光标移动、鼠标点击、键盘按下、UI按钮点击时,我们会被触发的大量事件淹没。rootEffect
函数也是如此。视图上的每个元素都可以触发消息。当应用程序很小时,它会产生少量消息。随着时间的推移,当视图变得高级时。更多动作将附加到元素事件上。消息的数量将增加。每条消息都将传递到存储。状态将更新。然后,相同的消息和新状态将作为参数传递给rootEffect
方法。现在这条消息将迎来新的生命。
让我们仔细看看列表项组件的效果。
export const currentItem = () => {
const fromView = merge(
pipe(
whenSetCurrentItem,
withArg(pipe(onState, queryTodos, queryItems)),
map(([{ payload: id }, items]: [any, any[]]) => {
const currentItem = items.find(item => item.id === id);
return ItemActions.setCurrent(currentItem);
})
),
pipe(
whenUpdateTodoTitle,
withArg(pipe(onState, queryCurrent, queryId),
pipe(onState, queryCurrent, queryTitle)),
map(([a, itemId, editTitle]) =>
ToDoActions.updateTodo(itemId, { title: editTitle }))
),
pipe(
whenSetComplete,
map(({ payload: { id, complete } }) => ToDoActions.updateTodo(id, { complete }))
),
pipe(
whenRemoveItem,
map(({ payload: id }) => ToDoActions.deleteTodo(id))
)
);
const fromService = merge(
pipe(
whenUpdateTodoResult,
map(() => ToDoActions.fetchItems())
)
);
return merge(
fromView,
fromService
);
}
它是一个复合函数,最终会生成另一个函数。这里有一个诀窍。整个currentItem
方法被声明为一个函数是有目的的。该函数将构建另一个函数。返回的函数将是rootEffect
函数的一部分。
让我们仔细看看rootEffect
函数是如何构建的。
import { main } from './main';
import { currentItem } from './todoItem';
import { merge } from '../itrx';
export const rootEffect = () => merge(
main(),
currentItem()
);
再说一次。它是一个构建另一个函数的函数。这是函数式编程中常见的做法。当大任务被分解成小操作时。有用于小操作的函数。还有一个复合函数。它是小函数的组合。用于执行更大的任务。
这是最终的handler
函数,它将执行一个大任务。它是effectsMiddleware
函数的一部分。
...
// creates the function with can be defined like that
// function handler(action, state, dispatch) { ... }
let handler = rootEffect();
...
这就是它的调用方式。
...
// in reality used two arguments. actions and store.getState()
// (a) => store.dispatch(a)) is never used. It is left here for playground.
for (const a of [].concat(...handler(action, store.getState(), (a) => store.dispatch(a)))) {
store.dispatch(a);
}
...
函数的结果是另一组动作。要分发到存储。视图被编程为分发动作。效果处理器也被编程为分发动作。它是以特殊方式编程的。
让我们尝试研究rootEffect
函数的生命周期。它像rootEffect()
这样创建。并像rootEffect()(<action>, <state>)
这样使用。它被分成两部分以提高性能。第一个初始化阶段只执行一次。当整个函数构建完成时。然后它会非常频繁地执行。在这种情况下,它将与初始化一起被调用。它将比实际需要走更长的路。
rootEffect
函数的内部组合可以分解为许多小任务。这是另一个效果。这次是针对main
组件的。
export const main = () => {
const init = merge(
pipe(
ofType('@INIT'),
map(() => ToDoActions.fetchItems())
)
);
const fromView = merge(
pipe(
whenCreateTodo,
withArg(pipe(onState, queryMain, queryNewTodoTitle)),
map(([a, newTodoTitle]) => ToDoActions.createTodo(newTodoTitle))
),
pipe(
whenUpdateNewTitle,
map(({ payload }) => MainActions.updateNewTodoTitle(payload))
),
pipe(
whenToggleAllComplete,
withArg(pipe(onState, queryTodos, queryItems),
pipe(onState, queryMain, queryToggleAllComplete)),
map(([, items, isCompleted]) => API.markAllItemsCompleted(items, isCompleted)),
map(completedItems => completedItems.map
(item => ToDoActions.updateTodo(item.id, item)))
),
pipe(
whenSetActiveFilter,
withArg(pipe(onState, queryTodos, queryItems)),
map(([a, todos]) => MainActions.updateItems(todos))
),
pipe(
whenClearCompleted,
withArg(pipe(onState, queryTodos, queryItems, queryCompleteItems)),
map(([a, completeItems = []]) => completeItems.map
(item => ToDoActions.deleteTodo(item.id)))
)
);
const fromService = merge(
pipe(
whenCreateTodoResult,
map(() => MainActions.updateNewTodoTitle(''))
),
pipe(
merge(whenCreateItem, whenDeleteItem),
map(() => ToDoActions.fetchItems())
),
pipe(
merge(whenChangeItems, whenCreateItem, whenDeleteItem),
withArg(pipe(onState, queryTodos, queryItems)),
map(([action, todos]) => {
return MainActions.updateItems(todos);
})
)
);
return merge(
init,
fromView,
fromService
);
}
它的工作方式如下:首先,它会运行初始请求,从后端获取项目。动作为ToDoActions.fetchItems()
。然后它会监听来自UI的更多消息。最后,它会处理内部命令。例如在创建新待办事项表单时重置待办事项标题。运行更多后端请求。刷新待办事项。使用从后端获取的待办事项更新UI。
有两种自定义运算符函数。有点像运算符方法。例如map
、pipe
、merge
、ofType
。作为构建更高级逻辑的基本块。以及定制的运算符函数。我们称它们为“query
”和“when
”函数。它们将与运算符函数一起使用。它们用于将小型逻辑组合到自定义运算符函数中。
“query
”函数基于选择器和map
运算符函数。“when
”函数基于消息类型和ofType
运算符函数。
这是所有“query
”和“when
”自定义运算符函数的列表。它们是构建效果的小型构建块。
// for the main module
const queryMain = map(selectMain);
const queryNewTodoTitle = map(selectNewTodoTitle);
const queryToggleAllComplete = map(selectToggleAllComplete);
const queryCompleteItems = map(selectCompleteItems);
const whenUpdateNewTitle = ofType(MainActionTypes.UI_UPDATE_NEW_TITLE);
const whenCreateTodo = ofType(MainActionTypes.UI_CREATE_TODO);
const whenToggleAllComplete = ofType(MainActionTypes.UI_TOGGLE_ALL_COMPLETE);
const whenSetActiveFilter = ofType(MainActionTypes.UI_SET_ACTIVE_FILTER);
const whenClearCompleted = ofType(MainActionTypes.UI_CLEAR_COMPLETED);
// for the todo item module
export const queryCurrent = map(selectCurrent);
export const queryTitle = map(selectTitle);
export const queryComplete = map(selectComplete);
export const queryId = map(selectId);
const whenSetCurrentItem = ofType(ItemActionTypes.UI_SET_CURRENT_ITEM);
const whenUpdateTodoTitle = ofType(ItemActionTypes.UI_UPDATE_TODO_TITLE);
const whenSetComplete = ofType(ItemActionTypes.UI_SET_COMPLETE);
const whenRemoveItem = ofType(ItemActionTypes.UI_REMOVE_ITEM);
// for the todo model
export const queryTodos = map(selectTodos);
export const queryItems = map(selectItems);
export const whenChangeItems = ofType(ToDoActionTypes.FETCH_TODOS_RESULT);
export const whenCreateItem = ofType(ToDoActionTypes.CREATE_TODO_RESULT);
export const whenDeleteItem = ofType(ToDoActionTypes.DELETE_TODO_RESULT);
export const whenCreateTodoResult = ofType(ToDoActionTypes.CREATE_TODO_RESULT);
export const whenUpdateTodoResult = ofType(ToDoActionTypes.UPDATE_TODO_RESULT);
让我们详细检查一下运算符函数。
export const onAction = (...abc) => [abc[0]];
export const onState = (...abc) => [abc[1]];
export const onDispatch = (...abc) => [abc[2]];
export const pipe = (...fn) => (...abc) =>
fn.reduce((res, curr) => res.length ? curr(...res) : [], abc);
export const merge = (...fn) => (...abc) =>
fn.reduce((res, curr) => [...res, ...curr(...abc)], []);
export const map = (fn) => (...abc) => [fn(...abc)];
export const withArg = (...fn) => (a, ...abc) => [[a, ...merge(...fn)(a, ...abc)], ...abc];
export const filter = (fn) => (...abc) => fn(...abc) ? [...abc] : [];
export const ofType = (type) => filter(a => a.type === type);
这是一个名为“itrx
”的小型库。它提供了执行小型操作的模块。类似于流。但这个解决方案适用于数组。
三个函数onAction
、onState
和onDispatch
。这些运算符函数提供actions
、state
和dispatch
参数。它们只选择传递给handler
(rootEffects()
的结果)函数的一个参数。
经常使用的第一个方法是pipe
。这是函数式编程中一种管道方法。经过了一些定制。因为它期望分发消息。结果可能为空、一条消息或多条消息。在这种情况下,所有运算符函数都经过调整,可以处理作为函数或数组的数据。它的定义如下。
请注意内联if
。它会一直工作到数组。并且数组不为空。结果可能提供一个空数组。然后管道将停止处理参数中的其余方法并返回结果。
export const pipe = (...args) =>
(...abc) => args.reduce((res, fn) => res.length ? fn(...res) : [], abc);
它将首先调用方法。评估它并将结果传递给下一个方法。像这样
...
pipe(
whenCreateTodo,
withArg(pipe(onState, queryMain, queryNewTodoTitle)),
map(([a, newTodoTitle]) => ToDoActions.createTodo(newTodoTitle))
),
...
首先将调用方法whenCreateTodo
。然后结果将传递给方法withArg
。最后,下一个结果将传递给map
。
merge
函数旨在应用...fn
中的每个函数,并传递相同的参数...abc
。它经过调整,以映射参数中提供的每个函数。然后将结果合并到单个数组中。
merge
函数的使用方式如下。
return merge(
init,
fromView,
fromService
);
通过引入merge
函数,它能够将来自一个组件的消息分组到一个单元中。
map
方法有助于选择最终使用的动作。类似于数组的map
方法。任何自定义逻辑都可以在那里使用。尽量保持其小巧。否则,它可能会产生大的副作用。而副作用越大,就越难理解它在做什么。类似于模板。
map(({ payload }) => MainActions.updateNewTodoTitle(payload))
最棘手的函数是withArg
。这是一个用于下一个管道函数的参数更改函数。让我们再次仔细看看它。
export const withArg = (...fn) => (a, ...abc) => [[a, ...merge(...fn)(a, ...abc)], ...abc];
这是它的使用方式。
withArg(pipe(onState, queryTodos, queryItems)),
map(([action, todos]) => {
return MainActions.updateItems(todos);
})
为了更好地理解,让我们看看另一个map
方法。这次没有更改参数。
map(action => ToDoActions.updateTodo(action.payload.id, action.payload.complete))
但是,默认情况下,action
、state
和dispatch
参数将传递给map
方法。这是一种通用的方法。通常,它需要构建基于状态一部分的逻辑。虽然,我们可以使用类似state.todo.items
这样的方式。这对于小型应用程序是可行的。将来,深入引用状态中嵌套属性可能会成为一个问题。
首先,通过点号请求嵌套状态是不安全的。某些部分可能具有null
值。这将导致错误,例如:
VM135419:1 Uncaught TypeError: Cannot read property 'items' of null
at <anonymous>:1:6
</anonymous>
第二。它会产生对状态的强耦合。状态结构的每次演变都会导致效果也更新。
有一个解决方案。选择器用于选取状态的一小部分。通常,当状态结构改变时,选择器也会改变。我们可以再次重用选择器。这次用于构建效果。使其变慢的是调整选择器的便捷方式。让选择器辅助构建效果。而withArg
就是答案。
让我们再检查一次。
withArg(pipe(onState, queryTodos, queryItems),
pipe(onState, queryMain, queryToggleAllComplete)),
其中queryTodos
和queryItems
查询方法通过管道连接。一旦评估,它们将提取状态的一小部分。queryMain
和queryToggleAllComplete
也是如此。状态的另一部分。最终,它将产生一组参数,如[items, isCompleted]
。对于map
运算符函数map(([, items, isCompleted]) => API.markAllItemsCompleted(items, isCompleted))
。采用这种方法。承认它会起作用是很不寻常的。一旦使用,它变得如此方便,以至于在构建效果时很难忽略它。
filter
函数将用于按动作类型选择消息。并跳过其余部分。类似于数组的filter
方法。在filter
函数的帮助下,可以构建“when
”运算符函数,例如:
// assuming that
export const ofType = (type) => filter(a => a.type === type);
// then we can build such operator as
const whenCreateTodoResult = ofType(ToDoActionTypes.CREATE_TODO_RESULT);
乍一看,许多小函数只是为了调用另一个函数,这看起来很奇怪。但再一想,这可以被视为一种新语言。它建立在编程语言之上。在本文中,它是TypeScript。用于函数式开发。另一种常见方法。当函数成为最小的简单构建块时。它将逐渐演变为另一种高阶语言。建立在编程语言的语法之上。拥有独特的语言生态系统。类似于在声明式世界中出现的框架。
至于效果,它可以被视为声明数据绑定的另一种方式。当绑定命令定义明确并构建为通用块时。每个人都会习惯它们。知道如何使用它们。效果也是如此。这次可以选择构建自己的构建命令。这将拥有独特的语言生态系统。只需记住一条规则。效果模块应该很小。由小巧易懂的结构组成。类似于事件监听器和事件处理器的处理方式。
细心的读者可能会注意到许多部分与React/Redux相似。然而,这里省略了这一点。原因是我试图专注于模型-视图-更新设计模式。React/Redux是实现它的一种可能方式。
关于MVU设计模式的结论
MVU设计模式是一种函数式方法。
优点
易于理解。最小的构建块是函数。
应用程序状态可以存储为一个全局状态。
数据流朝一个方向调整。从视图,通过动作,再到状态。
更新状态消息以顺序方式组织。更新状态时同步数据所需的代码更少。
对于每一种OOP方法,函数式世界都有一个答案。例如getter/setter和selector/reducer。数据绑定和效果。
能够产生独特的高阶语言。建立在编程语言之上。
后果
工具只是函数。导致过度使用高阶函数。
全局状态可能会增长。这可能导致状态的逻辑碎片化。
数据流的单向性可能导致频繁的额外更新,而UI上没有变化。
状态可能变得脆弱。如果错误地更新状态的一小部分,可能会破坏整个UI。
OOP已经有很多工具。编程语言语法应该支持函数式编程。
导致创建高阶语言。需要额外的时间来学习。
感谢您的阅读。本文中的代码材料基于互联网上可找到的资源。我的工作是将其整合起来。使其作为实验环境运行。希望它会有用。