При работе с крупными интерфейсами часто возникает ситуация, когда форматирование подключается к каждому полю отдельно. В небольших формах это не создаёт проблем, однако в административных панелях, CRM-системах, интернет-магазинах и SPA-приложениях количество инстансов быстро растёт.
Типичный пример:
<input class="phone">
<input class="phone">
<input class="phone">
<input class="phone">
document.querySelectorAll('.phone').forEach(input => {
new Cleave(input, {
phone: true,
phoneRegionCode: 'US'
});
});
Каждый вызов new Cleave() создаёт:
При наличии десятков или сотен элементов это начинает влиять на:
Каждый экземпляр библиотеки содержит:
const cleave = new Cleave(input, {
numeral: true
});
Внутри создаются:
input;keydown;focus;cut;copy;При большом количестве экземпляров браузер начинает тратить ресурсы не только на рендер, но и на поддержку инфраструктуры библиотеки.
Одна из распространённых ошибок:
function activateMask(input) {
return new Cleave(input, {
creditCard: true
});
}
Если функция вызывается многократно для одного и того же поля:
activateMask(input);
activateMask(input);
activateMask(input);
то создаются новые обработчики поверх старых.
Это приводит к:
Гораздо эффективнее сохранять инстансы:
const instances = new Map();
function getCleave(input) {
if (instances.has(input)) {
return instances.get(input);
}
const instance = new Cleave(input, {
creditCard: true
});
instances.set(input, instance);
return instance;
}
Теперь для каждого поля существует только один объект.
WeakMap особенно полезен для DOM-элементов.
const instances = new WeakMap();
function init(input) {
if (!instances.has(input)) {
instances.set(input, new Cleave(input, {
numeral: true
}));
}
return instances.get(input);
}
Преимущества:
Необязательно создавать экземпляры сразу после загрузки страницы.
Плохой вариант:
document.querySelectorAll('input').forEach(input => {
new Cleave(input, options);
});
Если на странице 500 полей, то 500 экземпляров будут созданы мгновенно.
Более эффективный подход:
const initialized = new WeakSet();
document.addEventListener('focusin', event => {
const input = event.target;
if (!input.matches('.masked')) {
return;
}
if (initialized.has(input)) {
return;
}
new Cleave(input, {
phone: true,
phoneRegionCode: 'US'
});
initialized.add(input);
});
Преимущества:
Каждый экземпляр Cleave.js добавляет собственные listeners.
Если таких объектов много:
new Cleave(input1);
new Cleave(input2);
new Cleave(input3);
число обработчиков быстро растёт.
Часть логики можно перенести на родительский контейнер:
form.addEventListener('focusin', event => {
const input = event.target;
if (!input.dataset.cleave) {
return;
}
if (!input.cleaveInstance) {
input.cleaveInstance = new Cleave(input, {
numeral: true
});
}
});
Создание происходит только при необходимости.
В React, Vue и Angular поля постоянно удаляются и создаются заново.
Если экземпляры Cleave.js не уничтожаются:
new Cleave(input);
то:
const cleave = new Cleave(input, options);
cleave.destroy();
Метод удаляет:
useEffect(() => {
const cleave = new Cleave(ref.current, {
numeral: true
});
return () => {
cleave.destroy();
};
}, []);
mounted() {
this.cleave = new Cleave(this.$refs.input, {
numeral: true
});
},
beforeUnmount() {
this.cleave.destroy();
}
ngOnInit() {
this.cleave = new Cleave(this.input.nativeElement, {
numeral: true
});
}
ngOnDestroy() {
this.cleave.destroy();
}
При интенсивной работе с динамическими формами можно переиспользовать уже созданные объекты.
Например:
class CleavePool {
constructor() {
this.pool = [];
}
acquire(input, options) {
let instance = this.pool.pop();
if (!instance) {
instance = new Cleave(input, options);
}
return instance;
}
release(instance) {
instance.destroy();
this.pool.push(instance);
}
}
Подобный подход полезен:
Ошибка:
document.querySelectorAll('input')
Такой селектор захватывает:
Лучше:
document.querySelectorAll('[data-cleave]')
или:
form.querySelectorAll('.currency-input')
В таблице на 1000 строк:
<input class="price">
создание 1000 экземпляров становится тяжёлой операцией.
Современные подходы:
В DOM присутствуют только видимые элементы.
Следовательно:
function render() {
document.querySelectorAll('.phone').forEach(input => {
new Cleave(input, options);
});
}
Если render() вызывается повторно:
document.querySelectorAll('.phone').forEach(input => {
if (input.dataset.initialized) {
return;
}
new Cleave(input, options);
input.dataset.initialized = 'true';
});
class CleaveManager {
constructor() {
this.instances = new WeakMap();
}
init(input, options) {
if (this.instances.has(input)) {
return this.instances.get(input);
}
const instance = new Cleave(input, options);
this.instances.set(input, instance);
return instance;
}
destroy(input) {
const instance = this.instances.get(input);
if (!instance) {
return;
}
instance.destroy();
this.instances.delete(input);
}
}
new Cleave(input, {
onValueChanged(event) {
expensiveOperation(event.target.value);
}
});
Если экземпляров много:
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn(...args);
}, delay);
};
}
Использование:
new Cleave(input, {
onValueChanged: debounce(event => {
save(event.target.value);
}, 300)
});
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(node => {
if (node.cleaveInstance) {
node.cleaveInstance.destroy();
}
});
});
});
function Input() {
const ref = useRef();
new Cleave(ref.current, {
numeral: true
});
return <input ref={ref} />;
}
Экземпляр создаётся при каждом render-cycle.
function Input() {
const ref = useRef();
useEffect(() => {
const cleave = new Cleave(ref.current, {
numeral: true
});
return () => cleave.destroy();
}, []);
return <input ref={ref} />;
}
Плохо:
new Cleave(input, {
numeral: true,
numeralThousandsGroupStyle: 'thousand'
});
внутри больших циклов.
const numeralOptions = {
numeral: true,
numeralThousandsGroupStyle: 'thousand'
};
inputs.forEach(input => {
new Cleave(input, numeralOptions);
});
Вместо:
inputs.forEach(input => {
new Cleave(input, options);
});
можно использовать постепенную инициализацию:
function batchInit(inputs, size = 20) {
let index = 0;
function process() {
const batch = inputs.slice(index, index + size);
batch.forEach(input => {
new Cleave(input, options);
});
index += size;
if (index < inputs.length) {
requestAnimationFrame(process);
}
}
process();
}
Преимущества:
Не каждое поле требует Cleave.js.
Частая ошибка:
new Cleave(input, {
uppercase: true
});
Для простых задач выгоднее использовать нативные возможности:
text-transform: uppercase;
или:
<input type="number">
Чем меньше экземпляров библиотеки используется, тем ниже общая нагрузка приложения.
Полезно отслеживать число активных объектов:
console.log(manager.instances);
или:
window.__cleaveCount++;
Признаки проблем:
Особенно важны:
Основные признаки переизбытка экземпляров:
Наиболее эффективная архитектура обычно включает: