Как вы храните состояние массива в функциональном JavaScript? - PullRequest
8 голосов
/ 05 апреля 2019

В последнее время я изучал некоторое функциональное программирование на 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", который сам по себе не является меняется (всегда пустой массив). Чтобы исправить это, я могу думать только о двух решениях: изменить исходный массив «items» или сохранить ссылку на текущий массив items, оба из которых включают функции, имеющие некоторые побочные эффекты.

Я пытался найти способы реализовать это, но безуспешно. Есть ли способ исправить это с помощью чистых функций?

Заранее спасибо.

1 Ответ

1 голос
/ 08 апреля 2019

Образец модели - ndash; контроллер используется для решения описанной проблемы состояния. Вместо того, чтобы писать длинную статью о MVC, я буду преподавать на демонстрации. Допустим, мы создаем простой список задач. Вот функции, которые мы хотим:

  1. Пользователь должен иметь возможность добавлять новые задачи в список.
  2. Пользователь должен иметь возможность удалять задачи из списка.

Итак, давайте углубимся. Начнем с создания модели. Наша модель будет машина Мура :

// 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 , чтобы исправить это.

Вы также можете посмотреть React и Redux . Тем не менее, я не большой поклонник этого, потому что:

  1. Они используют классы, что делает все многословным и неуклюжим. Хотя вы можете сделать функциональные компоненты, если действительно хотите.
  2. Они объединяют вид и контроллер, что является плохим дизайном. Мне нравится помещать модели, представления и контроллеры в отдельные каталоги, а затем объединять их все в третьем каталоге приложения.
  3. Redux, который используется для создания модели, является отдельной библиотекой от React, которая используется для создания контроллера представления - ndash; Но не торговец.
  4. Это излишне сложно.

Тем не менее, он хорошо протестирован и поддерживается Facebook. Следовательно, стоит посмотреть.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...