Алгоритм структурированного клонирования - Интерфейсы веб API

Алгоритм структурированного клонирования
========================================

Алгоритм структурированного клонирования — это новый алгоритм, определённый спецификацией HTML5 для сериализации комплексных JavaScript объектов. Он более функционален, чем JSON в том что способен поддерживать сериализацию объектов содержащих циклические графы — первичные объекты, которые ссылаются на другие объекты у которых есть ссылка на первичные объекты в том же графе. В дополнение, в некоторых других случаях алгоритм структурированного клонирования может быть более эффективен, чем JSON.

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

Преимущества перед JSON
-----------------------

Вот основные преимущества структурированного клонирования перед JSON:

* Структурированные клоны могут копировать `RegExp` объекты.
* Структурированные клоны могут копировать `Blob`, `File`, и `FileList` объекты.
* Структурированные клоны могут копировать `ImageData` объекты. The dimensions of the clone's `CanvasPixelArray` (en-US) will match the original and have a duplicate of the same pixel data.
* Structured clones can correctly duplicate objects containing cyclic graphs of references.
Исключения, не работающие со структурированными клонами
-------------------------------------------------------

* `Error` and `Function` objects cannot be duplicated by the structured clone algorithm; attempting to do so will throw a `DATA_CLONE_ERR` exception.
* Attempting to clone DOM nodes will likewise throw a `DATA_CLONE_ERR` exception.
* Certain parameters of objects are not preserved:
    + The `lastIndex` field of `RegExp` objects is not preserved.
    + Property descriptors, setters, and getters (as well as similar metadata-like features) are not duplicated. For example, if an object is marked read-only using a property descriptor, it will be read-write in the duplicate, since that's the default condition.
    + The prototype chain does not get walked and duplicated.
Поддерживаемые типы
-------------------

| Object type | Notes |
| --- | --- |
| All primitive types | However not symbols |
| Boolean object |  |
| String object |  |
| Date |  |
| RegExp | The `lastIndex` field is not preserved. |
| `Blob` |  |
| `File` |  |
| `FileList` |  |
| ArrayBuffer |  |
| ArrayBufferView (en-US) | This basically means all typed arrays like Int32Array etc. |
| `ImageData` |  |
| Array |  |
| Object | This just includes plain objects (e.g. from object literals) |
| Map (en-US) |  |
| Set |  |

Другой вариант: вложенное копирование
--------------------------------------

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

```js
function clone(objectToBeCloned) {
  // Basis.
  if (!(objectToBeCloned instanceof Object)) {
    return objectToBeCloned;
  }

  var objectClone;

  // Filter out special objects.
  var Constructor = objectToBeCloned.constructor;
  switch (Constructor) {
    // Implement other special objects here.
    case RegExp:
      objectClone = new Constructor(objectToBeCloned);
      break;
    case Date:
      objectClone = new Constructor(objectToBeCloned.getTime());
      break;
    default:
      objectClone = new Constructor();
  }

  // Clone each property.
  for (var prop in objectToBeCloned) {
    objectClone[prop] = clone(objectToBeCloned[prop]);
  }

  return objectClone;
}
```

**Примечание:** Этот алгоритм реализован только для `RegExp`, `Array`, и `Date` специальных объектов. Вы можете реализовать другие условия, удовлетворяющие вашим потребностям.

Ещё по теме
-----------

* HTML5 Specification: Safe passing of structured data
* `window.history`
* `window.postMessage()`
* Web Workers
* Components.utils.cloneInto


Использование Web Workers - Интерфейсы веб API

Использование Web Workers
=========================

Web Worker-ы предоставляют простое средство для запуска скриптов в фоновом потоке. Поток Worker'а может выполнять задачи без вмешательства в пользовательский интерфейс. К тому же, они могут осуществлять ввод/вывод, используя `XMLHttpRequest` (хотя атрибуты `responseXML` и `channel` всегда будут равны null). Существующий Worker может отсылать сообщения JavaScript коду-создателю через обработчик событий, указанный этим кодом (и наоборот). Эта статья даёт детальную инструкцию по использованию Web Workers.

Web Workers API
---------------

Worker - это объект, создаваемый конструктором (например, `Worker()`) и запускающий именной JavaScript файл — этот файл содержит код, который будет выполнен в потоке Worker'а; объекты же Workers запускаются в другом глобальном контексте, отличающемся от текущего, - `window`. Поэтому использование переменной `window` для получения текущего глобального контекста (вместо `self`) внутри `Worker` вернёт ошибку.

Контекст Worker'а представлен объектом `DedicatedWorkerGlobalScope` (en-US) в случае выделенных Workers (обычные Workers используются одним скриптом; совместные Workers используют объект `SharedWorkerGlobalScope` (en-US)). Выделенный Worker доступен только из скрипта-родителя, в то время как совместные Workers могут быть доступны из нескольких сценариев.

**Примечание:** Смотрите страницу Web Workers API для справки по Workers и прочие руководства.

Вы можете запускать любой код внутри потока worker-а, за некоторыми исключениями. Например, вы не можете прямо манипулировать DOM внутри worker-а, или использовать некоторые методы по умолчанию и свойства объекта `window`. Но вы можете использовать большой набор опций, доступный под `Window`, включая WebSockets, и механизмы хранения данных, таких как IndexedDB и относящихся только к Firefox OS Data Store API. Для дополнительной информации смотрите Functions and classes available to workers (en-US).

Данные передаются между worker-ами и главным потоком через систему сообщений — обе стороны передают свои сообщения, используя метод `postMessage()` и отвечают на сообщения при помощи обработчика событий `onmessage` (сообщение хранится в атрибуте data события `Message`). Данные при этом копируются, а не делятся.

Объекты Workers могут, в свою очередь, создавать новые объекты workers, и так до тех пор, пока всё работает в рамках текущей страницы. Плюс к этому, объекты workers могут использовать `XMLHttpRequest` для сетевого ввода/вывода, но есть исключение - атрибуты `responseXML` и `channel` объекта `XMLHttpRequest` всегда возвращают `null`.

Выделенные Workers
------------------

Как уже упоминалось выше, выделенный Worker доступен только для скрипта, который его вызвал. В этом разделе речь пойдёт о JavaScript, который можно найти в нашем основном примере выделенного Worker (запустить скрипт): этот пример позволяет ввести два числа для умножения. Эти числа отправляются в Worker, перемножаются, а результат возвращается на страницу и отображается.

Этот пример достаточно тривиален, но для ознакомления с базовыми концепциями worker-ов мы решили его упростить. Более продвинутые детали описаны далее в статье.

### Определение поддержки Worker

Для большего контроля над ошибками и обратной совместимости, рекомендуется обернуть ваш код доступа к worker-у в следующий (main.js):

```js
if (window.Worker) {

  ...

}
```

### Создание выделенного worker

Создание нового worker-а — это легко. Всё что вам нужно это вызвать конструктор `Worker()`, указав URI скрипта для выполнения в потоке worker-а (main.js):

```js
var myWorker = new Worker("worker.js");
```

### Передача сообщений в и из выделенного worker

Магия worker-ов происходит через `postMessage()` метод и обработчик событий `onmessage`. Когда вы хотите отправить сообщение в worker, вы доставляете сообщение к нему вот так (main.js):

```js
first.onchange = function () {
  myWorker.postMessage([first.value, second.value]);
  console.log("Message posted to worker");
};

second.onchange = function () {
  myWorker.postMessage([first.value, second.value]);
  console.log("Message posted to worker");
};
```

В приведённом фрагменте кода мы имеем два `<input>` элемента, представленных переменными `first` и `second`; когда значение любой из переменных изменяется, `myWorker.postMessage([first.value,second.value])` используется для отправки обоих значений, представленных в виде массива, в worker. Посредством аргумента `message` возможна передача практически любых данных в worker.

Внутри worker-a мы можем обрабатывать сообщения и отвечать на них при помощи добавления обработчика события `onmessage` подобным образом (worker.js):

```js
onmessage = function (e) {
  console.log("Message received from main script");
  var workerResult = "Result: " + e.data[0] \* e.data[1];
  console.log("Posting message back to main script");
  postMessage(workerResult);
};
```

Обработчик `onmessage` позволяет нам запустить некий код всякий раз, когда получен пакет с сообщением, доступным в атрибуте `data` события `message`. В примере выше мы просто перемножаем вместе две цифры, после чего используем `postMessage()` снова, чтобы отправить полученный результат назад в основной поток.

Возвращаясь в основной поток, мы используем `onmessage` снова, чтобы отреагировать на сообщение, отправленное нам назад из worker-а:

```js
myWorker.onmessage = function (e) {
  result.textContent = e.data;
  console.log("Message received from worker");
};
```

В примере выше мы берём данные из события сообщения и ставим их как `textContent` у результирующего абзаца, чтобы показать пользователю результат этой калькуляции.

**Примечание:** Обратите внимание, что `onmessage()` и `postmessage()` должны вызываться из экземпляра Worker в главном потоке, но не в потоке worker-а. Это связано с тем, что внутри потока worker-а, worker выступает в качестве глобального объекта.

**Примечание:** При передаче сообщения между основным потоком и потоком worker-а, оно копируется или "передаётся" (перемещается), не делится между потоками. Читайте Transferring data to and from workers: further details для более подробного объяснения.

### Завершение работы worker-а

Прекращение работы worker-а главного потока достигается методом `terminate`:

```js
myWorker.terminate();
```

Поток worker-а немедленно уничтожается.

### Обработка ошибок

При ошибке во время выполнения worker-а, вызывается его обработчик событий `onerror`. Он принимает событие `error`, которое реализует интерфейс `ErrorEvent`.

Событие не всплывает и его можно отменить. Для отмены действия по умолчанию, worker может вызвать метод `preventDefault()` в обработчике события ошибки.

У события ошибки есть три поля, которые представляют интерес:

`message`

Сообщение об ошибке в читаемом виде.

`filename`

Имя файла со скриптом, в котором ошибка произошла.

`lineno`

Номер строки в файле, в котором произошла ошибка.

### Создание subworkers

Worker-ы могут запускать другие worker-ы. Так называемые sub-worker'ы должны быть того же происхождения (same-origin), что и родительский документ. Кроме того, URI для subworker-ов рассчитываются относительно родительского worker'а, а не родительского документа. Это позволяет worker-ам проще следить за тем, где находятся их зависимости.

### Импорт скриптов и библиотек

Worker потоки имеют доступ к глобальной функции, `importScripts()`, которая позволяет импортировать скрипты с того же домена в их область видимости. Функция принимает ноль и более URI параметров, как список ссылок на ресурсы для импорта; все нижеприведённые примеры верны:

```js
importScripts(); /\* imports nothing \*/
importScripts("foo.js"); /\* imports just "foo.js" \*/
importScripts("foo.js", "bar.js"); /\* imports two scripts \*/
```

Браузер загружает каждый указанный скрипт и исполняет его. Любые глобальные объекты, создаваемые каждым скриптом могут быть использованы в worker'е. Если скрипт не удалось загрузить, будет брошена ошибка `NETWORK_ERROR`, и последующий код не будет исполнен. Тем не менее код, исполненный ранее (включая отложенный при помощи `window.setTimeout()` (en-US)) останется функционален. Объявления функций идущие **после** вызова метода `importScripts()` также будут доступны, т.к. объявления функций всегда обрабатываются перед остальным кодом.

**Примечание:** Скрипты могут быть загружены в произвольном порядке, но их исполнение будет в том порядке, в котором имена файлов были переданы в `importScripts()`. Функция выполняется синхронно; `importScripts()` не вернёт исполнение, пока все скрипты не будут загружены и исполнены.

Разделяемые worker-ы (Shared workers)
-------------------------------------

Разделяемый worker доступен нескольким разным скриптам — даже если они находятся в разных окнах, фреймах или даже worker-ах. В этом разделе мы обсудим JavaScript, который можно найти в нашем базовом примере разделяемых worker-ов (запустить разделяемый worker): Он очень похож на базовый пример выделенных worker-ов, за исключением двух функций, которые доступны из разных скриптовых файлов: *умножение двух чисел* или *возведение числа в степень.* Оба скрипта используют один и тот же worker для необходимых вычислений.

Здесь мы сосредоточимся на разнице между выделенными и разделёнными worker-ами. Обратите внимание, что в данном примере есть две HTML страницы с JavaScript-кодом, которые используют один и тот же файл worker-а.

**Примечание:** Если разделяемый worker может быть доступен из нескольких контекстов просмотра, то все они должны иметь одно и то же происхождение (одни и те же протокол, хост и порт).

>
> \*\*Примечание:\*\*В Firefox разделяемый worker не может быть использован совместно документами в приватном и неприватном окне (Firefox bug 1177621).
>
>
>

### Создание разделяемого worker-а

Запуск разделяемого worker-а очень похож на запуск выделенного worker-а, но используется другой конструктор (см. index.html и index2.html) — в каждом документе необходимо поднять worker, для этого следует написать такой код:

```js
var myWorker = new SharedWorker("worker.js");
```

Большая разница заключается в том, что с разделяемым worker-ом необходимо взаимодействовать через объект `port` — явно открыв порт, с помощью которого скрипты могут взаимодействовать с worker-ом (в случае выделенного worker-а это происходит неявно).

Соединение с портом должно быть осуществлено либо неявно, используя обработчик событие `onmessage`, либо явно, вызвав метод **start()** перед тем, как отправлять любые сообщения. Вызов метода start() необходим только тогда, когда подписка на событие реализована через метод `addEventListener()`.

>
> \*\*Примечание:\*\*Когда используется метод `start()` чтобы открыть соединение с портом, его необходимо вызывать и в родительском потоке и в потоке worker-а, если необходима двухсторонняя коммуникация.
>
>
>

```js
myWorker.port.start(); // в родительском потоке
```

```js
port.start(); // в потоке worker-а, где переменная port является ссылкой на порт
```

### Передача сообщений в/из разделяемого worker-а

Теперь сообщения могут быть отправлены worker-у, как и прежде, но метод `postMessage()` должен вызываться из объекта `port` (ещё раз, вы можете увидеть схожие конструкции в multiply.js и square.js):

```js
squareNumber.onchange = function () {
  myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
  console.log("Message posted to worker");
};
```

Теперь на стороне worker-а. Здесь код немного сложнее (worker.js):

```js
self.addEventListener("connect", function (e) {
  // требуется addEventListener()
  var port = e.ports[0];
  port.onmessage = function (e) {
    var workerResult = "Result: " + e.data[0] \* e.data[1];
    port.postMessage(workerResult);
  };
  port.start(); // вызов необязательный, т.к. используется обработчик событий onmessage
});
```

Первый этап состоит из события `onconnect`. Оно срабатывает, когда произошло подключение (т.е. когда в родительском потоке отработало событие `onmessage` или когда в нем был вызван метод `start()`).

Мы используем атрибут события `ports`, чтобы получить порт и сохранить его в переменной.

Второй этап — это обработчик события `message` на сохранённом порту. Он нужен для подсчёта и вывода результата вычисления в основной поток. Установка обработчика `message` в потоке worker-а также открывает подключение к родительскому потоку, поэтому вызов на `port.start()` на самом деле не нужен (см. код обработчика `onconnect`).

Последний этап — возвращение в основной поток и обработка сообщения от worker‑а (ещё раз, вы можете увидеть схожие конструкции в multiply.js и square.js):

```js
myWorker.port.onmessage = function (e) {
  result2.textContent = e.data[0];
  console.log("Message received from worker");
};
```

Когда сообщение приходит через порт от worker-а, мы проверяем тип результата вычислений и затем вставляем его в соответствующий абзац.

О потоковой безопасности
------------------------

Интерфейс `Worker` создаёт настоящие потоки на уровне операционной системы, что может смутить опытных программистов и навести их на мысли о проблемах, связанных с конфликтом доступа к общим объектам.

На самом деле создать такие проблемы достаточно сложно, так как worker-ы жёстко контролируются. У них нет доступа к непотокобезопасным объектам DOM, а все данные между потоками передаются в качестве сериализованных объектов. Придётся очень постараться, чтобы вызывать проблемы потокобезопасности в вашем коде.

Передача данных в и из worker-ов: другие детали
-----------------------------------------------

Передача данных между главной страницей и worker-ом происходит путём копирования, а не передачи по ссылке. Объекты сериализуются при передаче и затем десериализуются на другом конце. Страница и worker не используют совместно одни и те же экземпляры, для каждого создаётся свой. Большинство браузеров реализуют это структурированным клонированием (structured cloning (en-US)).

Для иллюстрации этого мы создадим функцию `emulateMessage()`, которая будет имитировать поведение значения, которое клонируется, но не используется совместно при переходе от worker-а к главной странице или наоборот.

```js
function emulateMessage(vVal) {
  return eval("(" + JSON.stringify(vVal) + ")");
}

// Tests

// test #1
var example1 = new Number(3);
console.log(typeof example1); // object
console.log(typeof emulateMessage(example1)); // number

// test #2
var example2 = true;
console.log(typeof example2); // boolean
console.log(typeof emulateMessage(example2)); // boolean

// test #3
var example3 = new String("Hello World");
console.log(typeof example3); // object
console.log(typeof emulateMessage(example3)); // string

// test #4
var example4 = {
  name: "John Smith",
  age: 43,
};
console.log(typeof example4); // object
console.log(typeof emulateMessage(example4)); // object

// test #5
function Animal(sType, nAge) {
  this.type = sType;
  this.age = nAge;
}
var example5 = new Animal("Cat", 3);
alert(example5.constructor); // Animal
alert(emulateMessage(example5).constructor); // Object
```

Значения, которые клонируются и совместно не используются, называются сообщениями. Как вы, возможно, знаете, сообщения могут быть отправлены в главную страницу и из неё, используя `postMessage()`, и `data` (en-US), содержа данные, передаваемые из worker-а.

**example.html**: (главная страница):

```js
var myWorker = new Worker("my\_task.js");

myWorker.onmessage = function (oEvent) {
  console.log("Worker said : " + oEvent.data);
};

myWorker.postMessage("ali");
```

**my\_task.js** (worker):

```js
postMessage("I'm working before postMessage('ali').");

onmessage = function (oEvent) {
  postMessage("Hi " + oEvent.data);
};
```

Алгоритм структурированного клонирования может принять JSON и некоторые вещи, которые JSON не может принять, например, циклические ссылки.

### Примеры передачи данных

#### Пример #1: Расширенная передача JSON данных и создание системы коммутации

Если вам нужно передать сложные данные и вызвать множество различных функций как на главной странице, так и в worker-е, вы можете создать следующую систему.

В первую очередь мы создаём класс QueryableWorker, который принимает url worker-а, стандартный обработчик событий (defaultListener) и обработчик ошибок. Этот класс будет отслеживать всех обработчиков и поможет нам общаться с воркером.

```js
function QueryableWorker(url, defaultListener, onError) {
  var instance = this,
    worker = new Worker(url),
    listeners = {};

  this.defaultListener = defaultListener || function () {};

  if (onError) {
    worker.onerror = onError;
  }

  this.postMessage = function (message) {
    worker.postMessage(message);
  };

  this.terminate = function () {
    worker.terminate();
  };
}
```

Затем мы добавляем методы добавления/удаления обработчиков.

```js
this.addListeners = function (name, listener) {
  listeners[name] = listener;
};

this.removeListeners = function (name) {
  delete listeners[name];
};
```

Здесь мы создадим у worker-а два простых события для примера: получение разницы двух чисел и создание оповещения через три секунды. Но сначала нам нужно реализовать метод sendQuery, который проверит есть ли вообще у worker-а обработчик, который мы собираемся вызвать.

```js
/\*
 Эта функция принимает по крайней мере один аргумент: имя метода, который мы хотим вызвать.
 Далее мы можем передать методу необходимые ему аргументы.
 \*/
this.sendQuery = function () {
  if (arguments.length < 1) {
    throw new TypeError(
      "QueryableWorker.sendQuery takes at least one argument",
    );
    return;
  }
  worker.postMessage({
    queryMethod: arguments[0],
    queryArguments: Array.prototype.slice.call(arguments, 1),
  });
};
```

Завершим QueryableWorker методом `onmessage`. Если worker имеет соответствующий метод, который мы запросили, он также должен вернуть соответствующий обработчик и аргументы, которые нам нужны. Останется лишь найти его в `listeners`:

```js
worker.onmessage = function (event) {
  if (
    event.data instanceof Object &&
    event.data.hasOwnProperty("queryMethodListener") &&
    event.data.hasOwnProperty("queryMethodArguments")
  ) {
    listeners[event.data.queryMethodListener].apply(
      instance,
      event.data.queryMethodArguments,
    );
  } else {
    this.defaultListener.call(instance, event.data);
  }
};
```

Теперь к самому worker-у. Сначала следует определить эти два простых метода:

```js
var queryableFunctions = {
  getDifference: function (a, b) {
    reply("printStuff", a - b);
  },
  waitSomeTime: function () {
    setTimeout(function () {
      reply("doAlert", 3, "seconds");
    }, 3000);
  },
};

function reply() {
  if (arguments.length < 1) {
    throw new TypeError("reply - takes at least one argument");
    return;
  }
  postMessage({
    queryMethodListener: arguments[0],
    queryMethodArguments: Array.prototype.slice.call(arguments, 1),
  });
}

/\* This method is called when main page calls QueryWorker's postMessage method directly\*/
function defaultReply(message) {
  // do something
}
```

И `onmessage`:

```js
onmessage = function (event) {
  if (
    event.data instanceof Object &&
    event.data.hasOwnProperty("queryMethod") &&
    event.data.hasOwnProperty("queryMethodArguments")
  ) {
    queryableFunctions[event.data.queryMethod].apply(
      self,
      event.data.queryMethodArguments,
    );
  } else {
    defaultReply(event.data);
  }
};
```

Полный код примера:

**example.html** (основная страница):

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>MDN Example - Queryable worker</title>
    <script type="text/javascript">
 /\*
 QueryableWorker instances methods:
 \* sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc): calls a Worker's queryable function
 \* postMessage(string or JSON Data): see Worker.prototype.postMessage()
 \* terminate(): terminates the Worker
 \* addListener(name, function): adds a listener
 \* removeListener(name): removes a listener
 QueryableWorker instances properties:
 \* defaultListener: the default listener executed only when the Worker calls the postMessage() function directly
 \*/
 function QueryableWorker(url, defaultListener, onError) {
 var instance = this,
 worker = new Worker(url),
 listeners = {};

 this.defaultListener = defaultListener || function () {};

 if (onError) {
 worker.onerror = onError;
 }

 this.postMessage = function (message) {
 worker.postMessage(message);
 };

 this.terminate = function () {
 worker.terminate();
 };

 this.addListener = function (name, listener) {
 listeners[name] = listener;
 };

 this.removeListener = function (name) {
 delete listeners[name];
 };

 /\*
 This functions takes at least one argument, the method name we want to query.
 Then we can pass in the arguments that the method needs.
 \*/
 this.sendQuery = function () {
 if (arguments.length < 1) {
 throw new TypeError(
 "QueryableWorker.sendQuery takes at least one argument",
 );
 return;
 }
 worker.postMessage({
 queryMethod: arguments[0],
 queryMethodArguments: Array.prototype.slice.call(arguments, 1),
 });
 };

 worker.onmessage = function (event) {
 if (
 event.data instanceof Object &&
 event.data.hasOwnProperty("queryMethodListener") &&
 event.data.hasOwnProperty("queryMethodArguments")
 ) {
 listeners[event.data.queryMethodListener].apply(
 instance,
 event.data.queryMethodArguments,
 );
 } else {
 this.defaultListener.call(instance, event.data);
 }
 };
 }

 // your custom "queryable" worker
 var myTask = new QueryableWorker("my\_task.js");

 // your custom "listeners"
 myTask.addListener("printStuff", function (result) {
 document
 .getElementById("firstLink")
 .parentNode.appendChild(
 document.createTextNode("The difference is " + result + "!"),
 );
 });

 myTask.addListener("doAlert", function (time, unit) {
 alert("Worker waited for " + time + " " + unit + " :-)");
 });
 </script>
  </head>
  <body>
    <ul>
      <li>
        <a
 id="firstLink"
 href="javascript:myTask.sendQuery('getDifference', 5, 3);"
 >What is the difference between 5 and 3?</a
 >
      </li>
      <li>
        <a href="javascript:myTask.sendQuery('waitSomeTime');"
 >Wait 3 seconds</a
 >
      </li>
      <li>
        <a href="javascript:myTask.terminate();">terminate() the Worker</a>
      </li>
    </ul>
  </body>
</html>
```

**my\_task.js** (код worker-а):

```js
var queryableFunctions = {
  // пример #1: получить разницу между двумя числами
  getDifference: function (nMinuend, nSubtrahend) {
    reply("printStuff", nMinuend - nSubtrahend);
  },
  // пример #2: подождать три секунды
  waitSomeTime: function () {
    setTimeout(function () {
      reply("doAlert", 3, "seconds");
    }, 3000);
  },
};

// системные функции

function defaultReply(message) {
  // your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly
  // do something
}

function reply() {
  if (arguments.length < 1) {
    throw new TypeError("reply - not enough arguments");
    return;
  }
  postMessage({
    queryMethodListener: arguments[0],
    queryMethodArguments: Array.prototype.slice.call(arguments, 1),
  });
}

onmessage = function (oEvent) {
  if (
    oEvent.data instanceof Object &&
    oEvent.data.hasOwnProperty("queryMethod") &&
    oEvent.data.hasOwnProperty("queryMethodArguments")
  ) {
    queryableFunctions[oEvent.data.queryMethod].apply(
      self,
      oEvent.data.queryMethodArguments,
    );
  } else {
    defaultReply(oEvent.data);
  }
};
```

Можно переключать содержимое каждой главной страницы -> worker и worker -> сообщение главной страницы. И имена свойств "queryMethod", "queryMethodListeners", "queryMethodArguments" могут быть любыми пока они согласуются с `QueryableWorker` и `worker`.

### Передача данных с помощью передачи владения (передаваемые объекты)

Google Chrome 17+ and Firefox 18+ имеют дополнительную возможность передачи определённых типов объектов (передаваемые объекты реализующие `Transferable` интерфейс) к или из worker-а с высокой производительностью. Эти объекты передаются из одного контекста в другой без операций копирования, что приводит к значительному повышению производительности при отправке больших наборов данных. Думайте об этом как о передаче по ссылке в мире C/C++. Однако в отличии от передачи по ссылке, "версия" из вызывающего контекста больше недоступна после передачи. Владельцем становится новый контекст. Для примера, после передачи `ArrayBuffer` из главной страницы к worker-у, исходный `ArrayBuffer` очищается и более недоступен для использования. Его содержание (в буквальном смысле) переносится в рабочий контекст.

```js
// Create a 32MB "file" and fill it.
var uInt8Array = new Uint8Array(1024 \* 1024 \* 32); // 32MB
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i;
}

worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
```

**Примечание:** Для дополнительной информации о передаваемых объектах, производительности и поддержки для этого метода, читайте Transferable Objects: Lightning Fast! на HTML5 Rocks.

Встроенные worker-ы
-------------------

Не существует утверждённого способа встроить код worker-а в рамках веб-страницы, как элемент `<script>` делает для обычных скриптов. Но элемент `<script>`, который не имеет атрибута `src` и атрибута `type`, которому не назначен выполняемый MIME type, можно считать блоком данных для использования JavaScript. Блок данных "Data blocks" — это более общее свойство HTML5, может содержать любые текстовые данные. Так, worker может быть встроен следующим образом:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>MDN Example - Embedded worker</title>
    <script type="text/js-worker">
 // Этот script НЕ БУДЕТ анализироваться JS движками, потому что его MIME-тип text/js-worker.
 var myVar = 'Hello World!';
 // Остальная часть кода вашего воркера идёт сюда.
 </script>
    <script type="text/javascript">
 // Этот script БУДЕТ проанализирован JS движкам, потому что его MIME-тип text/javascript.
 function pageLog(sMsg) {
 // Use a fragment: browser will only render/reflow once.
 var oFragm = document.createDocumentFragment();
 oFragm.appendChild(document.createTextNode(sMsg));
 oFragm.appendChild(document.createElement("br"));
 document.querySelector("#logDisplay").appendChild(oFragm);
 }
 </script>
    <script type="text/js-worker">
 // Этот script НЕ БУДЕТ анализироваться JS движками, потому что его MIME-тип text/js-worker.
 onmessage = function(oEvent) {
 postMessage(myVar);
 };
 // Остальная часть кода вашего воркера идёт сюда.
 </script>
    <script type="text/javascript">
 // Этот script БУДЕТ проанализирован JS движкам, потому что его MIME-тип text/javascript.

 // В прошлом...:
 // blob builder существовал
 // ... но теперь мы используем Blob...:
 var blob = new Blob(
 Array.prototype.map.call(
 document.querySelectorAll("script[type='text\/js-worker']"),
 function (oScript) {
 return oScript.textContent;
 },
 ),
 { type: "text/javascript" },
 );

 // Создание нового свойства document.worker, содержащего все наши "text/js-worker" скрипты.
 document.worker = new Worker(window.URL.createObjectURL(blob));

 document.worker.onmessage = function (oEvent) {
 pageLog("Received: " + oEvent.data);
 };

 // Запуск воркера.
 window.onload = function () {
 document.worker.postMessage("");
 };
 </script>
  </head>
  <body>
    <div id="logDisplay"></div>
  </body>
</html>
```

Встраиваемый worker теперь внесён в новое custom свойство `document.worker` Также стоит отметить, что вы также можете преобразовать функцию в BLOB-объект, а затем сгенерировать URL объекта из этого BLOB-объекта. Например:

```js
function fn2workerURL(fn) {
  var blob = new Blob(["(" + fn.toString() + ")()"], {
    type: "application/javascript",
  });
  return URL.createObjectURL(blob);
}
```

Другие примеры
--------------

В этой секции представлено ещё несколько примеров как использовать worker-ы.

### Выполнение вычислений в фоне

Worker-ы в основном полезны для того, чтобы позволить вашему коду выполнять ресурсоёмкие вычисления, не блокируя поток пользовательского интерфейса. В этом примере, worker используется для вычисления числа Фибоначчи.

#### Код JavaScript

Следующий код JavaScript хранится в файле "fibonacci.js", на который ссылается HTML в следующем разделе.

```js
var results = [];

function resultReceiver(event) {
  results.push(parseInt(event.data));
  if (results.length == 2) {
    postMessage(results[0] + results[1]);
  }
}

function errorReceiver(event) {
  throw event.data;
}

onmessage = function (event) {
  var n = parseInt(event.data);

  if (n == 0 || n == 1) {
    postMessage(n);
    return;
  }

  for (var i = 1; i <= 2; i++) {
    var worker = new Worker("fibonacci.js");
    worker.onmessage = resultReceiver;
    worker.onerror = errorReceiver;
    worker.postMessage(n - i);
  }
};
```

Worker устанавливает свойство `onmessage` для функции, которая будет получать сообщения, отправленные при вызове `postMessage()` рабочего объекта (обратите внимание, что это отличается от определения глобальной *переменной* с таким именем или определения *функции* с таким именем. `var onmessage` и `function onmessage` будет определять глобальные свойства с этими именами , но они не будут регистрировать функцию для получения сообщений, отправленных веб-страницей, которая создала worker). Это запускает рекурсию, порождая новые копии для обработки каждой итерации вычисления.

#### HTML код

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Test threads fibonacci</title>
  </head>
  <body>
    <div id="result"></div>

    <script language="javascript">
 var worker = new Worker("fibonacci.js");

 worker.onmessage = function (event) {
 document.getElementById("result").textContent = event.data;
 dump("Got: " + event.data + "\n");
 };

 worker.onerror = function (error) {
 dump("Worker error: " + error.message + "\n");
 throw error;
 };

 worker.postMessage("5");
 </script>
  </body>
</html>
```

Веб-страница создаёт элемент `div` с ID `result` , который используется для отображения результата, а затем порождает worker. После порождения worker-а, обработчик `onmessage` настроен для отображения результатов путём установки содержимого элемента `div`, и обработчик `onerror` настроен на выброс сообщения об ошибке.

Наконец, сообщение отправляется worker-у, чтобы запустить его.

Попробуйте этот пример.

### Выполнение веб I/O в фоне

Вы можете найти пример этого в статье Использование worker-ов в расширениях.

### Разделение задач между множественными worker-ами

Поскольку многоядерные компьютеры становятся все более распространёнными, часто бывает полезно разделить вычислительно сложные задачи между несколькими worker-ами, которые затем могут выполнить эти задачи на многопроцессорных ядрах.

Другие типы worker-ов
---------------------

В дополнение к выделенным и совместно используемым web worker-ам доступны другие типы worker-ов:

* ServiceWorkers (en-US), по сути, действуют как прокси-серверы, которые размещаются между веб-приложениями, браузером и сетью (при наличии). Они предназначены (помимо прочего) для создания эффективного автономного взаимодействия, перехвата сетевых запросов и принятия соответствующих действий в зависимости от того, доступна ли сеть, и обновлены ли ресурсы на сервере. Они также разрешают доступ push-уведомлениям и API фоновой синхронизации.
* Chrome Workers это worker типа Firefox-only, который вы можете использовать, если вы разрабатываете дополнения и хотите использовать worker-ы в расширениях и иметь доступ к js-ctypes в вашем worker-е. Смотрите `ChromeWorker` для более подробной информации.
* Audio Workers предоставляют возможность прямой обработки звука по сценарию в контексте web worker-а.
Функции и интерфейсы доступные в worker-ах
------------------------------------------

Внутри web worker-а вы можете использовать большинство стандартных функций JavaScript, включая:

* `Navigator`
* `XMLHttpRequest`
* `Array`, `Date`, `Math`, и `String`
* `Window.requestAnimationFrame`, `WindowTimers.setTimeout`, и `WindowTimers.setInterval` (en-US)

Главное, что вы не можете сделать в Worker это напрямую повлиять на родительскую страницу. Это включает в себя манипулирование DOM и использование объектов этой страницы. Вы должны сделать это косвенно, отправив сообщение обратно основному сценарию через `DedicatedWorkerGlobalScope.postMessage` (en-US), а затем выполнив изменения оттуда.

**Примечание:** Для знакомства с полным списком функций, доступных для worker-ов, смотрите статью Функции и интерфейсы доступные worker-ам (en-US).

Спецификации
------------

| Specification |
| --- |
| HTML Standard # workers |

Совместимость с браузерами
--------------------------

{{Compat}}Смотрите также
--------------

* `Worker` интерфейс
* `SharedWorker` интерфейс
* Функции доступные для worker-ов (en-US)
* Продвинутые концепции и примеры


PushManager.subscribe() - Интерфейсы веб API

PushManager.subscribe()
=======================

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

Метод **`subscribe()`** интерфейса `PushManager` позволяет осуществлять подписку на push-уведомления.

Возвращает `Promise`, который разрешается в объект `PushSubscription` (en-US), содержащий детали push-подписки. Новая push-подписка создаётся в случае, если сервис-воркер не имеет существующей подписки.

Синтаксис
---------
```

PushManager.subscribe(options).then(function(pushSubscription) { ... } );
```

### Свойства

`options Необязательный`

Объект, содержащий необязательные параметры конфигурации. Может иметь следующие свойства:

* `userVisibleOnly`: Булевое значение, указывающее на то, будет ли возвращаемая подписка использоваться для сообщений, чей эффект может быть видим для пользователя.
* `applicationServerKey`: открытый ключ ECDSA P-256, закодированный в Base64 `DOMString` или `ArrayBuffer`, содержащий , который push-сервер будет использовать для аутентификации сервера приложений. Если указано, все сообщения с сервера вашего приложения должны использовать схему аутентификации VAPID и включать JWT, подписанный соответствующим закрытым ключом. Этот ключ \*\**НЕ* \*\*тот же ключ ECDH, который вы используете для шифрования данных. Для получения дополнительной информации см. «Using VAPID with WebPush».

>
> \*\*Примечание:\*\*Свойство, требуемое некоторыми браузерами, например Chrome и Edge.
>
>
>

### Возвращает

`Promise` который разрешается в объект `PushSubscription` (en-US).

Пример
------

```js
this.onpush = function (event) {
  console.log(event.data);
  // Отсюда можно записывать данные в IndexedDB, отправлять их в любое
  // открытое окно, отображать уведомление и т. д.
};

navigator.serviceWorker
  .register("serviceworker.js")
  .then(function (serviceWorkerRegistration) {
    serviceWorkerRegistration.pushManager.subscribe().then(
      function (pushSubscription) {
        console.log(pushSubscription.subscriptionId);
        console.log(pushSubscription.endpoint);
        // Детали push-подписки, требуемые сервером приложения,
        // теперь доступны, и могут быть отправлены, к примеру,
        // при помощи XMLHttpRequest.
      },
      function (error) {
        // При разработке это часто помогает отлавливать ошибки в консоли.
        // В продакшен-среде это также может быть полезно для отправки отчёта
        // об ошибках на сервер приложения.
        console.log(error);
      },
    );
  });
```

Спецификации
------------

| Specification |
| --- |
| Push API # dom-pushmanager-subscribe |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Смотрите также
--------------

* Using the Push API (en-US)
* Push Notifications on the Open Web, Matt Gaunt


SVGAElement.target - Интерфейсы веб API

SVGAElement.target
==================

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

Свойство **`SVGAElement.target`** только для чтения. `SVGAElement` возвращает объект `SVGAnimatedString` (en-US), указывающий часть целевого окна, фрейма, панели, в которой откроется при активации ссылки.

Это свойство используется, когда существует множество возможных целей для конечного ресурса, например, когда родительский документ является документом HTML или HTML-документом multi-frame.

Синтаксис
---------
```

myLink.target = 'value';
```

### Значение

`SVGAnimatedString` (en-US), укажет конечную цель ресурса для открытия документа при активации ссылки.
 Примеры значений можно найти здесь.

Пример
------

Код взят из «Пример кода SVGAElement».

```js
...
var linkRef = document.querySelector('a');
linkRef.target ='\_blank';
...
```

Характеристики
--------------

| Specification |
| --- |
| Scalable Vector Graphics (SVG) 2 # \_\_svg\_\_SVGAElement\_\_target |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Смотрите также
--------------

* `target (en-US)`


VRDisplayEvent: свойство display - Интерфейсы веб API

VRDisplayEvent: свойство display
================================

**Устарело:** Эта возможность была удалена из веб-стандартов. Хотя некоторые браузеры по-прежнему могут поддерживать её, она находится в процессе удаления. Не используйте её ни в старых, ни в новых проектах. Страницы или веб-приложения, использующие её, могут в любой момент сломаться.

**Не стандартно:** Эта возможность не является стандартной и стандартизировать её пока никто не собирается. Не используйте её на сайтах, смотрящих во внешний мир: она будет работать не у всех пользователей. Также могут присутствовать большие несовместимости между реализациями и её поведение может в будущем измениться.

Доступное только для чтения свойство **`display`** интерфейса `VRDisplayEvent` возвращает объект `VRDisplay` (en-US), связанный с этим событием.

**Примечание:** Это свойство было частью старого WebVR API, который был заменён на WebXR Device API.

### Значение

Объект `VRDisplay` (en-US).

Примеры
-------

```js
window.addEventListener("vrdisplaypresentchange", (e) => {
  console.log(
    `Устройство ${e.display.displayId} отображения было изменено по причине: ${e.reason}.`,
  );
});
```

Спецификации
------------

Это свойство было частью старого WebVR API, который был заменён на WebXR Device API. Больше не планируется сделать его стандартом.

Пока все браузеры не реализуют новые WebXR API (en-US), рекомендуется использовать такие фреймворки как A-Frame, Babylon.js, Three.js или полифил для разработки приложений WebXR, которые будут работать во всех браузерах.

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Смотрите также
--------------

* WebVR API


window.requestAnimationFrame() - Интерфейсы веб API

window.requestAnimationFrame()
==============================

**`window.requestAnimationFrame`** указывает браузеру на то, что вы хотите произвести анимацию, и просит его запланировать перерисовку на следующем кадре анимации. В качестве параметра метод получает функцию, которая будет вызвана перед перерисовкой.

**Примечание:** Ваш callback метод сам должен вызвать `requestAnimationFrame()` иначе анимация остановится.

Вы должны вызывать этот метод всякий раз, когда готовы обновить анимацию на экране, чтобы запросить планирование анимации. Обычно запросы происходят 60 раз в секунду, но чаще всего совпадают с частотой обновления экрана. В большинстве браузеров в фоновых вкладках или скрытых `<iframe>`, вызовы `requestAnimationFrame()` приостанавливаются, для того, чтобы повысить производительность и время работы батареи.

Callback методу передаётся один аргумент, `DOMHighResTimeStamp`, который содержит текущее время (количество миллисекунд, прошедших с момента time origin). Когда колбэки, отправленные в очередь с помощью `requestAnimationFrame()` начинают вызывать несколько колбэков в одном кадре, каждый получает одинаковый timestamp, хотя для вычисления каждого callback было затрачено время. Этот timestamp - десятичное число в миллисекундах, но с минимальной точностью в 1ms (1000 µs).

Синтаксис
---------

```js
window.requestAnimationFrame(callback);
```

### Параметры

`callback`

Функция, которая будет вызвана, когда придёт время обновить вашу анимацию на следующей перерисовке.

`element` Необязательный

Необязательный параметр (не используется в Firefox или IE), определяющий элемент, который визуально содержит всю анимацию. Для canvas'а и WebGL'a им должен быть `<canvas>`. Для других элементов вы можете опустить этот параметр для чуть лучшего пользовательского опыта.

### Возвращаемое значение

`requestID` — длинное целое, являющееся уникальным идентификатором для записи, содержащей callback. Оно не равно нулю, но других предположений о его значении делать не следует. Вы можете передать его в `window.cancelAnimationFrame()` для отмены вызова.

Пример
------

```js
var start = null;
var element = document.getElementById("SomeElementYouWantToAnimate");

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.transform =
    "translateX(" + Math.min(progress / 10, 200) + "px)";
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);
```

Спецификации
------------

| Specification |
| --- |
| HTML Standard # dom-animationframeprovider-requestanimationframe |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Смотрите также
--------------

* `window.mozAnimationStartTime`
* `window.cancelAnimationFrame()`
* mozRequestAnimationFrame - Blog post
* requestAnimationFrame for smart animating - Blog post
* Animating with javascript: from setInterval to requestAnimationFrame - Blog post


Document.registerElement() - Интерфейсы веб API

Document.registerElement()
==========================

Limited availability
--------------------

This feature is not Baseline because it does not work in some of the most widely-used browsers.

* Learn more
* See full compatibility
* Report feedback
Метод **`Document.registerElement()`** регистрирует новый кастомный элемент (custom element (en-US)) в браузере и возвращает конструктор для этого нового элемента.

**Примечание:** Это экспериментальная технология. Браузер который вы используете должен поддерживать Веб Компоненты (Web Components). Смотри больше: Enabling Web Components in Firefox.

**Примечание:** Устарело. Используйте интерфейс CustomElementRegistry (en-US) и его метод define(), чтобы зарегистрировать новый Тег.

Syntax
------
```

var constructor = document.registerElement(tag-name, options);
```

### Parameters

*tag-name*

Имя кастомного элемента. Имя должно содержать символ дефиса (-), например: `my-tag`.

*options Необязательный*

An object that names the prototype to base the custom element on, and an existing tag to extend. Both of these are optional.

Example
-------

Here is a very simple example:

```js
var Mytag = document.registerElement("my-tag");
```

Now the new tag is registered in the browser. The `Mytag` variable holds a constructor that you can use to create a `my-tag` element in the document as follows:

```js
document.body.appendChild(new Mytag());
```

This inserts an empty `my-tag` element that will be visible if you use the browser's developer tools. It will not be visible if you use the browser's view source capability. And it won't be visible in the browser unless you add some content to the tag. Here is one way to add content to the new tag:

```js
var mytag = document.getElementsByTagName("my-tag")[0];
mytag.textContent = "I am a my-tag element.";
```

Спецификации
------------

| Specification |
| --- |
| HTML Standard # dom-customelementregistry-define-dev |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Смотрите также
--------------

* Метод CustomElementRegistry.define


Using server-sent events - Интерфейсы веб API

Using server-sent events
========================

Разрабатывать web-приложения, использующие Server-Sent Events намного проще, чем с использованием websockets. Нужно всего лишь немного кода на стороне сервера, чтобы переправлять события web-приложению, но клиентская часть кода для обработки этих событий работает почти точно так же, как и для любых других событий.

Получение событий от сервера
----------------------------

Server-Sent Event API содержится внутри интерфейса `EventSource`. Чтобы открыть соединение с сервером для начала записи событий, которые он присылает, необходимо создать новый объект `EventSource`, который будет указывать на URI скрипта, который создаёт события. Например:

```js
const evtSource = new EventSource("ssedemo.php");
```

Если файл с генератором событий размещён на другом домене, то должен быть создан новый объект `EventSource` в который следует передать помимо URI ещё и словарь опций. Например, если предположить, что клиентский скрипт находится на `example.com`:

```js
const evtSource = new EventSource("//api.example.com/ssedemo.php", {
  withCredentials: true,
});
```

Как только вы создали экземпляр `EventSource`, вы можете начать получать сообщения с сервера, добавив обработчик события `message` :

```js
evtSource.onmessage = function (event) {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");

  newElement.innerHTML = "message: " + event.data;
  eventList.appendChild(newElement);
};
```

Этот код обрабатывает входящие сообщения (то есть уведомления от сервера, на которых нет поля `event`) и добавляет текст сообщения в список в HTML-документе.

Вы также можете обрабатывать события, используя `addEventListener()`:

```js
evtSource.addEventListener("ping", function (event) {
  const newElement = document.createElement("li");
  const time = JSON.parse(event.data).time;

  newElement.innerHTML = "ping at " + time;
  eventList.appendChild(newElement);
});
```

Этот код аналогичен коду выше, за исключением того, что он будет вызываться автоматически всякий раз, когда сервер отправляет сообщение с полем `event`, установленным в «ping»; затем он парсит JSON в поле `data` и выводит эту информацию.

Отправка событий с сервера
--------------------------

Код на стороне сервера, который отправляет события, должен отвечать, используя MIME-тип `text/event-stream`. Каждое уведомление отправляется в виде блока текста, оканчивающегося парой новых строк (`\n`) . Подробнее о формате потока событий см. Event stream format.

PHP код, который мы используем для примера приведён ниже:

```php
date\_default\_timezone\_set("America/New\_York");
header('Cache-Control: no-cache');
header("Content-Type: text/event-stream\n\n");

$counter = rand(1, 10);
while (1) {
  // Every second, send a "ping" event.

  echo "event: ping\n";
  $curDate = date(DATE\_ISO8601);
  echo 'data: {"time": "' . $curDate . '"}';
  echo "\n\n";

  // Send a simple message at random intervals.

  $counter--;

  if (!$counter) {
    echo 'data: This is a message at time ' . $curDate . "\n\n";
    $counter = rand(1, 10);
  }

  ob\_end\_flush();
  flush();
  sleep(1);
}
```

Приведённый выше код генерирует событие каждую секунду с типом события «ping». Данные каждого события - это объект JSON, содержащий метку времени ISO 8601, соответствующую дате, когда было сгенерировано событие. Через случайные интервалы отправляется простое сообщение (без типа `event`).

**Примечание:** вы можете найти полный пример, который использует код, показанный в этой статье на GitHub - см. Simple SSE demo using PHP.

Обработка ошибок
----------------

Когда возникают проблемы (такие как тайм-аут ответа сети или проблемы, связанные с контролем доступа (en-US)), тогда генерируется событие `error`. Вы можете обработать это событие программно, реализовав метод `onerror` для объекта `EventSource`:

```js
evtSource.onerror = function (error) {
  console.error("⛔ EventSource failed: ", error);
};
```

Закрытие потоков событий
------------------------

По умолчанию, если соединение между клиентом и сервером закрывается, то соединение сбрасывается. Прервать соединение можно с помощью метода `.close()`.

```js
evtSource.close();
```

Формат потока событий
---------------------

Поток событий представляет из себя простой поток текста, который должен иметь кодировку UTF-8 (en-US). Сообщения в потоке событий разделяются парой символов новой строки. Двоеточие, как первый символ строки считается комментарием и игнорируется.

**Примечание:** Строка комментария может использоваться, чтобы предотвратить тайм-аут соединений; сервер может периодически отправлять комментарий, чтобы поддерживать соединение.

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

### Поля

Каждое полученное сообщение имеет некоторую комбинацию следующих полей, по одному на строку:

`event`

Строка, идентифицирующая тип описанного события. Если `event` указан, то событие будет отправлено в браузер обработчику указанного имени события. Исходный код сайта должен использовать `addEventListener()` для обработки именованных событий. Обработчик `onmessage` вызывается, если для сообщения не указано имя события.

`data`

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

`id`

Идентификатор события для установки значения последнего ID события для объекта `EventSource`.

`retry`

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

Другие названия полей игнорируются.

**Примечание:** If a line doesn't contain a colon, the entire line is treated as the field name with an empty value string.

### Примеры

#### Сообщения с данными

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

Второе сообщение содержит поле данных со значением «some text». Третье сообщение содержит поле данных со значением «another message \n with two lines». Обратите внимание на специальный символ новой строки в значении.

```json
: this is a test stream

data: some text

data: another message
data:
```

#### Именованные события

Данный пример отправляет именованные события. У каждого из них есть имя события, указанное в поле `event`, и поле `data`, значением которого является соответствующая строка JSON с данными, необходимыми для клиента, чтобы реагировать на событие. Поле `data` может, конечно, содержать любые строковые данные; это не обязательно должен быть JSON.

```json
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
```

#### Смешивание и сопоставление

Вам не нужно использовать только неназванные сообщения или именованные события. Вы можете смешать их вместе в одном потоке событий.

```json
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

data: Here's a system message of some kind that will get used
data: to accomplish some task.

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
```

Совместимость с браузерами
--------------------------

### EventSource

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


AudioParam.setValueAtTime() - Интерфейсы веб API

AudioParam.setValueAtTime()
===========================

`setValueAtTime()` метод интерфейса `AudioParam` (en-US) позволяющий мгновенно точно по времени изменять значение `AudioParam` , сравнивая с `AudioContext.currentTime`. Новое значение даётся в значении параметра.

Синтаксис
---------
```

var AudioParam = AudioParam.setValueAtTime(value, startTime)
```

### Параметры

value

Число с плавающей точкой представляет значение AudioParam изменяемое в данное время.

startTime

A double representing the time (in seconds) after the `AudioContext` was first created that the change in value will happen. A `TypeError` is thrown if this value is negative.

### Returns

A reference to this `AudioParam` object. In some browsers older implementations of this interface return void.

Examples
--------

This simple example features a media element source with two control buttons (see our webaudio-examples repo for the source code, or view the example live). When the buttons are pressed, the `currGain` variable is incremented/decremented by 0.25, then the `setValueAtTime()` method is used to set the gain value equal to `currGain`, one second from now (`audioCtx.currentTime + 1`.)

```js
// create audio context
var AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();

// set basic variables for example
var myAudio = document.querySelector("audio");
var pre = document.querySelector("pre");
var myScript = document.querySelector("script");

pre.innerHTML = myScript.innerHTML;

var targetAtTimePlus = document.querySelector(".set-target-at-time-plus");
var targetAtTimeMinus = document.querySelector(".set-target-at-time-minus");

// Create a MediaElementAudioSourceNode
// Feed the HTMLMediaElement into it
var source = audioCtx.createMediaElementSource(myAudio);

// Create a gain node and set it's gain value to 0.5
var gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5;
var currGain = gainNode.gain.value;

// connect the AudioBufferSourceNode to the gainNode
// and the gainNode to the destination
source.connect(gainNode);
gainNode.connect(audioCtx.destination);

// set buttons to do something onclick
targetAtTimePlus.onclick = function () {
  currGain += 0.25;
  gainNode.gain.setValueAtTime(currGain, audioCtx.currentTime + 1);
};

targetAtTimeMinus.onclick = function () {
  currGain -= 0.25;
  gainNode.gain.setValueAtTime(currGain, audioCtx.currentTime + 1);
};
```

Specifications
--------------

| Specification |
| --- |
| Web Audio API # dom-audioparam-setvalueattime |

Совместимость с браузерами
--------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

See also
--------

* Using the Web Audio API (en-US)


Синхронизация видимости элемента с Intersection Observer API - Интерфейсы веб API

Синхронизация видимости элемента с Intersection Observer API
============================================================

Intersection Observer API позволяет в асинхронном режиме уведомлять приложение о том, что какой-то интересующий нас элемент в той или иной степени перекрыл родительский или другой элемент, в том числе `Document`. В этой статье мы построим пример блога, в котором в DOM динамически встраиваются рекламные блоки. Затем, с помощью Intersection Observer API, мы выясним, сколько времени показывается каждая отдельная реклама пользователю. Когда такая реклама показывается дольше, чем одну минуту, мы заменяем её на новую.

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

Есть важная причина, почему мы используем отслеживание видимости рекламы. Вышло так, что наиболее частое употребление Flash или скриптов в Web рекламе нужно для того, чтобы оценивать эффективность рекламы, а значит, её стоимость. Без Intersection Observer API эта задача свелась бы к повсеместному применению setTimeout и setInterval для каждой отдельной рекламы. Такие техники могут драматически ухудшить производительность страницы. Использование API в этом случае может позволит браузеру взять на себя обработку сложной логики и не только ускорит приложение, но и спасёт вас от ошибок, которые обязательно появятся при использовании setTimeout / setInterval.

Начнём!

Создадим сайт
-------------

### Структура приложения: HTML

Структура Web-приложений не очень сложна. Мы будем использовать CSS Grid для стилизации и макетирования, так что всё достаточно очевидно:

```html
<div class="wrapper">
  <header>
    <h1>A Fake Blog</h1>
    <h2>Showing Intersection Observer in action!</h2>
  </header>

  <aside>
    <nav>
      <ul>
        <li><a href="#link1">A link</a></li>
        <li><a href="#link2">Another link</a></li>
        <li><a href="#link3">One more link</a></li>
      </ul>
    </nav>
  </aside>

  <main></main>
</div>
```

Это заготовка для приложения. В верхней части приложения находится блок `<header>`. Ниже - боковая панель `<aside>`, заполненная ссылками. В самом конце структуры - основное тело приложения. Приложение стартует с пустым элементом `<main>` - он будет заполнен позже с помощью JavaScript.

### Стилизация приложения с помощью CSS

После определения структуры приложения мы переходим к стилизации. Давайте рассмотрим каждый компонент по отдельности.

#### Основа

Мы создаём стили для `<body>` и `<main>` так, чтобы определить фоновый цвет и сеточную систему.

```css
body {
  font-family: "Open Sans", "Arial", "Helvetica", sans-serif;
  background-color: aliceblue;
}

.wrapper {
  display: grid;
  grid-template-columns: auto minmax(min-content, 1fr);
  grid-template-rows: auto minmax(min-content, 1fr);
  max-width: 700px;
  margin: 0 auto;
  background-color: aliceblue;
}
```

Элемент приложения `<body>` сконфигурирован так, чтобы использовать общеупотребимый шрифт из семейства sans-serif и цвет `"aliceblue"` в качестве фона. Класс `"wrapper"` оборачивает всё приложение, включая header, sidebar и body content.

Обёртка определяет также CSS Grid сетку, как сетку из двух колонок и двух строк. Первая колонка (размер вычисляется автоматически на основе контента) используется для боковой панели, а вторая колонка (будет использоваться для основного содержимого) имеет ширину, достаточную, чтобы вместить содержимое и занять всю доступную площадь.

Первая строка будет использована для заголовка сайта. Размеры строк определяются схожим образом - размер первой вычисляется на основе контента, а второй - занимает всё доступной пространство, но не меньше размера, необходимого для показа содержимого.

Ширина обёртки зафиксирована - 700px, так что её размер будет удобен для представления приложения в MDN.

The wrapper's width is fixed at 700px so that it will fit in the available space when presented inline on MDN below.

#### Заголовок

Заголовок достаточно прост, так как в нашем примере он содержит небольшой текст.

```css
header {
  grid-column: 1 / -1;
  grid-row: 1;
  background-color: aliceblue;
}
```

Значение `grid-row` (en-US) равно 1, так как мы хотим поместить заголовок в верхнюю строку сеточной системы. Более интересно использование `grid-column`; Мы указываем здесь, что блок занимает пространство с первой колонки до первой с конца (то есть последней).

#### Боковая панель

Боковая панель используется для показа ссылок на другие страницы приложения. Ни одна из них не будет работать в нашем примере. Вся боковая панель нужна, чтобы просто приблизить внешний вид приложения к реальному блогу. Боковая панель создаётся с помощью элемента `<aside>`.

```css
aside {
  grid-column: 1;
  grid-row: 2;
  background-color: cornsilk;
  padding: 5px 10px;
}

aside ul {
  padding-left: 0;
}

aside ul li {
  list-style: none;
}

aside ul li a {
  text-decoration: none;
}
```

Важно отметить, что значение `grid-column` здесь установлено в 1 для того, чтобы поместить панель в левую часть экрана. Если вы поменяете это значение на "-1", то панель переместится вправо, однако, в этом случае, вам понадобится немного изменить стили внутренних элементов. Значение `grid-row` (en-US) равно 2, чтобы боковая панель заняла область вдоль области контента.

#### Область контента

Контент будет содержаться в элементе `<main>`.

```css
main {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  margin-left: 16px;
  font-size: 16px;
}
```

Главная особенность здесь - контент занимает вторую колонку и вторую строку.

#### Статьи

Каждая статья состоит из элемента `<article>`:

```css
article {
  background-color: white;
  padding: 6px;
}

article:not(:last-child) {
  margin-bottom: 8px;
}

article h2 {
  margin-top: 0;
}
```

Эти стили создают область с белым фоном с небольшими отступами как внутри области, так и между областями.

#### Рекламные блоки

Наконец, рекламные блоки. Нужно заметить, что каждый отдельный рекламный блок может изменять свои стили динамически (мы увидим это позже):

```css
.ad {
  height: 96px;
  padding: 6px;
  border-color: #555;
  border-style: solid;
  border-width: 1px;
}

.ad:not(:last-child) {
  margin-bottom: 8px;
}

.ad h2 {
  margin-top: 0;
}

.ad div {
  position: relative;
  float: right;
  padding: 0 4px;
  height: 20px;
  width: 120px;
  font-size: 14px;
  bottom: 30px;
  border: 1px solid black;
  background-color: rgba(255, 255, 255, 0.5);
}
```

Здесь нет никакой магии. Простой CSS.

### Совмещаем с JavaScript

Перейдём к JavaScript-коду, который всё оживит. Начнём с глобальных переменных:

```js
let contentBox;

let nextArticleID = 1;
let visibleAds = new Set();
let previouslyVisibleAds = null;

let adObserver;
let refreshIntervalID = 0;
```

Вот что здесь используется:

`contentBox`

Ссылка на элемент `<main>`. Это место, куда мы будем вставлять статьи и рекламу.

`nextArticleID`

Каждая статья получает уникальный цифровой ID. Эта переменная позволяет понять, какой следующий ID использовать.

`visibleAds`

`Set` используется для отслеживания текущих видимых на экране рекламных блоков.

`previouslyVisibleAds`

Используется для временного хранения списка рекламных блоков в то время, как документ невидим (например, если пользователь переключился на другой таб)

`adObserver`

Содержит экземпляр `IntersectionObserver`, используемый для вычисления наложения рекламных блоков и границ элемента `<main>`.

`refreshIntervalID`

Переменная для хранения ID интервала, который возвращается функцией `setInterval()`. Этот интервал будет использоваться для запуска периодических обновлений рекламных блоков.

#### Установка

Для первичного запуска приложения мы вызовем функцию `startup()`:

```js
window.addEventListener("load", startup, false);

function startup() {
  contentBox = document.querySelector("main");

  document.addEventListener("visibilitychange", handleVisibilityChange, false);

  let observerOptions = {
    root: null,
    rootMargin: "0px",
    threshold: [0.0, 0.75],
  };

  adObserver = new IntersectionObserver(intersectionCallback, observerOptions);

  buildContents();
  refreshIntervalID = window.setInterval(handleRefreshInterval, 1000);
}
```

Вначале мы получаем элемент `<main>`, в который можем вставлять содержимое. Затем мы устанавливаем обработчик на событие . Это событие срабатывает, когда документ меняет состояние между видим/невидим, например, когда пользователь переключается между табами. Intersection Observer API не должен засчитывать пересечение с элементом Main, если пользователь не будет в это время смотреть на вкладку. Таким образом, мы должны останавливать наши таймеры каждый раз, когда пользователь уходит со страницы. С помощью этого обработчика.

Затем мы устанавливаем параметры для `IntersectionObserver`. Параметры определяют, что IntersectionObserver должен отслеживать перекрытия с областью видимости документа (параметр `root` в значении `null`). У нас нет отступов для модификации корневой области; мы хотим отслеживать совпадение границ элементов и видимого документа именно для целей обнаружения перекрытий.

Параметр "порог" (`threshold`) содержит массив со значениями 0.0 и 0.75; Это заставит обработчик вызываться каждый раз, когда целевой элемент становится полностью обёрнут или только начинает выходить из зоны перекрытия (коэффициент перекрытия 0.0) или проходит порог в 75% видимости в обоих направлениях (коэффициент перекрытия 0.75).

Наблюдатель `adObserver` создаётся с помощью конструктора `IntersectionObserver`. В аргументы конструктора мы передаём колбэк-функцию (`intersectionCallback`) и ранее определённый объект параметров.

После этого мы вызываем функцию `buildContents()`. Её мы напишем чуть позже. Функция генерирует и вставляет в контейнер статьи и рекламные блоки.

В конце мы устанавливаем интервал, который каждую секунду запускает проверку - нужно ли что-то обновить. Нам необходимо второе обновление, так как в каждом отдельном рекламном блоке мы показываем таймер. В реальном приложении это не понадобится.

#### Обработка изменения видимости документа

Давайте рассмотрим обработчик события . Это событие срабатывает, когда документ становится видим или невидим. Как правило, это случается, когда пользователь переключается между табами. Так как Intersection Observer отслеживает только перекрытия элемента с корневым элементом, нам необходимо отдельно позаботиться о детекции видимости документа. Для этого мы используем Page Visibility API.

```js
function handleVisibilityChange() {
  if (document.hidden) {
    if (!previouslyVisibleAds) {
      previouslyVisibleAds = visibleAds;
      visibleAds = [];
      previouslyVisibleAds.forEach(function (adBox) {
        updateAdTimer(adBox);
        adBox.dataset.lastViewStarted = 0;
      });
    }
  } else {
    previouslyVisibleAds.forEach(function (adBox) {
      adBox.dataset.lastViewStarted = performance.now();
    });
    visibleAds = previouslyVisibleAds;
    previouslyVisibleAds = null;
  }
}
```

Так как событие само по себе не указывает, стал ли документ видимым или, наоборот, невидимым, мы должны вручную проверить свойство `document.hidden`. В теории, это событие может сработать несколько раз, поэтому нам нужно обрабатывать только те рекламные блоки, учёт которых ещё не был приостановлен.

Для остановки таймеров нам нужно удалить ссылки на рекламные блоки из коллекции `visibleAds` и пометить их как неактивные. Чтобы это сделать, мы начинаем с сохранения ссылок на текущие видимые элементы в переменную `previouslyVisibleAds`. Это нужно, чтобы в дальнейшем можно было восстановить счётчики для этих блоков. Так мы указываем приложению, что эту рекламу не надо считать активной. Затем, если пользователь вернулся в документ, мы вызываем функцию `updateAdTimer()` для каждого отложенного элемента. Эта функция обновляет общее время видимости элемента. После этого мы присваиваем переменной `dataset.lastViewStarted` значение 0, что означает, что таймер не запущен.

Если документ стал видимым, мы выполняем обратный процесс: сначала мы проходим через коллекцию `previouslyVisibleAds`. Для каждого элемента мы присваиваем `dataset.lastViewStarted` значение, соответствующее текущему времени документа (в миллисекундах с момента создания документа). Это время можно узнать с помощью метода `performance.now()`. Затем мы присваиваем переменной `visibleAds` закешированное ранее значение `previouslyVisibleAds`, с обнулением последней переменной. Теперь рекламные блоки перезапущены и настроены, так что время простоя не будет учитываться.

#### Обработчик изменений наложения

При каждой итерации в браузерном event loop, каждый наблюдатель `IntersectionObserver` проверяет, не прошёл ли какой-либо из элементов-целей через пороговые значения наблюдателя. Для каждого наблюдателя список таких целей собирается в один список и отправляется в колбэк-функцию наблюдателя. Каждый элемент списка - это `IntersectionObserverEntry` (en-US) объект. В нашем приложении `intersectionCallback()` выглядит так:

```js
function intersectionCallback(entries) {
  entries.forEach(function (entry) {
    let adBox = entry.target;

    if (entry.isIntersecting) {
      if (entry.intersectionRatio >= 0.75) {
        adBox.dataset.lastViewStarted = entry.time;
        visibleAds.add(adBox);
      }
    } else {
      visibleAds.delete(adBox);
      if (
        entry.intersectionRatio === 0.0 &&
        adBox.dataset.totalViewTime >= 60000
      ) {
        replaceAd(adBox);
      }
    }
  });
}
```

Как мы упоминали ранее, колбэк-функция `IntersectionObserver` получает на вход массив элементов, которые активировали наблюдателя. В нашей функции мы итерируемся по этому массиву. Если элемент пересекается с корневым элементом, мы знаем, что он стал видимым. Если он становится видимым более, чем на 75%, мы считаем, что реклама видима и мы запускаем таймер, выставляя значение `dataset.lastViewStarted` равным времени изменения параметра перекрытия `entry.time` (en-US). Затем мы добавляем рекламный блок в набор `visibleAds`.

Если рекламный блок уходит из зоны видимости, мы удаляем его из набор видимых элементов. Затем, в зависимости от значения `entry.ratio` (en-US), мы либо меняем рекламу, либо ставим на паузу. Так, если значение равно 0.0 и реклама уже была видна минимум минуту, мы вызываем функцию `replaceAd()` . В этом случае пользователь видит разные рекламные блоки, но сама реклама меняется незаметно для пользователя.

#### Обработка периодический событий

Каждую секунду у нас срабатывает интервал `handleRefreshInterval()`, который мы задали в функции `startup()`. Главная задача этого интервала - обновлять таймеры каждую секунду и перерисовывать значение таймеров.

```js
function handleRefreshInterval() {
  let redrawList = [];

  visibleAds.forEach(function (adBox) {
    let previousTime = adBox.dataset.totalViewTime;
    updateAdTimer(adBox);

    if (previousTime != adBox.dataset.totalViewTime) {
      redrawList.push(adBox);
    }
  });

  if (redrawList.length) {
    window.requestAnimationFrame(function (time) {
      redrawList.forEach(function (adBox) {
        drawAdTimer(adBox);
      });
    });
  }
}
```

Массив `redrawList` используется для хранения списка рекламных блоков, которые должны быть перерисованы в следующем цикле перерисовки. Это нужно, так как таймеры текущих рекламных блоков не всегда совпадают с реальными таймингами из-за прочих системных процессов. Или из-за того, что вы указали в качестве интервала промежуток не в 1000мс.

Затем, для каждого видимого рекламного блока, мы сохраняем значение `dataset.totalViewTime` (количество миллисекунд, которое текущая реклама была видима с момента последнего обновления этого значения). После этого вызываем функцию `updateAdTimer()` для обновления времени. Если оно изменилось, мы вставляем рекламный блок в список `redrawList`. Таким образом, при обработке следующего кадра приложение знает, что нужно перерисовать.

И, наконец, если существует хоть один элемент, который нужно перерисовать, мы будем используем `requestAnimationFrame()`, чтобы отложить отрисовку каждого элемента на тот момент, когда будет формироваться следующий кадр.

#### Обновление таймера видимости рекламы

Ранее мы уже видели, что если нам нужно обновить общее время видимости рекламы - мы вызываем функцию `updateAdTimer()`. Эта функция принимает в качестве аргумента объект `HTMLDivElement`.

```js
function updateAdTimer(adBox) {
  let lastStarted = adBox.dataset.lastViewStarted;
  let currentTime = performance.now();

  if (lastStarted) {
    let diff = currentTime - lastStarted;

    adBox.dataset.totalViewTime =
      parseFloat(adBox.dataset.totalViewTime) + diff;
  }

  adBox.dataset.lastViewStarted = currentTime;
}
```

Для отслеживания времени видимости элемента мы используем два data-атрибута на каждом рекламном блоке:

`lastViewStarted`

Время в миллисекундах относительно первоначальной загрузки страницы до момента, когда счётчик рекламного блока был обновлён или блок стал невидим. Если значение равно нулю - блок не был видим в последний раз, когда проверялся.

`totalViewTime`

Общее время видимости рекламного блока.

Значение этих атрибутов можно получить с помощью `HTMLElement.dataset` (en-US). Значения - строки, но вы можете конвертировать их в числа. Фактически, JavaScript делает это автоматически, но нам всё равно придётся в одном месте сделать это вручную.

Функция начинается с выяснения времени, когда происходила последняя проверка видимости рекламы (`adBox.dataset.lastViewStarted`). Мы также получаем текущее время с момента создания документа с помощью `performance.now()` `currentTime`.

Если время последней проверки lastStarted не равно нулю - это значит, что таймер сейчас уже запущен. В этом случае мы вычисляем разницу между текущим временем и временем старта проверки. Это значение покажет, сколько реклама была видима с момента последнего старта детекции. Затем это значение прибавляем к уже имеющемуся `totalViewTime`. Обратите внимание не вызов `parseFloat()`: так как все значения из Dataset - строки, JavaScript пытается соединить строки вместо того, чтобы просуммировать числа.

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

#### Показываем таймер рекламы

Внутри каждого рекламного блока мы отображаем текущее значение общего времени видимости в формате мин:сек. Для этого мы передаём в функцию `drawAdTimer` контейнер:

```js
function drawAdTimer(adBox) {
  let timerBox = adBox.querySelector(".timer");
  let totalSeconds = adBox.dataset.totalViewTime / 1000;
  let sec = Math.floor(totalSeconds % 60);
  let min = Math.floor(totalSeconds / 60);

  timerBox.innerText = min + ":" + sec.toString().padStart(2, "0");
}
```

Функция находит внутри переданного контейнера блок с классом `timer`. Затем забирает данные о текущем общем времени видимости блока. С помощью деления на 1000, 60 и 60 мы преобразуем результат в нужный формат (миллисекунды -> секунды -> минуты / секунды)

Метод `String.padStart()` используется для того, чтобы убедиться, что число секунд всегда состоят из двух цифр.

#### Строим содержимое страницы

Функция `buildContents()` вызывается при старте приложения. Она формирует тело статьи и добавляет рекламные блоки:

```js
let loremIpsum =
  "<p>Lorem ipsum dolor sit amet, consectetur adipiscing" +
  " elit. Cras at sem diam. Vestibulum venenatis massa in tincidunt" +
  " egestas. Morbi eu lorem vel est sodales auctor hendrerit placerat" +
  " risus. Etiam rutrum faucibus sem, vitae mattis ipsum ullamcorper" +
  " eu. Donec nec imperdiet nibh, nec vehicula libero. Phasellus vel" +
  " malesuada nulla. Aliquam sed magna aliquam, vestibulum nisi at," +
  " cursus nunc.</p>";

function buildContents() {
  for (let i = 0; i < 5; i++) {
    contentBox.appendChild(createArticle(loremIpsum));

    if (!(i % 2)) {
      loadRandomAd();
    }
  }
}
```

Переменная `loremIpsum` содержит текст, который мы используем как тело статьи. Разумеется, в реальном мире вы будете забирать статьи из какой-то базы данных. Но это тема другой статьи, поэтому мы пошли простым путём.

`buildContents()` создаёт страницу с пятью статьями. Каждая нечётная статья содержит рекламные блоки. Статьи будут вставлены в блок контента `<main>`. после того, как будет вызван метод `createArticle()`, который мы разберём позже.

Рекламные блоки создаются с помощью функции `loadRandomAd()`. Эта функция создаёт и вставляет блоки одновременно. Как мы увидим позже, эта же функция может и заменить уже существующую рекламу. Но пока что просто добавим рекламу в существующий текст.

#### Создаём статью

Для создания элемента статьи `<article>` и её содержимого мы используем функцию `createArticle()`, которая в качестве входных данных принимает строку-текст статьи.

```js
function createArticle(contents) {
  let articleElem = document.createElement("article");
  articleElem.id = nextArticleID;

  let titleElem = document.createElement("h2");
  titleElem.id = nextArticleID;
  titleElem.innerText = "Article " + nextArticleID + " title";
  articleElem.appendChild(titleElem);

  articleElem.innerHTML += contents;
  nextArticleID += 1;

  return articleElem;
}
```

Сперва, элемент `<article>` создаётся и ему присваивается уникальный ID `nextArticleID` (это просто счётчик от нуля до бесконечности). Затем мы создаём и добавляем элемент `<h2>` (en-US) для заголовка и применяем HTML из переменной `contents`. Наконец, мы увеличиваем значение `nextArticleID` (таким образом, следующий элемент получит уникальный ID) и возвращаем элемент статьи обратно.

#### Создание рекламного блока

Функция `loadRandomAd()` имитирует загрузку рекламы и её добавление на страницу. Если вы не указываете значение для `replaceBox`, создаётся и применяется новый контейнер для рекламы. Если вы указали `replaceBox`, этот контейнер рассматривается, как уже существующий элемент. Вместо создания нового, существующий элемент изменяется, чтобы содержать актуальные данные. Это помогает избежать риска неэффективной перерисовки элементов, если вы сначала будете удалять элемент из DOM, а затем вставлять новый.

```js
function loadRandomAd(replaceBox) {
  let ads = [
    {
      bgcolor: "#cec",
      title: "Eat Green Beans",
      body: "Make your mother proud—they're good for you!",
    },
    {
      bgcolor: "aquamarine",
      title: "MillionsOfFreeBooks.whatever",
      body: "Read classic literature online free!",
    },
    {
      bgcolor: "lightgrey",
      title: "3.14 Shades of Gray: A novel",
      body: "Love really does make the world go round...",
    },
    {
      bgcolor: "#fee",
      title: "Flexbox Florist",
      body: "When life's layout gets complicated, send flowers.",
    },
  ];
  let adBox, title, body, timerElem;

  let ad = ads[Math.floor(Math.random() \* ads.length)];

  if (replaceBox) {
    adObserver.unobserve(replaceBox);
    adBox = replaceBox;
    title = replaceBox.querySelector(".title");
    body = replaceBox.querySelector(".body");
    timerElem = replaceBox.querySelector(".timer");
  } else {
    adBox = document.createElement("div");
    adBox.className = "ad";
    title = document.createElement("h2");
    body = document.createElement("p");
    timerElem = document.createElement("div");
    adBox.appendChild(title);
    adBox.appendChild(body);
    adBox.appendChild(timerElem);
  }

  adBox.style.backgroundColor = ad.bgcolor;

  title.className = "title";
  body.className = "body";
  title.innerText = ad.title;
  body.innerHTML = ad.body;

  adBox.dataset.totalViewTime = 0;
  adBox.dataset.lastViewStarted = 0;

  timerElem.className = "timer";
  timerElem.innerText = "0:00";

  if (!replaceBox) {
    contentBox.appendChild(adBox);
  }

  adObserver.observe(adBox);
}
```

Вначале мы определяем массив `ads`. Этот массив содержит данные, необходимые для создания рекламных блоков. В реальном приложении, конечно, мы будем загружать эти данные из базы или, что более вероятно, из рекламного сервиса, который будет использовать какой-то API. Тем не менее, наша простая задача решается: каждый рекламный блок представлен тремя свойствами: фоновым цветом (`bgcolor`), заголовком (`title`) и текстовым содержимым (`body`).

Затем мы определяем несколько переменных:

`adBox`

Определяет контейнер, содержащий рекламу. Вновь добавленные рекламные блоки будут добавлены к странице с помощью`Document.createElement()`. Когда замещается существующая реклама, в этой переменной указан элемент (`replaceBox`).

`title`

Содержит ссылку на элемент `<h2>` (en-US).

`body`

Содержит ссылку на элемент `<p>`.

`timerElem`

Содержит ссылку на элемент таймера `<div>`.

Случайный рекламный блок вычисляется с помощью `Math.floor(Math.random() * ads.length)`. Результат этой функции - целое число между 0 и максимальным количеством рекламных блоков. Соответствующий рекламный блок теперь доступен нам из переменной `adBox`.

Если `replaceBox` содержит какое-то значение, мы рассматриваем его как элемент рекламного блока. Мы завершаем наблюдение за элементом с помощью `IntersectionObserver.unobserve()` (en-US). Затем собираем в локальные переменные данные из каждого свойства элемента: заголовок, тело и таймер.

Если никакое значение не указано для `replaceBox`, мы создаём новый элемент. Создаётся новый контейнер `<div>`. Его CSS-параметры задаются с помощью класса `"ad"`. Затем создаются заголовок рекламного блока, его текст и таймер. Соответственно, это `<h2>` (en-US), `<p>` и `<div>`. Эти элементы применяются к контейнеру `adBox`.

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

Наступаем время присвоить data-параметры, чтобы отслеживать видимость рекламных блоков с помощью установки `adBox.dataset.totalViewTime` и `adBox.dataset.lastViewStarted` равными нулю.

Наконец, мы устанавливаем CSS-класс контейнеру таймера. С помощью этого класса приложение сможет с лёгкостью собирать данные и обновлять их для каждого таймера. По умолчанию, текст этого контейнера - "0:00".

Если мы создаём новую рекламу, мы должны применить элемент к страницы с помощью `Document.appendChild()`. Если мы лишь заменяем рекламный блок - он уже представлен в DOM и всё, что нам нужно сделать - это обновить его. Затем мы вызываем функцию `observe()` (en-US). `adObserver` начинает отслеживать изменения перекрытия элементов в видимой области приложения. С этого момента любой рекламный блок, который становится на 100% скрыт или хотя бы на один пиксель видим или преодолевает порог в 75% видимости в любом направлении, запускает вычисление таймингов и обновление содержимого таймеров.

#### Замена существующей рекламы

Наша функция обработки перекрытия отслеживает рекламные блоки. Когда они становятся на 100% и общее время их видимости достаточное для того, рекламный блок заменяется на новый. Когда это происходит, вызывается функция `replaceAd()`.

```js
function replaceAd(adBox) {
  let visibleTime;

  updateAdTimer(adBox);

  visibleTime = adBox.dataset.totalViewTime;
  console.log(
    " Replacing ad: " +
      adBox.querySelector("h2").innerText +
      " - visible for " +
      visibleTime,
  );

  loadRandomAd(adBox);
}
```

`replaceAd()` начинается с вызова `updateAdTimer()` для существующего рекламного блока, чтобы убедиться, что таймер обновлён. С помощью этого вызова мы убеждаемся, что `totalViewTime`, который мы используем для обработки, действительно совпадает с тем, что видел пользователь. Мы логируем это значение и загружаем в рекламный блок новые данные. Помните, что в реальном мире вы не должны логировать подобные вещи, а скорее использовать API для сбор логов.

Результат
---------

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

Смотрите также
--------------

* Intersection Observer API
* Page Visibility API