Главная » Статьи » Паттерн отложенной загрузки в JavaScript

Паттерн отложенной загрузки в JavaScript

От автора: вы можете отложить ресурсоемкие в вычислительном отношении операции до тех пор, пока они не понадобятся, с помощью свойств доступа.

Традиционно разработчики создают свойства внутри классов JavaScript для любых данных, которые могут потребоваться в экземпляре. Это не проблема для небольших фрагментов данных, которые легко доступны внутри конструктора. Однако, если некоторые данные необходимо рассчитать, вы можете не захотеть рассчитывать их до того времени как в них возникнет необходимость. Например, рассмотрим этот класс:

class MyClass { constructor() { this.data = someExpensiveComputation(); }
}

Здесь свойство data создается в результате выполнения некоторых дорогостоящих вычислений. Предварительный расчет может оказаться неэффективным, если вы не уверены, что это свойство будет использоваться. К счастью, есть несколько способов отложить эти операции на потом.

Паттерн использования по требованию

Самый простой способ оптимизировать выполнение дорогостоящей операции — дождаться, пока данные потребуются, прежде чем выполнять вычисления. Например, вы можете использовать свойство доступа с геттером для выполнения вычислений по запросу:

class MyClass { get data() { return someExpensiveComputation(); }
}

В этом случае ваши дорогостоящие вычисления не будут выполняться до тех пор, пока кто-то не прочитает свойство data в первый раз, что является улучшением. Однако такие же дорогостоящие вычисления выполняются каждый раз при чтении свойства data, что хуже, чем в предыдущем примере, где, по крайней мере, вычисление выполнялось только один раз. Это не очень хорошее решение.

Паттерн беспорядочной ленивой загрузки

Вычисление при доступе к свойству — хорошее начало. Что вам действительно нужно, так это кэшировать информацию после этого момента и просто использовать кешированную версию. Но где вы храните эту информацию для облегчения доступа? Самый простой подход — определить свойство с тем же именем и установить его значение для вычисленных данных, например:

class MyClass { get data() { const actualData = someExpensiveComputation(); Object.defineProperty(this, "data", { value: actualData, writable: false, configurable: false, enumerable: false }); return actualData; }
}

Здесь свойство data снова определяется как геттер класса, но на этот раз оно кэширует результат. Вызов Object.defineProperty() создает новое свойство data с фиксированным значением actualData. После этого возвращается само значение. В следующий раз, когда к свойству data обращаются, оно будет читать из вновь созданного свойства, а не вызывать геттер:

const object = new MyClass(); // calls the getter
const data1 = object.data; // reads from the data property
const data2 = object.data;

Фактически, все вычисления выполняются только при первом чтении свойства data. Каждое последующее чтение свойства data возвращает кешированную версию.

Единственным недостатком этого шаблона является то, что свойство data начинается как неперечисляемое свойство-прототип и заканчивается как неперечисляемое собственное свойство:

const object = new MyClass();
console.log(object.hasOwnProperty("data")); // false const data = object.data;
console.log(object.hasOwnProperty("data")); // true

Хотя во многих случаях это различие не важно, важно понимать этот шаблон, так как он может вызвать небольшие проблемы при передаче объекта. К счастью, эту проблему легко решить с помощью обновленного шаблона.

Паттерн отложенной загрузки

Если у вас есть вариант использования, когда важно, чтобы свойство с ленивой загрузкой всегда существовало в экземпляре, вы можете использовать Object.defineProperty() для создания свойства внутри конструктора класса. Это немного сложнее, чем в предыдущем примере, но это гарантирует, что свойство существует только в экземпляре. Вот пример:

class MyClass { constructor() { Object.defineProperty(this, "data", { get() { const actualData = someExpensiveComputation(); Object.defineProperty(this, "data", { value: actualData, writable: false, configurable: false }); return actualData; }, configurable: true, enumerable: true }); }
}

Здесь конструктор создает свойство data, используя Object.defineProperty(). Свойство создается в экземпляре (с помощью this), а также указывает свойство, которое должно быть перечислимым и настраиваемым (типично для собственных свойств). Особенно важно установить свойство data как настраиваемое, чтобы его можно было снова вызвать Object.defineProperty().

Затем выполняется вычисление и вызывается Object.defineProperty() второй раз. Вычисленные данные возвращаются из геттера. В следующий раз, когда свойство data будет прочитано, оно будет прочитано из сохраненного значения. В качестве бонуса свойство data теперь существует только как собственное свойство и действует одинаково как до, так и после первого чтения:

const object = new MyClass();
console.log(object.hasOwnProperty("data")); // true const data = object.data;
console.log(object.hasOwnProperty("data")); // true

Для классов это, скорее всего, шаблон, который вы хотите использовать; литералы объектов, с другой стороны, могут использовать более простой подход.

Паттерн отложенной загрузки для литералов

Если вы используете объектный литерал вместо класса, процесс должен быть проще, потому что геттеры, определенные в объектных литералах, определяются как перечислимые собственные свойства (а не свойства прототипа), так и свойства данных. Это означает, что вы можете использовать беспорядочный паттерн ленивой загрузки для классов:

const object = { get data() { const actualData = someExpensiveComputation(); Object.defineProperty(this, "data", { value: actualData, writable: false, configurable: false, enumerable: false }); return actualData; }
}; console.log(object.hasOwnProperty("data")); // true const data = object.data;
console.log(object.hasOwnProperty("data")); // true

Заключение

Функция переопределения свойств объекта в JavaScript дает уникальную возможность кэшировать информацию, вычисление которой может оказаться затратным. Вы можете отложить вычисление до первого чтения свойства, а затем кэшировать результат для дальнейшего использования. Этот подход работает как для классов, так и для литералов объектов, и даже немного проще, потому что вам не нужно беспокоиться о том, что ваш геттер попадет в прототип.

Один из лучших способов повысить производительность — не выполнять одну и ту же работу дважды, поэтому каждый раз, когда вы можете кэшировать результат для дальнейшего использования, вы ускоряете свою программу. Использование паттерна отложенной загрузки позволяет любому свойству стать слоем кэширования для повышения производительности.

Автор: Nicholas C. Zakas

Источник: humanwhocodes.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен