Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.

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

Web Workers API

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

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

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

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

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

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

Выделенные Workers

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

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

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

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

if (window.Worker) {

  ...

}

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

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

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

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

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

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-а:

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

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

Заметка: URI, переданный как параметр конструктору Worker, должен подчинаться same-origin политике.

В данный момент существуют расхождения между производителями браузеров о том, какие URI являются same-origin; Gecko 10.0 (Firefox 10.0 / Thunderbird 10.0 / SeaMonkey 2.7) и более поздние версии позволяют использовать data URI в качестве worker, в то время, как Internet Explorer 10 не позволяет для этих целей использовать Blob URI.

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

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

Завершение worker-а

Если необходимо немедленно завершить запущенный worker из главного потока, вы можете сделать так вызвав метод worker-а terminate:

myWorker.terminate();

Поток worker-а прекращается немедленно без возможности завершить свои операции или убрать за собой.

В потоке worker-а, worker-ы могут закрыть себя, вызвав собственный метод close:

close();

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

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

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

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

message
Сообщение об ошибке в читаемом виде.
filename
Имя файла со скриптом, в котором ошибка произошла.
lineno
Номер строки в файле, в котором произошла ошибка.

Создание subworkers

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

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

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

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

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

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

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

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

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

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

Заметка: В Firefox разделяемый worker не может быть использован совместно документами в приватном и не приватном окне (баг 1177621).

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

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

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

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

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

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

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

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

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

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

Теперь на стороне worker-а. Здесь код немного сложнее (worker.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):

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

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

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

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

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

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

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

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

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 содержит данные передаваемые из работника.

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

var myWorker = new Worker("my_task.js");

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

myWorker.postMessage("ali");

my_task.js (работник):

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

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

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

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

Пример #1: Создание универсального "asynchronous eval()"

Следующий пример показывает как использовать работника для асинхронного выполнения любого javaScript кода, разрешенного в работнике, путем вызова eval() в нем.

// Syntax: asyncEval(code[, listener])

var asyncEval = (function () {
  var aListeners = [], oParser = new Worker("data:text/javascript;charset=US-ASCII,onmessage%20%3D%20function%20%28oEvent%29%20%7B%0A%09postMessage%28%7B%0A%09%09%22id%22%3A%20oEvent.data.id%2C%0A%09%09%22evaluated%22%3A%20eval%28oEvent.data.code%29%0A%09%7D%29%3B%0A%7D");

  oParser.onmessage = function (oEvent) {
    if (aListeners[oEvent.data.id]) { aListeners[oEvent.data.id](oEvent.data.evaluated); }
    delete aListeners[oEvent.data.id];
  };

  return function (sCode, fListener) {
    aListeners.push(fListener || null);
    oParser.postMessage({
      "id": aListeners.length - 1,
      "code": sCode
    });
  };
})();

data URL  эквивалентны сетевому запросу, со следующим ответом:

onmessage = function (oEvent) {
  postMessage({
    "id": oEvent.data.id,
    "evaluated": eval(oEvent.data.code)
  });
}

Пример использования:

// asynchronous alert message...
asyncEval("3 + 2", function (sMessage) {
    alert("3 + 2 = " + sMessage);
});

// asynchronous print message...
asyncEval("\"Hello World!!!\"", function (sHTML) {
    document.body.appendChild(document.createTextNode(sHTML));
});

// asynchronous void...
asyncEval("(function () {\n\tvar oReq = new XMLHttpRequest();\n\toReq.open(\"get\", \"http://www.mozilla.org/\", false);\n\toReq.send(null);\n\treturn oReq.responseText;\n})()");

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

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

example.html (the main page):

 

MDN Example - Queryable worker

my_task.js (the worker):

// your custom PRIVATE functions

function myPrivateFunc1 () {
  // do something
}

function myPrivateFunc2 () {
  // do something
}

// etc. etc.

// your custom PUBLIC functions (i.e. queryable from the main page)

var queryableFunctions = {
  // example #1: get the difference between two numbers:
  getDifference: function (nMinuend, nSubtrahend) {
      reply("printSomething", nMinuend - nSubtrahend);
  },
  // example #2: wait three seconds
  waitSomething: function () {
      setTimeout(function() { reply("alertSomething", 3, "seconds"); }, 3000);
  }
};

// system functions

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

function reply (/* listener name, argument to pass 1, argument to pass 2, etc. etc */) {
  if (arguments.length < 1) { throw new TypeError("reply - not enough arguments"); return; }
  postMessage({ "vo42t30": arguments[0], "rnb93qh": Array.prototype.slice.call(arguments, 1) });
}

onmessage = function (oEvent) {
  if (oEvent.data instanceof Object && oEvent.data.hasOwnProperty("bk4e1h0") && oEvent.data.hasOwnProperty("ktp3fm1")) {
    queryableFunctions[oEvent.data.bk4e1h0].apply(self, oEvent.data.ktp3fm1);
  } else {
    defaultQuery(oEvent.data);
  }
};

It is possible to switch the content of each mainpage -> worker and worker -> mainpage message.

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

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

// 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.

Встроенные работники

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

 

MDN Example - Embedded worker

Встраиваемый работник теперь внесен в новое custom свойство document.worker

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

 

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

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

Workers are mainly useful for allowing your code to perform processor-intensive calculations without blocking the user interface thread. In this example, a worker is used to calculate Fibonacci numbers.

The JavaScript code

The following JavaScript code is stored in the "fibonacci.js" file referenced by the HTML in the next section.

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);
  }
 };

The worker sets the property onmessage to a function which will receive messages sent when the worker object's postMessage() is called (note that this differs from defining a global variable of that name, or defining a function with that name. var onmessage and function onmessage will define global properties with those names, but they will not register the function to receive messages sent by the web page that created the worker). This starts the recursion, spawning new copies of itself to handle each iteration of the calculation.

HTML код

 

Test threads fibonacci

 

The web page creates a div element with the ID result , which gets used to display the result, then spawns the worker. After spawning the worker, the onmessage handler is configured to display the results by setting the contents of the div element, and the onerror handler is set to dump the error message.

Finally, a message is sent to the worker to start it.

Try this example.

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

You can find an example of this in the article Using workers in extensions .

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

As multi-core computers become increasingly common, it's often useful to divide computationally complex tasks among multiple workers, which may then perform those tasks on multiple-processor cores.

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

In addition to dedicated and shared web workers, there are other types of worker available:

  • ServiceWorkers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs.
  • Chrome Workers are a Firefox-only type of worker that you can use if you are developing add-ons and want to use workers in extensions and have access to js-ctypes in your worker. See ChromeWorker for more details.
  • Audio Workers provide the ability for direct scripted audio processing to be done in a web worker context.

Функции и интерфейсы доступные в worker-ах

You can use most standard JavaScript features inside a web worker, including:

The main thing you can't do in a Worker is directly affect the parent page. This includes manipulating the DOM and using that page's objects. You have to do it indirectly, by sending a message back to the main script via DedicatedWorkerGlobalScope.postMessage, then actioning the changes from there.

Note: For a complete list of functions available to workers, see Functions and interfaces available to workers.

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

Specification Status Comment
HTML Living Standard Живой стандарт No change from Unknown.
Unknown Неизвестно Initial definition.

Браузерная совместимость

We're converting our compatibility data into a machine-readable JSON format. This compatibility table still uses the old format, because we haven't yet converted the data it contains. Find out how you can help!

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Basic support 4[1] 3.5 (1.9.1) 10.0 10.6[1] 4[2]
Shared workers 4[1] 29 (29) Нет 10.6 5
Нет 6.1[4]
Passing data using structured cloning 13 8 (8) 10.0 11.5 6
Passing data using transferable objects 17 webkit
21
18 (18) Нет 15 6
Global URL 10[3]
23
21 (21) 11 15 6[3]
Feature Android Chrome for Android Firefox Mobile (Gecko) Firefox OS (Gecko) IE Phone Opera Mobile Safari Mobile
Basic support 4.4 4[1] 3.5 1.0.1 10.0 11.5[1] 5.1[2]
Shared workers Нет 4[1] 8 1.0.1 Нет Нет Нет
Passing data using structured cloning Нет 4 8 1.0.1 Нет Нет Нет
Passing data using transferable objects Нет Нет 18 1.0.1 Нет Нет Нет

[1] Chrome and Opera give an error "Uncaught SecurityError: Failed to construct 'Worker': Script at 'file:///Path/to/worker.js' cannot be accessed from origin 'null'." when you try to run a worker locally. It needs to be on a proper domain.

[2] As of Safari 7.1.2, you can call console.log from inside a worker, but it won't print anything to the console. Older versions of Safari don't allow you to call console.log from inside a worker.

[3] This feature is implemented prefixed as webkitURL.

[4] Safari removed SharedWorker support.

Смотри также

Метки документа и участники

Метки: 
Обновлялась последний раз: citizen55,