使用观察者模式的待办事项列表
由 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>
在这个 HTML 中,我们有一个无序列表元素,用于保存我们的待办事项;一个表单元素,用于向列表中添加待办事项;最后还有一个script元素,用于保存我们的 JavaScript 代码。
主题将用于存储待办事项。因此,我们创建一个数组列表来存储待办事项。
<script>
let todos = []; // Subject
</script>
接下来,我们创建一个观察者列表。(将使用该列表的函数)
<script>
let todos = []; // Subject
let observers = [];
</script>
接下来,我们实现添加待办事项的功能。每个待办事项都需要唯一标识,因此请为每个项目分配一个 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);
}
介绍我们的第一位观察员
当你尝试运行该应用程序时,你会发现屏幕上没有任何显示。这是因为我们还没有将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);
});
}
现在,我们将此函数注册为观察者,方法是将其添加到我们的观察者列表中observers。为此,我们创建一个辅助函数来创建register新的观察者。
function registerObserver(observer) {
// The observers array is basically an array of functions
observers.push(observer);
}
registerObserver(displayTodos);
尽管已注册为观察者,但没有任何内容显示。这是因为我们的todos数组尚未通知观察者。
我们创建一个notifyObservers函数,该函数将遍历observers数组并调用每个observer函数来检测是否发生了更新。
function notifyObservers() {
observers.forEach((observer) => observer());
}
然后,notifyObservers每当我们更改主题时,我们都会调用该函数。
function addTodo(item) {
todos.push(item);
notifyObservers(); // Add this line
}
现在,在浏览器中运行该应用程序,即可看到待办事项被添加到列表中。
恭喜你发现了第一个 bug 🥳
您可能已经注意到,每次添加新项目时,列表都会翻倍。我们可以通过先清空列表来解决这个问题。
// Inside the displayTodos function
function displayTodos() {
const ul = document.querySelector('ul');
ul.innerHTML = ''; // Add this line
现在“添加”功能已经实现,接下来需要删除待办事项。首先,我们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);
});
}
然后,我们创建一个removeTodo函数,用于按 ID 删除待办事项。
function removeTodo(id) {
todos = todos.filter((todo) => todo.id !== id);
notifyObservers();
}
然后我们给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)
引入第二位观察者
最后一步是将列表保存到本地存储中,并在页面重新加载时加载它。我们希望本地存储充当观察者,并在收到通知时保存列表。
function persistData() {
localStorage.setItem("saved-todos", JSON.stringify(todos));
}
registerObserver(persistData);
然后,我们在页面加载时加载已保存的待办事项。
function loadTodos(todoList) {
todos = todoList;
notifyObservers();
}
window.addEventListener("load", () => {
const savedTodos = localStorage.getItem("saved-todos");
if (savedTodos) {
loadTodos(JSON.parse(savedTodos));
}
});
整洁代码
我们的代码可以运行,满足最低要求,但不够优雅。仔细观察你会发现,代码分为两类:一类是操作无序列表元素的,另一类是操作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,
}
}
接下来,我们使用它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 = "";
});
});
该createSubject函数遵循观察者模式设计。我们通过注册为观察者来订阅待办事项。如果我们不再想接收通知怎么办?
很简单。我们可以在registerObserver方法中返回一个函数。
function registerObserver(observer) {
observers.push(observer);
return function () {
observers = observers.filter((currentObserver) => !== observer);
}
}
然后,我们可以保存注册后的返回值,并在以后调用它来取消注册。
const unregisterDisplayTodos = subject.registerObserver(displayTodos)
// later when we want to unregister
unregisterDisplayTodos(); // displayTodos will no longer be notified
鳍
Redux 是一个流行的 JavaScript 库,它使用了观察者模式。在下一篇文章中,我们将通过创建我们自己的小型 Redux 库来揭开 Redux 的神秘面纱。
祝您编程愉快!
文章来源:https://dev.to/devusman/to-do-list-with-observer-pattern-1cl7