如何在功能性JavaScript中存储数组的状态?

时间:2019-04-05 15:27:53

标签: javascript functional-programming

我最近一直在学习一些使用JavaScript的函数式编程,并且想通过编写一个仅包含函数式编程的简单ToDo应用程序来对我的知识进行测试。但是,我不确定如何以一种纯函数的方式存储列表状态,因为不允许函数产生副作用。让我用一个例子来解释。

比方说,我有一个名为“ Item”的构造函数,该构造函数仅具有要完成的任务,还有一个用于标识该项目的uuid。我还有一个items数组,其中包含所有当前项目,以及一个“ add”和“ delete”函数,如下所示:

function Item(name){
    this.name = name;
    this.uuid = uuid(); //uuid is a function that returns a new uuid
}

const items = [];

function addItem(name){
    const newItem = new Item(name);
    items.push(newItem);
}

function deleteItem(uuid){
    const filteredItems = items.filter(item => item.uuid !== uuid);
    items = filteredItems
}

现在这可以正常工作,但是如您所见,函数不是纯粹的:它们确实有副作用,不会返回任何东西。考虑到这一点,我尝试使其功能如下:

function Item(name){
    this.name = name;
    this.uuid = uuid(); //uuid is a function that returns a new uuid
}

const items = [];

function addItem(array, constructor, name){
    const newItem = new constructor(name);
    return array.concat(newItem);
}

function removeItem(array, uuid){
    return array.filter(item => item.uuid !== uuid);
}

现在函数是纯函数(或者,我想,如果我错了,请纠正我),但是为了存储项目列表,每次添加或删除项目时,我都需要创建一个新数组。这不仅效率低下,而且我也不知道如何正确实现它。假设我想每次在DOM中按下按钮时向列表中添加一个新项目:

const button = document.querySelector("#button") //button selector
button.addEventListener("click", buttonClicked)

function buttonClicked(){
    const name = document.querySelector("#name").value
    const newListOfItems = addItem(items, Item, name);
}

这又不是纯粹的功能,但是还有另一个问题:这将无法正常工作,因为每次调用该函数时,它将使用现有的“ items”数组创建一个新数组,而这本身并不是变化(总是一个空数组)。要解决此问题,我只能想到两种解决方案:修改原始的“项目”数组或存储对当前项目数组的引用,这两种方法都涉及具有某种副作用的功能。

我试图寻找实现此目标的方法,但没有成功。有什么办法可以使用纯函数来解决此问题?

谢谢。

1 个答案:

答案 0 :(得分:1)

model–view–controller模式用于解决您描述的状态问题。我不会写冗长的有关MVC的文章,而是通过演示进行教学。假设我们正在创建一个简单的任务列表。这是我们想要的功能:

  1. 用户应该能够将新任务添加到列表中。
  2. 用户应该能够从列表中删除任务。

因此,让我们开始吧。我们将从创建模型开始。我们的模型将是Moore machine

// The arguments of createModel are the state of the Moore machine.
//                    |
//                    v
const createModel = tasks => ({
    // addTask and deleteTask are the transition functions of the Moore machine.
    // They return new updated Moore machines and are purely functional.
    addTask(task) {
        if (tasks.includes(task)) return this;
        const newTasks = tasks.concat([task]);
        return createModel(newTasks);
    },
    deleteTask(someTask) {
        const newTasks = tasks.filter(task => task !== someTask);
        return createModel(newTasks);
    },
    // Getter functions are the outputs of the Moore machine.
    // Unlike the above transition functions they can return anything.
    get tasks() {
        return tasks;
    }
});

const initialModel = createModel([]); // initially the task list is empty

接下来,我们将创建视图,该视图是一个函数,给定模型的输出返回DOM列表:

// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
    const input = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("id", "newTask");

    const button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "Add Task");
    button.setAttribute("id", "addTask");

    const list = document.createElement("ul");

    for (const task of tasks) {
        const item = document.createElement("li");

        const span = document.createElement("span");
        span.textContent = task;

        const remove = document.createElement("input");
        remove.setAttribute("type", "button");
        remove.setAttribute("value", "Delete Task");
        remove.setAttribute("class", "remove");
        remove.setAttribute("data-task", task);

        item.appendChild(span);
        item.appendChild(remove);

        list.appendChild(item);
    }

    return [input, button, list];
};

最后,我们创建连接模型和视图的控制器:

const controller = model => {
    const app = document.getElementById("app"); // the place we'll display our app

    while (app.firstChild) app.removeChild(app.firstChild); // remove all children

    for (const element of createView(model)) app.appendChild(element);

    const newTask = app.querySelector("#newTask");
    const addTask = app.querySelector("#addTask");
    const buttons = app.querySelectorAll(".remove");

    addTask.addEventListener("click", () => {
        const task = newTask.value;
        if (task === "") return;
        const newModel = model.addTask(task);
        controller(newModel);
    });

    for (const button of buttons) {
        button.addEventListener("click", () => {
            const task = button.getAttribute("data-task");
            const newModel = model.deleteTask(task);
            controller(newModel);
        });
    }
};

controller(initialModel); // start the app

将它们放在一起:

// The arguments of createModel are the state of the Moore machine.
//                    |
//                    v
const createModel = tasks => ({
    // addTask and deleteTask are the transition functions of the Moore machine.
    // They return new updated Moore machines and are purely functional.
    addTask(task) {
        if (tasks.includes(task)) return this;
        const newTasks = tasks.concat([task]);
        return createModel(newTasks);
    },
    deleteTask(someTask) {
        const newTasks = tasks.filter(task => task !== someTask);
        return createModel(newTasks);
    },
    // Getter functions are the outputs of the Moore machine.
    // Unlike the above transition functions they can return anything.
    get tasks() {
        return tasks;
    }
});

const initialModel = createModel([]); // initially the task list is empty

// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
    const input = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("id", "newTask");

    const button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "Add Task");
    button.setAttribute("id", "addTask");

    const list = document.createElement("ul");

    for (const task of tasks) {
        const item = document.createElement("li");

        const span = document.createElement("span");
        span.textContent = task;

        const remove = document.createElement("input");
        remove.setAttribute("type", "button");
        remove.setAttribute("value", "Delete Task");
        remove.setAttribute("class", "remove");
        remove.setAttribute("data-task", task);

        item.appendChild(span);
        item.appendChild(remove);

        list.appendChild(item);
    }

    return [input, button, list];
};

const controller = model => {
    const app = document.getElementById("app"); // the place we'll display our app

    while (app.firstChild) app.removeChild(app.firstChild); // remove all children

    for (const element of createView(model)) app.appendChild(element);

    const newTask = app.querySelector("#newTask");
    const addTask = app.querySelector("#addTask");
    const buttons = app.querySelectorAll(".remove");

    addTask.addEventListener("click", () => {
        const task = newTask.value;
        if (task === "") return;
        const newModel = model.addTask(task);
        controller(newModel);
    });

    for (const button of buttons) {
        button.addEventListener("click", () => {
            const task = button.getAttribute("data-task");
            const newModel = model.deleteTask(task);
            controller(newModel);
        });
    }
};

controller(initialModel); // start the app
<div id="app"></div>

当然,这不是很有效,因为每次更新模型时都要更新整个DOM。但是,您可以使用virtual-dom之类的库来解决此问题。

您还可以查看ReactRedux。但是,我不喜欢它,因为:

  1. 他们使用类,这使所有内容变得冗长而笨拙。不过,如果您确实愿意,可以制作功能组件。
  2. 它们将视图和控制器结合在一起,这是糟糕的设计。我喜欢将模型,视图和控制器放在单独的目录中,然后将它们全部合并到第三个应用程序目录中。
  3. 用于创建模型的Redux是与React分离的独立库,用于创建视图控制器。虽然不是一个破坏交易的人。
  4. 它不必要地复杂。

但是,它已经过Facebook的严格测试和支持。因此,值得一看。