Цель этой статьи — помочь каждому постепенно понять общие идеи шаблонов проектирования через некоторые знакомые сцены в работе, чтобы каждый больше не чувствовал, что шаблоны проектирования трудно понять или даже трудно начать. Я уверен, что после прочтения этой статьи у вас будет четкое представление о шаблонах проектирования.
Зачем изучать шаблоны проектирования
Многие говорят: «Я работаю уже 3 года, а шаблоны проектирования так и не изучил, и это никак не влияет на мой кодинг», или «Я несколько раз читал теорию шаблонов проектирования, но могу не использовать их на работе». Для шаблонов проектирования всегда было сказано, что доброжелательный видит доброжелательного, а мудрый видит мудрого. Вот несколько причин, по которым вам следует изучать шаблоны проектирования:
- Улучшите свой код. Шаблоны проектирования следуют принципам единой ответственности, возможности повторного использования и наименьших знаний.Они могут помочь вам повысить удобочитаемость и возможность повторного использования кода в начале разработки кода, а также сыграть очень важную роль в рефакторинге кода. может помочь нам избавиться от утомительного
if...else...
Написав, вы можете прочитать статьи, которые я написал раньшеОптимизация кода проекта Оптимизация альтернативной формулировки - ИЗ-другому - Решить существующие проблемы. Шаблоны проектирования — это решения, предложенные предшественниками для некоторых конкретных проблем или сценариев в процессе проектирования и разработки программного обеспечения. Когда вы сталкиваетесь с подобными сценариями в процессе разработки, вы можете выбрать соответствующий шаблон проектирования в качестве одного из решений проблемы.
- Улучшите свою мягкую силу. Знание шаблонов проектирования может выделить вас на собеседовании среди многих интервьюеров и заставить по-другому мыслить на работе...
Шаблоны проектирования
GoF
Предлагается 23 шаблона проектирования, ноJavaScript
В отличие от других строго типизированных языков, традиционные шаблоны проектирования не применимы в полной мере, поэтому многие люди отказываются изучать шаблоны проектирования или считают, что шаблоны проектирования горьки и трудны для понимания.Здесь, в сочетании с нашей работой, мы только знакомимJavaScript
Несколько распространенных шаблонов проектирования.
Хотя каждый из этих шаблонов проектирования имеет свои особенности, имейте в виду, что все они одинаковы по своей сути.пакетные изменения. Найдите измененные части кода и инкапсулируйте изменения, чтобы сделать наш код более гибким и пригодным для повторного использования.
1. Одноэлементный шаблон
определение: гарантирует наличие только одного экземпляра класса и предоставляет к нему глобальную точку доступа.
Уникальностьwindow
Давайте сначала интерпретируем предложение «у класса имеет только один экземпляр» в определении. Мы все знаем, что когда мы создаем класс, мы можем пройтиnew
Ключевое слово генерирует новый объект (то есть экземпляр) С другой точки зрения, это означает, что класс в шаблоне singleton может бытьnew
Ключевое слово создается несколько раз и должно возвращать экземпляр объекта, который был создан в первый раз.
С этой идеей следующий код используется для реализации одноременового варианта осуществления:
class Singleton{
static getInstance(){
// 判断是否创建过实例了
if(!Singleton.instance){
// 如果没有创建过,则生成一个新的实例
Singleton.instance = new Singleton()
}
// 如果创建过,则返回该实例
return Singleton.instance
}
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1===s2) // true
В приведенном выше примере мы используемSingleton.instance
сохранитьSingleton
Созданный экземпляр, если экземпляр не был создан, создается новый экземпляр, мы видим, что независимо от того, сколько раз он выполняетсяSingleton.getInstance()
, возвращаемые результаты — это все экземпляры, сгенерированные впервые.
существуетJavaScript
мы редко используемclass
При таком способе написания нельзя использовать одноэлементный шаблон? Существует не только один способ реализации одноэлементного шаблона, мы также можем использовать замыкания для достижения:
Singleton.getInstance = (function() {
// 定义自由变量instance,模拟私有变量
let instance = null
return function() {
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new Singleton()
}
return instance
}
})()
Ядро одноэлементного паттерназаключается в том, чтобы обеспечить наличие только одного экземпляра и предоставить глобальный доступ, давайте рассмотрим два примера общих одноэлементных шаблонов в работе, чтобы помочь нам лучше понять одноэлементный шаблон.
Одноэлементный шаблон в vuex
vuex
даvue
Государственный управляющий,vuex
серединаstore
store
vuex
install
方法,这个方法在插件安装时被调用。 существуетinstall
В методе есть кусочек логики и то, что мы написали вышеgetInstance
Метод очень похож:
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件
if (Vue && _Vue === Vue) {
console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.')
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
}
Таким образом, гарантируется, что приложение будетinstall
однаждыVuex
Плагин, так каждыйVue
Экземпляры будут иметь только глобальныйStore
.
диалоговое/модальное всплывающее окно
На работе мы часто сталкиваемся со сценариями, в которых мы пишем модальные всплывающие окна (всплывающие диалоговые окна похожи), такие как наши всплывающие окна для входа. Особенности всплывающего окна входа:
- Он уникален на странице, одновременно не может быть двух всплывающих окон для входа.
- Когда пользователь нажимает кнопку в первый раз, создается всплывающее окно, при повторном нажатии кнопки новое всплывающее окно не создается, а отображается ранее созданное всплывающее окно.
В соответствии с этим мы можем реализовать демонстрацию всплывающего окна входа в систему:
<html>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
const createLoginLayer = (function () {
var div;
return function () {
if (!div) {
div = document.createElement('div');
div.innerHTML = '我是登录弹窗';
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById('loginBtn').onclick = function () {
const loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
</script>
</html>
2. Режим итератора
определение: относится к предоставлению способа последовательного доступа к элементам агрегатного объекта без раскрытия внутреннего представления объекта.
Определения могут быть не очень хорошими, но мы можем начать с имени. Итератор построен на многих языках, таких как JS в итератор средней группы -Array.prototype.forEach
[1, 2, 3].forEach((item, index) => {
console.log(`当前元素${item}的下标为${index}`)
})
Array.prototype.forEach
forEach
реализовать итератор
Мы можем реализовать итератор самиeach
:
const each = function (ary, callback) {
for (let i = 0, l = ary.length; i < l; i++) {
callback.call(ary[i], i, ary[i]); // 把下标和元素当作参数传给 callback 函数
}
};
each([1, 2, 3], function (i, n) {
console.log([i, n]);
});
Конечно, использование итераторов более того, в конце эталонного процента рекомендовала чтение текстовых книг и статей.
3. Шаблон декоратора
определение: Динамически добавлять новые поведения на объекты.
Приближается Рождество, многие друзья украсят елку, мы повесим на елку много праздничных украшений, но мы не будем разрушать первоначальную структуру елки, это модель декоратора в нашей жизни.
функция декоратора
Хорошее представление шаблона декоратора в JavaScript:функция декоратора. Например, когда мы поддерживаем проект, внезапно появляются новые требования, и нам нужно добавить новые функции к исходным функциям. Оригинальная функция была написана бывшим коллегой и прошла через руки нескольких человек Реализация внутри очень грязная Лучше всего попробовать не менять исходную функцию, а переписать функцию, сохранив оригинальную ссылку.
Например, мы хотим датьwindow
связыватьonload
событие, но если кто-то уже связывал это событие ранее, мы перезапишем поведение в предыдущем событии, если напишем его напрямую. Во избежание перезаписи предыдущегоwindow.onload
Поведение в функции, мы обычно сначала сохраняем оригиналwindow.onload
, поместите его в новый window.onload
Выполнить через:
window.onload = function () {
console('原先执行的行为');
};
const _onload = window.onload || function () {};
window.onload = function () {
_onload();
console.log('添加新行为');
};
новыйwindow.onload
Функции — наши декораторы.
Чтобы добавить новые функции в динамическое поведение
- должны поддерживаться
_onload
Эта промежуточная переменная, если цепочка оформления функции длиннее, требуется больше промежуточных переменных. -
this
указывает на проблему, когда функция вызывается как метод объекта,this
указывает на объект (приведенный выше пример не имеет этой проблемы, но мы должны учитывать, что исходная реализация функции полезна дляthis
Случай).
Для решения этих двух задач мы используемФункции высшего порядкадля украшения функции.
Мы добавляем новое поведение к исходной функции, и добавленное новое поведение выполняется до или после функции. Мы реализуем два метода -Function.prototype.before
добавить новое поведение к функции, которая предшествует выполнению функции,Function.prototype.after
Добавьте новое поведение в функцию после ее выполнения:
Function.prototype.before = function (beforefn) {
var __self = this; // 保存原函数的引用
return function () {
// 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
};
};
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};
вернуться к вершинеwindow.onload
например, мы можем написать:
window.onload = function () {
console('原先执行的行为');
};
window.onload = (window.onload || function () {}).after(function () {
console.log('添加新行为');
}).after(function () {
console.log('继续添加其他新行为');
});
Событие Похороны
Если мы добавим скрытое событие к кнопке поиска, нам нужно сделать две вещи: во-первых, реализовать функцию поиска, а во-вторых, сообщить данные.
// 普通实现
const btnCLick = () => {
console.log("搜索功能");
console.log("上报数据");
};
// 装饰器模式实现
const search = () => {
console.log("搜索功能");
};
const sendData = () => {
console.log("上报数据");
};
const btnCLick = search.after(sendData);
Во втором способе реализации (реализация шаблона декоратора) мы сделали более детальное разделение обязанностей кнопок, чтобы обеспечить принцип единой ответственности функций.
запрос axios плюс параметр токена
Ниже приведен запрос axios, который мы инкапсулировали.
var request = function(url, type, data){
console.log(data);
// 具体代码略
};
Теперь вам нужно добавить к каждому запросуtoken
параметр
// 普通实现
var request = function(url, type, data = {}){
data.token = getToken();
console.log(data);
};
// 装饰器模式实现
var request = function(url, type, data = {}){
console.log(data);
};
request = request.before((url, type, data = {})=>{
data.token = getToken();
})
В реализации рисунка декоратора мы не изменили оригинальную функцию. Оригинальная функция является относительно чистой функцией односторонней ответственности, что улучшаетrequest
Повторное использование функций.
определение
Два примера подробно описаны ниже.
агент событий
Делегация событий - это ситуация, которую мы часто сталкиваемся, например, мы хотим нажать на каждыйli
Когда этикетка напечатана, щелкнувшисьli
Содержимое тега, если бы мы не использовали делегат события, было бы написано так:
<html>
<body>
<ul id="parent">
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
<li>列表项4</li>
<li>列表项5</li>
</ul>
<script>
const aNodes = document.getElementsByTagName('li')
const aLength = aNodes.length
for (let i = 0; i < aLength; i++) {
aNodes[i].addEventListener('click', function (e) {
e.preventDefault()
alert(`我是${aNodes[i].innerText}`)
})
}
</script>
</body>
</html>
Итак, мы даем 5li
Элементы добавили события прослушивателя, но в реальном проекте много элементов списка, и накладные расходы производительности на добавление событий прослушивателя к каждому элементу списка очень велики.
Далее, учитывая, что само событие имеет характеристики всплытия, мы можем использовать прокси-режим для реализации мониторинга событий для дочерних элементов.Нам нужно только добавить события прослушивания к родительскому элементу:
const element = document.getElementById('parent');
element.addEventListener('click', (e) => {
const target = e.target;
console.log(target.innerHTML);
})
const getData = (function() {
const cache = {};
return function(url) {
if (cache[url]) {
return Promise.resolve(cache[url]);
}
return $.ajax.get(url).then((res) => {
cache[url] = res;
return res;
}).catch(err => console.error(err))
}
})();
getData('/getData'); // 发起http请求
getData('/getData'); // 返回缓存数据
определение
Есть много примеров адаптеров в нашей жизни. Например, разъем для наушников моего iPhone 13 имеет квадратную голову. Если вы хотите использовать круглые наушники, вы должны добавить адаптер для его использования. Один конец адаптера Круглое отверстие. Наушники круглой головки подключены, а квадратная головка подключается к моим телефонам на другом конце, поэтому я могу использовать наушники счастливыми. Адаптер здесь действует как адаптер.
Таким же образом, мы также будем использовать идею шаблона адаптера при разработке программ.
Совместимый формат данных интерфейса
Режим адаптера часто используется в нашей повседневной работе, например, мы используемEcharts
Когда мы рисуем, мы все знаем,Echarts
серединаdata
Данные имеют определенный формат данных, например, когда мы рисуем круговую диаграмму, требуемый формат данных следующий:
data = [
{
name: "2020",
value: 14800,
},
{
name: "2021",
value: 23400,
},
];
И формат данных, которые мы получаем из внутреннего интерфейса, может быть таким:
data = {
2020: 14800,
2021: 23400,
};
давай менятьсяEcharts
Исходный код, чтобы соответствовать нашему формату данных, очевидно, очень нереалистичен, на этот раз нам нужно получить адаптер, чтобы преобразовать нас из внутреннего формата данных в формат данных, который может быть получен круговой диаграммой:
function adpter(data) {
const result = [];
for (let key in data) {
result.push({
name: key,
value: data[key],
});
}
return result;
}
Кроссбраузерная совместимость
Мы все знаем, что прослушиватели событий имеют проблемы с совместимостью в браузерах, поэтому jQuery
упакованный$('selector').on
Адаптер обработки событий используется для решения проблем межбраузерной совместимости.
function on(target, event, callback) {
if (target.addEventListener) {
// 标准事件监听
target.addEventListener(event, callback);
} else if (target.attachEvent) {
// IE低版本事件监听
target.attachEvent(event, callback)
} else {
// 低版本浏览器事件监听
target[`on${event}`] = callback
}
}
Разница между режимом декоратора и режимом прокси
Все три режима являются режимами-оболочками, которые не изменяют существующий интерфейс, главное отличие заключается в их назначении.
модель | намерение | |
---|---|---|
шаблон декоратора | ||
прокси-режим | Для контроля доступа к объектам | |
режим адаптера | В основном решить проблему несоответствия между двумя существующими интерфейсами | Обычно упаковывается только один раз |
6. Режим стратегии
определение: Определите набор алгоритмов, один инкапсулируют их и делают их взаимозаменяемыми.
Например, если мы хотим отсортировать набор данных от маленького к большому, у нас есть много алгоритмов на выбор: пузырьковая сортировка, быстрая сортировка или сортировка кучи. Вы можете выбрать алгоритм сортировки в соответствии с вашими предпочтениями, или вы можете выбрать наиболее подходящий алгоритм сортировки, наблюдая за характеристиками данных, Это режим стратегии, о котором мы хотим поговорить.
Алгоритм вот обобщенный «алгоритм», который может быть серией «Бизнес-правил», инкапсулированных.
повторное использование страницы
Мы сделали несколько проектов, добавляющих и редактируйте страницы, в большинстве случаев их страницы выглядят одинаково. Теперь мы должны увидеть детали проекта, новую единую, добавить пакет и редактировать детали четырех страниц, их содержимое одинаково, основными различиями являются следующими:
- Просмотр сведений: все формы можно только просматривать, их нельзя редактировать, нажмите кнопку «ОК», чтобы закрыть страницу.
Так как содержимое этих четырех страниц одинаковое, то я думаю как "эффективный разработчик", вы должны быть не настолько глупы, чтобы написать четыре одинаковых страницы, значит, нам нужно реализовать событие клика кнопки ОК на паблике page, это Пришло время решить, что делать в зависимости от ситуации.
Мы договорились о переменной, чтобы различать, какую страницу «воспроизводит» текущая общедоступная страница:
let source = ''
// view 查看详情;add 单个新增;batchAdd 批量新增;edit 修改详情
Не изучая шаблон стратегии, первое, о чем вы можете подумать, это использовать if...else... для реализации:
document.getElementById("confirmBtn").addEventListener("click", () => {
if (source === "view") {
console.log("关闭页面");
} else if (source === "add") {
console.log("调用单个新增接口");
console.log("其他操作");
} else if (source === "batchAdd") {
console.log("调用批量新增接口");
console.log("其他操作");
} else if (source === "edit") {
console.log("调用修改接口");
console.log("其他操作");
}
});
кучаif...else...
Чтение очень недружественное, низкое можно использовать повторно.
существуетJavaScript
, вы можете использовать характеристики объекта для реализации шаблона стратегии.В приведенном выше примере используется шаблон стратегии, и мы можем написать его следующим образом:
document.getElementById("confirmBtn").addEventListener("click", () => {
const strategies = {
view: () => {
console.log("关闭页面");
},
add: () => {
console.log("调用单个新增接口");
console.log("其他操作");
},
addBatch: () => {
console.log("调用批量新增接口");
console.log("其他操作");
},
edit: () => {
console.log("调用修改接口");
console.log("其他操作");
},
};
strategies[source]();
});
7. Шаблон наблюдателя
определение: определяет отношение зависимости «один ко многим», позволяя нескольким объектам-наблюдателям одновременно отслеживать целевой объект.При изменении состояния целевого объекта все объекты-наблюдатели будут уведомлены, чтобы их можно было обновить автоматически.
Принцип двусторонней привязки данных Vue
знакомыйVue
все знаютVue v2.x
Двусторонняя привязка по принципу данных используется в режиме наблюдателя, когда мы модифицируем слой данных, слой просмотра будет обновляться, бетон может видетьVue
Официальный сайтУглубленные принципы реагированиявведение.双向绑定原理的详细介绍以及实现,网上已经有很多文章了,推荐大家阅读.
- Компилятор: сканируйте и анализируйте соответствующие инструкции каждого узла и инициализируйте соответствующих подписчиков в соответствии с данными шаблона инициализации.
Процесс сотрудничества между тремя выглядит следующим образом;
Шина глобальных событий шины событий
существуетVue
Мы часто используемEvent Bus
Для достижения компонентной связи, строго говоря, это не режим наблюдателя, а режим издатель-подписчик (разница между ними будет рассмотрена позже). Эти два режима схожи по мышлению, и многие книги их не различают. Так что это пример здесь, больше не отдельный шаблон публикации-подписчика.
Event Bus
Использование заключается в следующем:
// 创建一个 Event Bus(本质上也是 Vue 实例)并导出:
const EventBus = new Vue()
export default EventBus
// 在主文件里引入EventBus,并挂载到全局:
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus
// 订阅事件:
this.bus.$on('someEvent', func) // 这里func指someEvent这个事件的监听函数
// 发布(触发)事件:
this.bus.$emit('someEvent', params) // 这里params指someEvent这个事件被触发时回调函数接收的入参
Видно, что явных издателей и подписчиков во всем процессе нет, только одинbus
в общей координации. Это особенность глобальной шины событий - все операции публикации/подписки на события должны проходить через центр событий, а все "приватные транзакции" запрещены!
Давайте реализуемEvent Bus
:
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 这里做了一次浅拷贝,是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
const handlers = this.handlers[eventName].slice()
// 如果有,则逐个调用队列里的回调函数
handlers.forEach((callback) => {
callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
Режим наблюдателя и релиз - разница между режимом абонента
Я ступил на яму этой точки знания. Потому что в большинстве книг говорится, что шаблон публикации-подписки — это шаблон наблюдателя, включая книгу «Шаблоны проектирования JavaScript и практика разработки». Я посмотрю на это позже, но на самом деле между ними есть разница.
-
Режим наблюдателя: возможность прямого прикосновения к наблюдаемому объекту. Проблема связи между модулями уменьшается. Два отдельных и несвязанных модуля также могут взаимодействовать, но они не полностью разделены. Наблюдаемый человек должен поддерживать набор наблюдателей, а наблюдатель должен реализовывать унифицированный метод. Чтобы наблюдатель мог вызывать, два связаны друг с другом.
-
Шаблон публикации-подписки: для публикации/подписки используется сторонняя платформа. Достигается полная развязка, а регистрация и запуск не зависят от третьей платформы обеих сторон.
Издатель не касается подписчика напрямую, но единая третья сторона завершает фактическую операцию связи, которая называется моделью публикации-подписки.. Разница между шаблоном наблюдателя и шаблоном публикации-подписки заключается в том, есть ли третья сторона и может ли издатель напрямую воспринимать подписчика (как показано на рисунке).
Эпилог
В этой статье шаблоны проектирования не обсуждаются подробно, основная цель состоит в том, чтобы показать вам некоторые сценарии, в которых используются шаблоны проектирования, чтобы вы больше не были знакомы с шаблонами проектирования. Затем, на основе этих примеров, чтобы понять определение шаблонов проектирования, я считаю, что будут разные чувства. Прочитав это, пообещайте мне, что в следующий раз, когда кто-то спросит вас о шаблонах проектирования, не говорите, что не знаете, хорошо?