65.9K
CodeProject 正在变化。 阅读更多。
Home

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

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2020年7月25日

MIT

22分钟阅读

viewsIcon

12093

在本文中,我们将看到如何以函数式方式构建一个小型应用程序。

应用程序开发的世界广阔而丰富。有小块,有更大的复合抽象,不同的编程语言,编程语言的库,新趋势,时尚,被遗忘的宝藏,以及未被注意的沉默组件。

通过本文,我们将以函数式方式构建一个小型应用程序。可预测的状态、选择器、动作、高阶组件、副作用将是主要角色。本文的材料可用于学习MVU。它可以用作实验场所,并将挑战那些寻求函数式开发知识的人。它概述了函数式方法对所实现应用程序形态的影响。

我们仍在发明,再发明。发现并重新发现。对我来说,一切都始于WndProc的辉煌。Windows API中的消息分发系统。现在时尚正逐渐转向函数式开发。消息分发队列、可预测状态容器、单子函数是对时尚的回应。

当我知道我不知道我不知道什么时。

模型-视图-更新(MVU) - 现有、再发明、再发现

MVU是一种逐渐从函数式编程语言中演变出来的设计模式。在早期阶段,面向对象编程(OOP)迈出了第一步。现有开发人员在解决需求时使用了函数式范式、单子运算符抽象、函数式mapreduce运算符。让我们尝试遵循函数式软件路径,并像使用现代技术一样解决任务。有哪些限制?这些限制是真的吗?也许这是为了OOP时尚而做出的权衡。

MVU草案中的子层

由于MVU是另一种设计模式。与MVC、MVVM类似,它将拥有类似的高层生态系统,如数据传输、模型、视图层。关于这些层的更多信息可以在文章介绍MVVM架构中找到。在本文中,我们重点关注差异。视图层有不同的形态。因为我们的责任只是函数。应用程序的状态将频繁更新。只有部分状态更新与UI更改相关。应该有一个工具,只选择并更新实际更改的UI部分。其余的UI将保持不变。有一个现代概念来解决这个问题。它就是虚拟DOM。由于完整的vDom是一个巨大的任务。这里将只介绍一个小型版本。足以展示其通用思想。

虚拟DOM

它分为三个部分

  1. 模板 - 将保留描述视图形状的标记
  2. DOM - 用于处理浏览器元素
  3. 虚拟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状态。它接受两个参数:oldDomstateOldDom默认为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适配器。每个onclickonchange等事件都是消息。每个DOM元素属性都是状态的一部分。类似地,我们附加监听器到DOM元素并读取,例如,value属性。现在我们可以在rootEffect方法的帮助下做同样的事情。这在文章末尾有所描述。这次是关于消息和状态。每个元素都可以有许多事件。而DOM最终可以有许多元素。现在你可以想象通过这种设置可以产生多少种消息。我们可以以同样的方式考虑DOM元素属性。这是一个完美的输入系统。它允许随时以任何方式控制UI的任何部分。具有顺序更新的中央命令分发队列。类似于命令行。函数式宇宙刚刚诞生。它不仅可以显示,还可以监听和交谈。

动作消息和状态片段Reducer

到目前为止,应用程序是静默的。让我们打破沉默。应用程序更新状态和分发事件的形式在本文中得到了很好的描述:减少Redux样板的三种方法(并提高生产力)。简单的想法是将状态更新任务视为简单的update操作。然后所有reducer可以根据低级任务进行分离。有点像我们如何编程处理数据库表。Insertupdatedelete操作。

关于这个想法,它是在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
            };
        }
    }
});

这些消息用于后端通信。它们可以分为命令:createupdatedelete。并按请求划分。启动远程请求的消息。从后端返回结果的消息。用于错误报告的消息。

所有这些都合并到这个最终的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_TODOUPDATE_TODODELETE_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标记,模板中的重要部分是这些方法:mapStateToPropsmapDispatchToProps。因为它是虚拟DOM。它将频繁重新渲染。mapStateToPropsmapDispatchToProps中的所有内容都应谨慎编写。这是可能导致整个应用程序性能下降的第一个地方。即使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应该很小。理想情况下用于一个任务:createupdatedelete

效果

声明式开发中数据绑定的答案。这次来自函数式范式。

就像我们可以将事件附加到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。

有两种自定义运算符函数。有点像运算符方法。例如mappipemergeofType。作为构建更高级逻辑的基本块。以及定制的运算符函数。我们称它们为“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”的小型库。它提供了执行小型操作的模块。类似于流。但这个解决方案适用于数组。

三个函数onActiononStateonDispatch。这些运算符函数提供actionsstatedispatch参数。它们只选择传递给handlerrootEffects()的结果)函数的一个参数。

经常使用的第一个方法是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))

但是,默认情况下,actionstatedispatch参数将传递给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)),

其中queryTodosqueryItems查询方法通过管道连接。一旦评估,它们将提取状态的一小部分。queryMainqueryToggleAllComplete也是如此。状态的另一部分。最终,它将产生一组参数,如[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已经有很多工具。编程语言语法应该支持函数式编程。

导致创建高阶语言。需要额外的时间来学习。

感谢您的阅读。本文中的代码材料基于互联网上可找到的资源。我的工作是将其整合起来。使其作为实验环境运行。希望它会有用。

其中一些列表

© . All rights reserved.