发布于 2026-01-06 1 阅读
0

观察者模式 DEV 的全球展示挑战赛(由 Mux 呈现):展示你的项目!

使用观察者模式的待办事项列表

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

在这篇文章中,我们将通过创建一个简单的待办事项应用程序来学习观察者模式。

简而言之,观察者模式类似于 Twitter 的关注者功能。当你发布推文时,所有关注者都会收到通知,他们可以决定是否阅读你的推文。我们可以说,我们的关注者正在观察我们的推文。

观察者模式只有两个组成部分:主题观察者观察者只想知道我们何时更新主题,它们并不关心更新发生的具体时间。

回到我们之前提到的推特比喻,我们的推文就是主题,而我们的粉丝就是观察者。

那么,这和我们的待办事项应用有什么关系呢?我们会在开发应用的过程中找到答案,但首先,我们需要了解应用的功能。

  • 我们希望能够在待办事项列表中添加独特的任务。
  • 我们希望能够从待办事项列表中删除一项任务。
  • 我们希望在页面重新加载后保留列表。

让我们来创建待办事项应用程序的HTML代码。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Observer Pattern</title>
    </head>
    <body>
        <ul></ul>
        <form>
            <input required type="text" />
            <button type="submit">Add</button>
        </form>
    </body>
    <script>
    // We'll add all our code here
    </script>
</html>
Enter fullscreen mode Exit fullscreen mode

在这个 HTML 中,我们有一个无序列表元素,用于保存我们的待办事项;一个表单元素,用于向列表中添加待办事项;最后还有一个script元素,用于保存我们的 JavaScript 代码。

主题将用于存储待办事项。因此我们创建一个数组列表来存储待办事项。

<script>
    let todos = []; // Subject
</script>
Enter fullscreen mode Exit fullscreen mode

接下来,我们创建一个观察者列表。(将使用该列表的函数)

<script>
    let todos = []; // Subject
    let observers = [];
</script>
Enter fullscreen mode Exit fullscreen mode

接下来,我们实现添加待办事项的功能。每个待办事项都需要唯一标识,因此请为每个项目分配一个 ID。

const form = document.querySelector("form");
form.addEventListener('submit', (event) => {
    event.preventDefault();
    const input = form.elements[0];
    const item = {
        id: Date.now(),
        description: input.value,
    };
    addTodo(item);
    input.value = ''; // Clear text input
});

function addTodo(item) {
    todos.push(item);
}
Enter fullscreen mode Exit fullscreen mode

介绍我们的第一位观察员

当你尝试运行该应用程序时,你会发现屏幕上没有任何显示。这是因为我们还没有将todos数组连接到 HTML 无序列表元素。

我们的 HTMLul元素对数组很感兴趣todos。它想要观察数组列表,以便将其显示在屏幕上。因此,它需要扮演观察者的角色。让我们实现一个函数来显示我们的列表。

function displayTodos() {
    const ul = document.querySelector('ul');
    todos.forEach((todo) => {
        const li = document.createElement('li');
        li.innerText = todo.description;
        ul.appendChild(li);
    });
}
Enter fullscreen mode Exit fullscreen mode

现在,我们将此函数注册为观察者,方法是将其添加到我们的观察者列表中observers。为此,我们创建一个辅助函数来创建register新的观察者。

function registerObserver(observer) {
    // The observers array is basically an array of functions
    observers.push(observer);
}

registerObserver(displayTodos);
Enter fullscreen mode Exit fullscreen mode

尽管已注册为观察者,但没有任何内容显示。这是因为我们的todos数组尚未通知观察者。
我们创建一个notifyObservers函数,该函数将遍历observers数组并调用每个observer函数来检测是否发生了更新。

function notifyObservers() {
    observers.forEach((observer) => observer());
}
Enter fullscreen mode Exit fullscreen mode

然后,notifyObservers每当我们更改主题时,我们都会调用该函数。

function addTodo(item) {
    todos.push(item);
    notifyObservers(); // Add this line
}
Enter fullscreen mode Exit fullscreen mode

现在,在浏览器中运行该应用程序,即可看到待办事项被添加到列表中。

恭喜你发现了第一个 bug 🥳

您可能已经注意到,每次添加新项目时,列表都会翻倍。我们可以通过先清空列表来解决这个问题。

// Inside the displayTodos function

function displayTodos() {
    const ul = document.querySelector('ul');
    ul.innerHTML = ''; // Add this line
Enter fullscreen mode Exit fullscreen mode

现在“添加”功能已经实现,接下来需要删除待办事项。首先,我们button为每个li元素添加删除功能。

function displayTodos() {
    const ul = document.querySelector('ul');
    ul.innerHTML = '';
    todos.forEach((todo) => {
        const li = document.createElement('li');
        li.innerText = todo.description;

        // Add these lines
        const button = document.createElement('button');
        button.innerText = 'Remove';
        li.appendChild(button);

        ul.appendChild(li);
    });
}
Enter fullscreen mode Exit fullscreen mode

然后,我们创建一个removeTodo函数,用于按 ID 删除待办事项。

function removeTodo(id) {
    todos = todos.filter((todo) => todo.id !== id);
    notifyObservers();
}
Enter fullscreen mode Exit fullscreen mode

然后我们给click删除按钮附加一个事件监听器,该监听器将调用该removeTodo函数。

// Inside the displayTodos function

const button = document.createElement('button');
button.innerText = 'Remove';
// Attach an event listener here
button.addEventListener('click', () => {
   removeTodo(todo.id);
});
li.appendChild(button)
Enter fullscreen mode Exit fullscreen mode

引入第二位观察者

最后一步是将列表保存到本地存储中,并在页面重新加载时加载它。我们希望本地存储充当观察者,并在收到通知时保存列表。

function persistData() {
    localStorage.setItem("saved-todos", JSON.stringify(todos));
}

registerObserver(persistData);
Enter fullscreen mode Exit fullscreen mode

然后,我们在页面加载时加载已保存的待办事项。

function loadTodos(todoList) {
    todos = todoList;
    notifyObservers();
}

window.addEventListener("load", () => {
    const savedTodos = localStorage.getItem("saved-todos");
    if (savedTodos) {
        loadTodos(JSON.parse(savedTodos));
    }
});
Enter fullscreen mode Exit fullscreen mode

整洁代码

我们的代码可以运行,满足最低要求,但不够优雅。仔细观察你会发现,代码分为两类:一类是操作无序列表元素的,另一类是操作todos数组列表的。我们混用了 UI 逻辑和状态逻辑,这是代码混乱的典型特征。

首先,我们将状态逻辑封装在一个函数中,并将register`getState`、add`getState` remove、`getState` 和load`getState` 函数作为方法暴露给一个对象。这称为抽象。这样,
我们的todos数组对 UI 逻辑代码就不可见了。因此,我们创建了getTodos访问数组的方法todos。这称为封装,即隐藏内部状态并通过方法将其暴露出来的艺术。

function createSubject() {
    let todos = [];
    let observers = [];

    function registerObserver(observer) {
      observers.push(observer);
    }

    function notifyObservers() {
      observers.forEach((observer) => observer());
    }

    function addTodo(item) {
        todos.push(item);
        notifyObservers();
    }

    function removeTodo(id) {
      todos = todos.filter((todo) => todo.id !== id);
      notifyObservers();
    }

    function loadTodos(todoList) {
        todos = todoList;
        notifyObservers();
    }

    function getState() {
        return todos;
    }

    return {
        registerObserver,
        addTodo,
        removeTodo,
        loadTodos,
        getState,
    }
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们使用它createSubject来创建一个待办事项主题。

const subject = createSubject();

function displayTodos() {
    const ul = document.querySelector("ul");
    ul.innerHTML = "";
    todos.forEach((todo) => {
        const li = document.createElement("li");
        li.innerText = todo.description;

        const button = document.createElement("button");
        button.innerText = "Remove";
        button.addEventListener("click", () => {
            subject.removeTodo(todo.id);
        });
        li.appendChild(button);

        ul.appendChild(li);
    });
}

subject.registerObserver(displayTodos)

subject.registerObserver(() => {
    localStorage.setItem("saved-todos", JSON.stringify(todos));
});

window.addEventListener("load", () => {
    const savedTodos = localStorage.getItem("saved-todos");
    if (savedTodos) {
        subject.loadTodos(JSON.parse(savedTodos));
    }

    const form = document.querySelector("form");
    form.addEventListener("submit", (event) => {
        event.preventDefault();
        const input = form.elements[0];
        const item = {
            id: Date.now(),
            description: input.value,
        };
        subject.addTodo(item);
        input.value = "";
    });
});
Enter fullscreen mode Exit fullscreen mode

createSubject函数遵循观察者模式设计。我们通过注册为观察者来订阅待办事项。如果我们不再想接收通知怎么办?
很简单。我们可以在registerObserver方法中返回一个函数。

function registerObserver(observer) {
    observers.push(observer);

    return function () {
        observers = observers.filter((currentObserver) => !== observer);
    }
}
Enter fullscreen mode Exit fullscreen mode

然后,我们可以保存注册后的返回值,并在以后调用它来取消注册。

const unregisterDisplayTodos = subject.registerObserver(displayTodos)

// later when we want to unregister
unregisterDisplayTodos(); // displayTodos will no longer be notified
Enter fullscreen mode Exit fullscreen mode

Redux 是一个流行的 JavaScript 库,它使用了观察者模式。在下一篇文章中,我们将通过创建我们自己的小型 Redux 库来揭开 Redux 的神秘面纱。

祝您编程愉快!

文章来源:https://dev.to/devusman/to-do-list-with-observer-pattern-1cl7