diff --git a/2-ui/1-document/11-modifying-document/article.md b/2-ui/1-document/11-modifying-document/article.md index 9fe5ea49..00d3ad84 100644 --- a/2-ui/1-document/11-modifying-document/article.md +++ b/2-ui/1-document/11-modifying-document/article.md @@ -87,7 +87,7 @@ div.innerHTML = "Ура! Вы прочитали это важ
`parentElem.insertBefore(elem, nextSibling)`
-
Вставляет `elem` в список дочерних `parentElem` перед элементом `nextSibling`. +
Вставляет `elem` в коллекцию детей `parentElem`, перед элементом `nextSibling`. Следующий код вставляет новый элемент перед вторым `
  • `: diff --git a/2-ui/1-document/4-traversing-dom/article.md b/2-ui/1-document/4-traversing-dom/article.md index 4e153737..e84f525f 100644 --- a/2-ui/1-document/4-traversing-dom/article.md +++ b/2-ui/1-document/4-traversing-dom/article.md @@ -274,7 +274,7 @@ for (var key in elems) { ```js elem.firstElementChild === elem.children[0] -elem.lastElementChild === body.children[body.children.length - 1] +elem.lastElementChild === elem.children[elem.children.length - 1] ``` @@ -301,19 +301,19 @@ elem.lastElementChild === body.children[body.children.length - 1]
    `TABLE`
      -
    • **`table.rows`** -- список строк `TR` таблицы.
    • +
    • **`table.rows`** -- коллекция строк `TR` таблицы.
    • `table.caption/tHead/tFoot` -- ссылки на элементы таблицы `CAPTION`, `THEAD`, `TFOOT`.
    • -
    • `table.tBodies` -- список элементов таблицы `TBODY`, по спецификации их может быть несколько.
    • +
    • `table.tBodies` -- коллекция элементов таблицы `TBODY`, по спецификации их может быть несколько.
    `THEAD/TFOOT/TBODY`
      -
    • `tbody.rows` -- список строк `TR` секции.
    • +
    • `tbody.rows` -- коллекция строк `TR` секции.
    `TR`
      -
    • **`tr.cells`** -- список ячеек `TD/TH`
    • +
    • **`tr.cells`** -- коллекция ячеек `TD/TH`
    • **`tr.sectionRowIndex`** -- номер строки в текущей секции `THEAD/TBODY`
    • `tr.rowIndex` -- номер строки в таблице
    diff --git a/2-ui/1-document/5-searching-elements-dom/2-tree/solution.md b/2-ui/1-document/5-searching-elements-dom/2-tree/solution.md deleted file mode 100644 index 55b45737..00000000 --- a/2-ui/1-document/5-searching-elements-dom/2-tree/solution.md +++ /dev/null @@ -1,18 +0,0 @@ -Сделаем цикл по узлам `
  • `: - -```js -var lis = document.getElementsByTagName('li'); - -for (i = 0; i < lis.length; i++) { - ... -} -``` - -В цикле для каждого `lis[i]` можно получить текст, используя свойство `firstChild`. Ведь первым в `
  • ` является как раз текстовый узел, содержащий текст названия. - -Также можно получить количество потомков, используя `lis[i].getElementsByTagName('li')`. - -Напишите код с этой подсказкой. - -Если уж не выйдет -- тогда откройте решение. - diff --git a/2-ui/1-document/5-searching-elements-dom/2-tree/solution.view/index.html b/2-ui/1-document/5-searching-elements-dom/2-tree/solution.view/index.html deleted file mode 100644 index e9b5d644..00000000 --- a/2-ui/1-document/5-searching-elements-dom/2-tree/solution.view/index.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - -
      -
    • Животные -
        -
      • Млекопитающие -
          -
        • Коровы
        • -
        • Ослы
        • -
        • Собаки
        • -
        • Тигры
        • -
        -
      • -
      • Другие -
          -
        • Змеи
        • -
        • Птицы
        • -
        • Ящерицы
        • -
        -
      • -
      -
    • -
    • Рыбы -
        -
      • Аквариумные -
          -
        • Гуппи
        • -
        • Скалярии
        • -
        - -
      • -
      • Морские -
          -
        • Морская форель
        • -
        -
      • -
      -
    • -
    - - - - - - \ No newline at end of file diff --git a/2-ui/1-document/5-searching-elements-dom/2-tree/source.view/index.html b/2-ui/1-document/5-searching-elements-dom/2-tree/source.view/index.html deleted file mode 100644 index 2f45460c..00000000 --- a/2-ui/1-document/5-searching-elements-dom/2-tree/source.view/index.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - -
      -
    • Животные -
        -
      • Млекопитающие -
          -
        • Коровы
        • -
        • Ослы
        • -
        • Собаки
        • -
        • Тигры
        • -
        -
      • -
      • Другие -
          -
        • Змеи
        • -
        • Птицы
        • -
        • Ящерицы
        • -
        -
      • -
      -
    • -
    • Рыбы -
        -
      • Аквариумные -
          -
        • Гуппи
        • -
        • Скалярии
        • -
        - -
      • -
      • Морские -
          -
        • Морская форель
        • -
        -
      • -
      -
    • -
    - - - - - - \ No newline at end of file diff --git a/2-ui/1-document/5-searching-elements-dom/2-tree/task.md b/2-ui/1-document/5-searching-elements-dom/2-tree/task.md deleted file mode 100644 index 91980462..00000000 --- a/2-ui/1-document/5-searching-elements-dom/2-tree/task.md +++ /dev/null @@ -1,14 +0,0 @@ -# Дерево - -[importance 5] - -Есть дерево [edit src="source"]в песочнице[/edit]. - -Напишите код, который для каждого элемента списка(`LI`) выведет: -
      -
    1. Текст в нём.
    2. -
    3. Количество вложенных в него элементов.
    4. -
    - -[demo src="solution"] - diff --git a/2-ui/1-document/6-searching-elements-internals/article.md b/2-ui/1-document/6-searching-elements-internals/article.md index bc5c7cfa..d2935b7c 100644 --- a/2-ui/1-document/6-searching-elements-internals/article.md +++ b/2-ui/1-document/6-searching-elements-internals/article.md @@ -52,11 +52,11 @@ ``` -Как видно, длина коллекции, найденной через `querySelectorAll`, осталась прежней. А длина списка, возвращённого `getElementsByTagName`, изменилась. +Как видно, длина коллекции, найденной через `querySelectorAll`, осталась прежней. А длина коллекции, возвращённой `getElementsByTagName`, изменилась. -Дело в том, что результат запросов `getElementsBy*` -- это не массив, а специальный объект, имеющий тип NodeList или HTMLCollection. Он похож на массив, так как имеет нумерованные элементы и длину, но внутри это не готовый список, а "живой поисковой запрос". +Дело в том, что результат запросов `getElementsBy*` -- это не массив, а специальный объект, имеющий тип NodeList или HTMLCollection. Он похож на массив, так как имеет нумерованные элементы и длину, но внутри это не готовая коллекция, а "живой поисковой запрос". -Собственно поиск выполняется только при обращении к элементам списка или к его длине. +Собственно поиск выполняется только при обращении к элементам коллекции или к её длине. ## Алгоритмы getElementsBy* @@ -132,7 +132,7 @@ alert( elems.length ); Если точнее -- будут сброшены все коллекции, привязанные к элементам вверх по иерархии от непосредственного родителя нового `div` и выше, то есть такие, которые потенциально могли измениться. И только они.
  • Во-вторых, если добавлен только `div`, то не будут сброшены запомненные элементы для поиска по другим тегам, например `elem.getElementsByTagName('a')`.
  • -
  • ...И, конечно же, не любые изменения DOM приводят к сбросу кешей, а только те, которые могут повлиять на список. Если где-то добавлен новый атрибут -- с поиском по тегу ничего не произойдёт.
  • +
  • ...И, конечно же, не любые изменения DOM приводят к сбросу кешей, а только те, которые могут повлиять на коллекцию. Если где-то добавлен новый атрибут элементу -- с кешем для `getElementsByTagName` ничего не произойдёт, так как атрибут никак не может повлиять на результат поиска по тегу.
  • Прочие поисковые методы, такие как `getElementsByClassName` тоже сбрасывают кеш при изменениях интеллектуально. diff --git a/4-ajax/6-xhr-onprogress/article.md b/4-ajax/6-xhr-onprogress/article.md index f4e5d844..07f532c9 100644 --- a/4-ajax/6-xhr-onprogress/article.md +++ b/4-ajax/6-xhr-onprogress/article.md @@ -2,21 +2,19 @@ Запрос `XMLHttpRequest` состоит из двух фаз:
      -
    1. Стадия закачки (upload). На ней данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов.
    2. -
    3. Стадия скачивания (download). После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время.
    4. +
    5. Стадия закачки (upload). На ней данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов. Для отслеживания прогресса на стадии закачки существует объект типа [XMLHttpRequestUpload](https://xhr.spec.whatwg.org/#xmlhttprequesteventtarget), доступный как `xhr.upload` и события на нём.
    6. +
    7. Стадия скачивания (download). После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время. На этой стадии используется обработчик `xhr.onprogress`.
    -Для каждой стадии предусмотрены события, "рассказывающие" о процессе выполнения. +Далее -- обо всём по порядку. [cut] -## XMLHttpRequestUpload +## Стадия закачки -Для отслеживания прогресса на стадии закачки существует объект [XMLHttpRequestUpload](https://xhr.spec.whatwg.org/#xmlhttprequesteventtarget), доступный как `xhr.upload`. +На стадии закачки для получения информации используем объект `xhr.upload`. У этого объекта нет методов, он только генерирует события в процессе закачки. А они-то как раз и нужны. -У него нет методов, он предназначен исключительно для обработки событий при закачке. - -Вот их полный список: +Вот полный список событий:
    • `loadstart`
    • `progress`
    • @@ -43,9 +41,13 @@ xhr.upload.onerror = function() { } ``` -После того, как загрузка завершена, будет начато скачивание ответа. +## Стадия скачивания -На этой фазе `xhr.upload` уже не нужен, а в дело вступают обработчики событий на самом объекте `xhr`, в частности `xhr.onprogress`: +После того, как загрузка завершена, и сервер соизволит ответить на запрос, `XMLHttpRequest` начнёт скачивание ответа сервера. + +На этой фазе `xhr.upload` уже не нужен, а в дело вступают обработчики событий на самом объекте `xhr`. В частности, событие `xhr.onprogress` содержит информацию о количестве принятых байт ответа. + +Пример обработчика: ```js xhr.onprogress = function(event) { @@ -55,13 +57,13 @@ xhr.onprogress = function(event) { Все события, возникающие в этих обработчиках, имеют тип [ProgressEvent](https://xhr.spec.whatwg.org/#progressevent), то есть имеют свойства `loaded` -- количество уже пересланных данных в байтах и `total` -- общее количество данных. -## Загрузка файла с индикатором прогресса +## Демо: загрузка файла с индикатором прогресса Современный `XMLHttpRequest` позволяет отправить на сервер всё, что угодно. Текст, файл, форму. Мы, для примера, рассмотрим загрузку файла с индикацией прогресса. Это требует от браузера поддержки [File API](http://www.w3.org/TR/FileAPI/), то есть исключает IE9-. -File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag'n'Drop или выбран в поле формы. +File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag'n'Drop или выбран в поле формы, и отправить его при помощи `XMLHttpRequest`. Форма для выбора файла с обработчиком `submit`: @@ -73,7 +75,8 @@ File API позволяет получить доступ к содержимо ``` -Здесь мы почти не пользуемся File API. Его роль -- получить файл из формы через свойство `files` элемента `` и далее мы сразу передадим его `xhr.send` в функции `upload`: +Мы получаем файл из формы через свойство `files` элемента `` и передаём его в функцию `upload`: ```js function upload(file) { var xhr = new XMLHttpRequest(); - // обработчики можно объединить в один, + // обработчик для закачки + xhr.upload.onprogress = function(event) { + log(event.loaded + ' / ' + event.total); + } + + // обработчики успеха и ошибки // если status == 200, то это успех, иначе ошибка xhr.onload = xhr.onerror = function() { if (this.status == 200) { @@ -101,10 +109,6 @@ function upload(file) { } }; - // обработчик для закачки - xhr.upload.onprogress = function(event) { - log(event.loaded + ' / ' + event.total); - } xhr.open("POST", "upload", true); xhr.send(file); @@ -112,15 +116,21 @@ function upload(file) { } ``` +Этот код отправит файл на сервер и будет сообщать о прогрессе при его закачке (`xhr.upload.onprogress`), а также об окончании запроса (`xhr.onload`, `xhr.onerror`). + Полный пример, основанный на коде выше: [codetabs src="progress"] ## Событие onprogress в деталях -Событие `onprogress` имеет одинаковый вид при закачке на сервер (`xhr.upload.onprogress`) и при получении ответа (`xhr.onprogress`). +При обработке события `onprogress` есть ряд важных тонкостей. -Оно получает объект `event` типа [ProgressEvent](https://xhr.spec.whatwg.org/#progressevent) со свойствами: +Можно, конечно, их игнорировать, но лучше бы знать. + +Заметим, что событие, возникающее при `onprogress`, имеет одинаковый вид на стадии закачки (в обработчике `xhr.upload.onprogress`) и при получении ответа (в обработчике `xhr.onprogress`). + +Оно представляет собой объект типа [ProgressEvent](https://xhr.spec.whatwg.org/#progressevent) со свойствами:
      `loaded`
      @@ -132,10 +142,11 @@ function upload(file) {
      `total`
      Общее количество байт для пересылки, если известно. -При HTTP-запросах оно передаётся в заголовке `Content-Length`. +А может ли оно быть неизвестно? +
        -
      • При закачке на сервер браузер всегда знает полный размер пересылаемых данных. Поэтому в `xhr.upload.onprogress` значение `lengthComputable` всегда будет `true`, а `total` -- содержать этот размер.
      • -
      • При скачивании данных -- уже задача сервера поставить этот заголовок. Если его нет, то `total = 0`, а для того, чтобы понять, что данных не `0`, а неизвестное количество -- служит `lengthComputable`, которое в данном случае равно `false`.
      • +
      • При закачке на сервер браузер всегда знает полный размер пересылаемых данных, так что `total` всегда содержит конкретное количество байт, а значение `lengthComputable` всегда будет `true`.
      • +
      • При скачивании данных -- обычно сервер в начале сообщает их общее количество в HTTP-заголовке `Content-Length`. Но он может и не делать этого, например если сам не знает, сколько данных будет или если генерирует их динамически. Тогда `total` будет равно `0`. А чтобы отличить нулевой размер данных от неизвестного -- как раз служит `lengthComputable`, которое в данном случае равно `false`.
      @@ -147,10 +158,10 @@ function upload(file) { Это обозначено в [спецификации progress notifications](http://www.w3.org/TR/XMLHttpRequest/#make-progress-notifications). -
    • **При получении данных доступен `xhr.responseText`.** +
    • **В процессе получения данных, ещё до их полной передачи, доступен `xhr.responseText`.** Можно до окончания запроса заглянуть в него и прочитать текущие полученные данные. Важно, что при пересылке строки в кодировке UTF-8 русские символы кодируются 2 байтами. Возможно, что в конце одного пакета данных окажется первая половинка символа, а в начале следующего -- вторая. Поэтому полагаться на то, что до окончания запроса в `responoseText` находится корректная строка нельзя. Исключение -- заведомо однобайтные символы, например цифры.
    • -
    • **При закачки `xhr.upload.onprogress` не гарантирует обработку загрузки сервером.** +
    • **Сработавшее событие `xhr.upload.onprogress` не гарантирует, что данные дошли.** Событие `xhr.upload.onprogress` срабатывает, когда данные отправлены браузером. Но оно не гарантирует, что сервер получил, обработал и записал данные на диск. Он говорит лишь о самом факте отправки. @@ -174,5 +185,5 @@ formData.append("myfile", file); xhr.send(formData); ``` -Данные будут отправлены в кодировке `multipart/form-data`. Серверный фреймворк обработает это как обычную форму. +Данные будут отправлены в кодировке `multipart/form-data`. Серверный фреймворк увидит это как обычную форму с файлом. Практически все серверные технологии имеют их встроенную поддержку. diff --git a/4-ajax/8-xhr-longpoll/article.md b/4-ajax/8-xhr-longpoll/article.md index 717f2f3c..0d31af5f 100644 --- a/4-ajax/8-xhr-longpoll/article.md +++ b/4-ajax/8-xhr-longpoll/article.md @@ -1,4 +1,4 @@ -# XMLHttpRequest: длинные опросы +# COMET с XMLHttpRequest: длинные опросы В этой главе мы рассмотрим способ организации COMET, то есть непрерывного получения данных с сервера, который очень прост и подходит в 90% реальных случаев. diff --git a/6-optimize/1-memory-leaks/article.md b/6-optimize/1-memory-leaks/article.md deleted file mode 100644 index 08ed1610..00000000 --- a/6-optimize/1-memory-leaks/article.md +++ /dev/null @@ -1,317 +0,0 @@ -# Утечки памяти - -*Утечки памяти* происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов. - -Обычно это происходит автоматически ([](/memory-management)). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях: - -[cut] -
        -
      1. Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.
      2. -
      3. Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включенным, чтобы не закрывать браузер с кучей вкладок. Приходит утром -- а браузер съел всю память и рухнул и сильно тормозит.
      4. -
      -Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре JavaScript-кода. Мы разберём несколько наиболее частых и важных примеров. - -## Коллекция утечек в IE - -### Утечка DOM ↔ JS в IE8- - -IE до версии 8 не умел очищать циклические ссылки, появляющиеся между DOM-объектами и объектами JavaScript. В результате и DOM и JS оставались в памяти навсегда. - -В браузере IE8 была проведена серьёзная работа над ошибками, но утечка в IE8- появляется, если круговая ссылка возникает "через объект". - -Чтобы было понятнее, о чём речь, посмотрите на следующий код. Он вызывает утечку памяти в IE8-: - -``` -function leak() { - // Создаём новый DIV, добавляем к BODY - var elem = document.createElement('div'); - document.body.appendChild(elem); - - // Записываем в свойство жирный объект - elem.__expando = { - bigAss: new Array(1000000).join('lalala') - }; - -*!* - // Создаём круговую ссылку. Без этой строки утечки не будет. - elem.__expando.__elem = elem; -*/!* - - // Удалить элемент из DOM. Браузер должен очистить память. - elem.parentElement.removeChild(elem); -} -``` - -Полный пример (только для IE8-, а также IE9 в режиме совместимости с IE8): - -[codetabs src="leak-ie8"] - - -Круговая ссылка и, как следствие, утечка может возникать и неявным образом, через замыкание: - -```js -function leak() { - var elem = document.createElement('div'); - document.body.appendChild(elem); - - elem.__expando = { - bigAss: new Array(1000000).join('lalala'), -*!* - method: function() {} // создаётся круговая ссылка через замыкание -*/!* - }; - - // Удалить элемент из DOM. Браузер должен очистить память. - elem.parentElement.removeChild(elem); -} -``` - -Полный пример (IE8-, IE9 в режиме совместимости с IE8): - -[codetabs src="leak-ie8-2"] - -Без привязки метода `method` к элементу здесь утечки не возникнет. - -Бывает ли такая ситуация в реальной жизни? Или это -- целиком синтетический пример, для заумных программистов? - -Да, конечно бывает. Например, при разработке графических компонент -- бывает удобно присвоить DOM-элементу ссылку на JavaScript-объект, который представляет собой компонент. Это упрощает делегирование и, в общем-то, логично, что DOM-элемент знает о компоненте на себе. Но в IE8- прямая привязка ведёт к утечке памяти! - -Примерно так: -```js -function Menu(elem) { - elem.onclick = function() {}; -} - -var menu = new Menu(elem); // Menu содержит ссылку на elem -*!* -elem.menu = menu; // такая привязка или что-то подобное ведёт к утечке в IE8 -*/!* -``` - -Полный пример (IE8-, IE9 в режиме совместимости с IE8): - -[codetabs src="leak-ie8-widget"] - -### Утечка IE8 при обращении к коллекциям таблицы - -Эта утечка происходит только в IE8 в стандартном режиме. В нём при обращении к табличным псевдо-массивам (напр. `rows`) создаются и не очищаются внутренние ссылки, что приводит к утечкам. - -Также воспроизводится в новых IE в режиме совместимости с IE8. - -Код: - -```js -var elem = document.createElement('div'); // любой элемент - -function leak() { - - elem.innerHTML = '
      1
      '; - -*!* - elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке - // при том, что мы даже не сохраняем значение в переменную -*/!* - - elem.removeChild(elem.firstChild); // удалить таблицу (*) - // alert(elem.childNodes.length) // выдал бы 0, elem очищен, всё честно -} -``` - -Полный пример (IE8): - -[codetabs src="leak-ie8-table"] - -Особенности: -
        -
      • Если убрать отмеченную строку, то утечки не будет.
      • -
      • Если заменить строку `(*)` на `elem.innerHTML = ''`, то память будет очищена, т.к. этот способ работает по-другому, нежели просто `removeChild` (см. главу [](/memory-management)).
      • -
      • Утечка произойдёт не только при доступе к `rows`, но и к другим свойствам, например `elem.firstChild.tBodies[0]`.
      • -
      - -Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией: - -```js -function empty(elem) { - while (elem.firstChild) elem.removeChild(elem.firstChild); -} -``` - -Если идёт доступ к табличным коллекциям и регулярное обновление таблиц при помощи DOM-методов -- утечка в IE8 будет расти. - -Более подробно вы можете почитать об этой утечке в статье [Утечки памяти в IE8, или страшная сказка со счастливым концом](http://habrahabr.ru/post/141451/). - -### Утечка через XmlHttpRequest в IE8- - -Следующий код вызывает утечки памяти в IE8-: - -```js -function leak() { - var xhr = new XMLHttpRequest(); - - xhr.open('GET', '/server.do', true); - - xhr.onreadystatechange = function() { - if (xhr.readyState == 4 && xhr.status == 200) { - // ... - } - } - - xhr.send(null); -} -``` - -Как вы думаете, почему? Если вы внимательно читали то, что написано выше, то имеете информацию для ответа на этот вопрос.. - -Посмотрим, какая структура памяти создается при каждом запуске: - - - -Когда запускается асинхронный запрос `xhr`, браузер создаёт специальную внутреннюю ссылку (internal reference) на этот объект. находится в процессе коммуникации. Именно поэтому объект `xhr` будет жив после окончания работы функции. - -Когда запрос завершен, браузер удаляет внутреннюю ссылку, `xhr` становится недостижимым и память очищается... Везде, кроме IE8-. - - -Полный пример (IE8): - -[codetabs src="leak-ie8-xhr"] - -Чтобы это исправить, нам нужно разорвать круговую ссылку `XMLHttpRequest ↔ JS`. Например, можно удалить `xhr` из замыкания: - -```js -function leak() { - var xhr = new XMLHttpRequest(); - - xhr.open('GET', 'something.js?' + Math.random(), true); - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - - if (xhr.status == 200) { - document.getElementById('test').innerHTML++; - } - -*!* - xhr = null; // по завершении запроса удаляем ссылку из замыкания -*/!* - } - - xhr.send(null); -} -``` - - - -Теперь циклической ссылки нет -- и не будет утечки. - - -## Объемы утечек памяти - -Объем "утекающей" памяти может быть небольшим. Тогда это почти не ощущается. Но так как замыкания ведут к сохранению переменных внешних функций, то одна функция может тянуть за собой много чего ещё. - -Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера). - -```js -function f() { - var data = "Большой объем данных, например, переданных сервером" - - /* делаем что-то хорошее (ну или плохое) с полученными данными */ - - function inner() { - // ... - } - - return inner; -} -``` - -Пока функция `inner` остается в памяти, `LexicalEnvironment` с переменной большого объема внутри висит в памяти. - -Висит до тех пор, пока функция `inner` жива. - -Как правило, JavaScript не знает, какие из переменных функции `inner` будут использованы, поэтому оставляет их все. Исключение -- виртуальная машина V8 (Chrome, Opera, Node.JS), она часто (не всегда) видит, что переменная не используется во внутренних функциях, и очистит память. - -В других же интерпретаторах, даже если код спроектирован так, что никакой утечки нет, по вполне разумной причине может создаваться множество функций, а память будет расти потому, что функция тянет за собой своё замыкание. - -Сэкономить память здесь вполне можно. Мы же знаем, что переменная `data` не используется в `inner`. Поэтому просто обнулим её: - -```js -function f() { - var data = "Большое количество данных, например, переданных сервером" - - /* действия с data */ - - function inner() { - // ... - } - -*!* - data = null; // когда data станет не нужна - -*/!* - - return inner; -} -``` - -## Поиск и устранение утечек памяти - -### Проверка на утечки - -Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку. - -
        -
      • **Необходимо помнить, что браузер может очистить память не сразу когда объект стал недостижим, а чуть позже.** Например, сборщик мусора может ждать, пока не будет достигнут определенный лимит использования памяти, или запускаться время от времени.
      • - -Поэтому если вы думаете, что нашли проблему и тестовый код, запущенный в цикле, течёт -- подождите примерно минуту, добейтесь, чтобы памяти ело стабильно и много. Тогда будет понятно, что это не особенность сборщика мусора.
      • **Если речь об IE, то надо смотреть "Виртуальную память" в списке процессов, а не только обычную "Память".** Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).
      • -
      • Для простоты отладки, если есть подозрение на утечку конкретных объектов, в них добавляют большие свойства-маркеры. Например, подойдет фрагмент текста: `new Array(999999).join('leak')`.
      • -
      - -### Настройка браузера - -Утечки могут возникать из-за расширений браузера, взимодействющих со страницей. Еще более важно, что **утечки могут быть следствием конфликта двух браузерных расширений** Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно. - -Чтобы понять, в расширениях дело или нет, нужно отключить их: - -
        -
      1. Отключить Flash.
      2. -
      3. Отключить анивирусную защиту, проверку ссылок и другие модули и дополнения.
      4. -
      5. Отключить плагины. Отключить ВСЕ плагины. -
          -
        • - Для IE есть параметр коммандной строки: - -``` -"C:\Program Files\Internet Explorer\iexplore.exe" -extoff -``` - -Кроме того необходимо отключить сторонние расширения в свойствах IE. - - - - -
        • -
        • Firefox необходимо запускать с чистым профилем. Используйте следующую команду для запуска менеджера профилей и создания чистого пустого профиля: - -``` -firefox --profilemanager -``` - -
        • -
        -
      6. -
      - -## Инструменты - -Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти. - - - -Можем посмотреть, сколько памяти и куда он использует. - -Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается. - -Замечательная статья на эту тему есть в документации: [Chrome Developer Tools: Heap Profiling](http://code.google.com/chrome/devtools/docs/heap-profiling.html). - -Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: *Удача!* - - diff --git a/6-optimize/1-memory-leaks/chrome.png b/6-optimize/1-memory-leaks/chrome.png deleted file mode 100644 index 0c88b10b..00000000 Binary files a/6-optimize/1-memory-leaks/chrome.png and /dev/null differ diff --git a/6-optimize/1-memory-leaks/goodluck.png b/6-optimize/1-memory-leaks/goodluck.png deleted file mode 100644 index 918b5022..00000000 Binary files a/6-optimize/1-memory-leaks/goodluck.png and /dev/null differ diff --git a/6-optimize/1-memory-leaks/ie1.png b/6-optimize/1-memory-leaks/ie1.png deleted file mode 100644 index ae04e2cc..00000000 Binary files a/6-optimize/1-memory-leaks/ie1.png and /dev/null differ diff --git a/6-optimize/1-memory-leaks/ie2.png b/6-optimize/1-memory-leaks/ie2.png deleted file mode 100644 index 93617434..00000000 Binary files a/6-optimize/1-memory-leaks/ie2.png and /dev/null differ diff --git a/6-optimize/1-memory-leaks/ie9_disable1.png b/6-optimize/1-memory-leaks/ie9_disable1.png deleted file mode 100644 index 84c98b78..00000000 Binary files a/6-optimize/1-memory-leaks/ie9_disable1.png and /dev/null differ diff --git a/6-optimize/1-memory-leaks/ie9_disable2.png b/6-optimize/1-memory-leaks/ie9_disable2.png deleted file mode 100644 index 3817bc53..00000000 Binary files a/6-optimize/1-memory-leaks/ie9_disable2.png and /dev/null differ diff --git a/6-optimize/1-memory-leaks/leak-ie8-2.view/index.html b/6-optimize/1-memory-leaks/leak-ie8-2.view/index.html deleted file mode 100644 index b89b80de..00000000 --- a/6-optimize/1-memory-leaks/leak-ie8-2.view/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - -

      Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.

      - - - - - \ No newline at end of file diff --git a/6-optimize/1-memory-leaks/leak-ie8-table.view/index.html b/6-optimize/1-memory-leaks/leak-ie8-table.view/index.html deleted file mode 100644 index e0017dce..00000000 --- a/6-optimize/1-memory-leaks/leak-ie8-table.view/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - -

      Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.

      - - - - - - \ No newline at end of file diff --git a/6-optimize/1-memory-leaks/leak-ie8-widget.view/index.html b/6-optimize/1-memory-leaks/leak-ie8-widget.view/index.html deleted file mode 100644 index 1bdd947c..00000000 --- a/6-optimize/1-memory-leaks/leak-ie8-widget.view/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - -

      Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.

      - - - - - - \ No newline at end of file diff --git a/6-optimize/1-memory-leaks/leak-ie8-xhr.view/index.html b/6-optimize/1-memory-leaks/leak-ie8-xhr.view/index.html deleted file mode 100644 index 67329b91..00000000 --- a/6-optimize/1-memory-leaks/leak-ie8-xhr.view/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - -

      Страница создаёт объект XMLHttpRequest каждые 50мс.

      - -

      Нажмите на кнопку и смотрите на память, она течёт в IE<9.

      - - - - - -
      Количество запросов: 0
      - - - - \ No newline at end of file diff --git a/6-optimize/1-memory-leaks/leak-ie8.view/index.html b/6-optimize/1-memory-leaks/leak-ie8.view/index.html deleted file mode 100644 index c2c1a4fc..00000000 --- a/6-optimize/1-memory-leaks/leak-ie8.view/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - -

      Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.

      - - - - - - \ No newline at end of file diff --git a/6-optimize/1-memory-leaks/leak-xhr-2.svg b/6-optimize/1-memory-leaks/leak-xhr-2.svg deleted file mode 100644 index e6faf68c..00000000 --- a/6-optimize/1-memory-leaks/leak-xhr-2.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - leak-xhr-2.svg - Created with bin/sketchtool. - - - - - - - - xhr - - - function - - - LexicalEnvironment - - - - - - - - - onreadystatechange - - - [[Scope]] - - - - - - \ No newline at end of file diff --git a/6-optimize/1-memory-leaks/leak-xhr.svg b/6-optimize/1-memory-leaks/leak-xhr.svg deleted file mode 100644 index 967a4b3c..00000000 --- a/6-optimize/1-memory-leaks/leak-xhr.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - leak-xhr.svg - Created with bin/sketchtool. - - - - - - - - xhr - - - function - - - LexicalEnvironment - - - - - - - - - - - property - - - onreadystatechange - - - внутренняя ссылка - - - [[Scope]] - - - - \ No newline at end of file diff --git a/6-optimize/2-memory-leaks-jquery/article.md b/6-optimize/2-memory-leaks-jquery/article.md deleted file mode 100644 index 70b76c45..00000000 --- a/6-optimize/2-memory-leaks-jquery/article.md +++ /dev/null @@ -1,144 +0,0 @@ - -# Утечки памяти при использовании jQuery - -В jQuery для хранения обработчиков событий и других вспомогательных данных, связанных с DOM-элементами, используется внутренний объект, который в jQuery 1 доступен через $.data. - -В jQuery 2 доступ к нему закрыт через замыкание, он стал локальной переменной внутри jQuery с именем `data_priv`, но в остальном всё работает точно так, как описано, и с теми же последствиями. - -## $.data - -Встроенная функция `$.data` позволяет хранить привязывать произвольные значения к DOM-узлам. - -Например: - -```js -//+ no-beautify -// присвоить -$(document).data('prop', { anything: "любой объект" }) - -// прочитать -alert( $(document).data('prop').anything ) // любой объект -``` - -Реализована она хитрым образом. Данные не хранятся в самом элементе, а во внутреннем объекте jQuery. - -jQuery-вызов `elem.data(prop, val)` делает следующее: - -
        -
      1. Элемент получает уникальный идентификатор, если у него такого еще нет: - -```js -elem[jQuery.expando] = id = ++jQuery.uuid; // средствами jQuery -``` - -`jQuery.expando` -- это случайная строка, сгенерированная jQuery один раз при входе на страницу. Уникальное свойство, чтобы ничего важного не перезаписать.
      2. -
      3. ...А сами данные сохраняются в специальном объекте `jQuery.cache`: - -```js -//+ no-beautify -jQuery.cache[id]['prop'] = { anything: "любой объект" }; -``` - -
      4. -
      - -Когда данные считываются из элемента: - -
        -
      1. Уникальный идентификатор элемента извлекается из `id = elem[ jQuery.expando]`. -
      2. Данные считываются из `jQuery.cache[id]`.
      3. -
      - -Смысл этого API в том, что DOM-элемент никогда не ссылается на JavaScript объект напрямую. Задействуется идентификатор, а сами данные хранятся в `jQuery.cache`. Утечек в IE не будет. - -К тому же все данные известны библиотеке, так что можно клонировать с ними и т.п. - -Как побочный эффект -- возникает утечка памяти, если элемент удален из DOM без дополнительной очистки. - -## Примеры утечек в jQuery - -Следующая функция `leak` создает jQuery-утечку во всех браузерах: - - -```html - - - -
      - - - -Утечка идёт... - - -``` - - -Утечка происходит потому, что обработчик события в jQuery хранится в данных элемента. В строке `(*)` элемент удален очисткой родительского `innerHTML`, но в `jQuery.cache` данные остались. - -Более того, система обработки событий в jQuery устроена так, что вместе с обработчиком в данных хранится и ссылка на элемент, так что в итоге оба -- и обработчик и элемент -- остаются в памяти вместе со всем замыканием! - -Ещё более простой пример утечки: - -Этот код также создает утечку: - -```js -function leak() { - $('
      ') - .click(function() {}) -} -``` - -...То есть, мы создаём элемент, вешаем на него обработчик... И всё. - -Такой код ведёт к утечке памяти как раз потому, что элемент `
      ` создан, но нигде не размещен :). После выполнения функции ссылка на него теряется. Но обработчик события `click` уже сохранил данные в `jQuery.cache`, которые застревают там навсегда. - -## Используем jQuery без утечек - -Чтобы избежать утечек, описанных выше, для удаления элементов используйте функции jQuery API, а не чистый JavaScript. - -Методы remove(), empty() и html() проверяют дочерние элементы на наличие данных и очищают их. Это несколько замедляет процедуру удаления, но зато освобождается память. - -К счастью обнаружить такие утечки легко. Проверьте размер `$.cache`. Если он большой и растет, то изучите кэш, посмотрите, какие записи остаются и почему. - -## Улучшение производительности jQuery - -У способа организации внутренних данных, применённого в jQuery, есть важный побочный эффект. - -Функции, удаляющие элементы, также должны удалить и связанные с ними внутренние данные. Для этого нужно для каждого удаляемого элемента проверить -- а нет ли чего во внутреннем хранилище? И, если есть -- удалить. - -Представим, что у нас есть большая таблица ``, и мы хотим обновить её содержимое на новое. Вызов `$('table').html(новые данные)` перед вставкой новых данных аккуратно удалит старые: пробежит по всем ячейкам и проверит внутреннее хранилище. - -Если это большая таблица, то обработчики, скорее всего, стоят не на ячейках, а на самом элементе `
      `, то есть используется делегирование. А, значит, тратить время на проверку всех подэлементов ни к чему. - -Но jQuery-то об этом не знает! - -Чтобы "грязно" удалить элемент, без чистки, мы можем сделать это через "обычные" DOM-вызовы или воспользоваться методом detach(). Его официальное назначение -- в том, чтобы убрать элемент из DOM, но сохранить возможность для вставки (и, соответственно, оставить на нём все данные). А неофициальное -- быстро убрать элемент из DOM, без чистки. - -Возможен и промежуточный вариант: никто не мешает сделать `elem.detach()` и поместить вызов `elem.remove()` в `setTimeout`. В результате очистка будет происходить асинхронно и незаметно. - -## Итого - -
        -
      • Утечки памяти при использовании jQuery возможны, если через DOM-методы удалять элементы, к которым привязаны данные или обработчики.
      • -
      • Чтобы утечки не было, достаточно убедиться, что элемент удаляется при помощи методов jQuery.
      • -
      • Побочный эффект -- при удалении элементов jQuery должна проверить наличие данных для них. Это сильно замедляет процесс удаления большого поддерева DOM.
      • -
      • Если мы значем, что обработчиков и данных нет -- гораздо быстрее удалять элементы при помощи вызова `detach` или обычного DOM.
      • -
      - - diff --git a/6-optimize/3-minification/article.md b/6-optimize/3-minification/article.md deleted file mode 100644 index 22cb1700..00000000 --- a/6-optimize/3-minification/article.md +++ /dev/null @@ -1,513 +0,0 @@ -# Как работают сжиматели JavaScript - -Перед выкладыванием JavaScript на "боевую" машину -- пропускаем его через минификатор (также говорят "сжиматель"), который удаляет пробелы и по-всякому оптимизирует код, уменьшая его размер. - -В этой статье мы посмотрим, как работают современные минификаторы, за счёт чего они укорачивают код и какие с ними возможны проблемы. -[cut] - -## Современные сжиматели - -Рассматриваемые в этой статье алгоритмы и подходы относятся к минификаторам последнего поколения. - -Вот их список: - -
        -
      • [Google Closure Compiler](https://developers.google.com/closure/compiler/)
      • -
      • [UglifyJS](https://github.com/mishoo/UglifyJS)
      • -
      • [Microsoft AJAX Minifier](http://ajaxmin.codeplex.com/)
      • -
      -Самые широко используемые -- первые два, поэтому будем рассматривать в первую очередь их. - -Наша цель -- понять, как они работают, и что интересного с их помощью можно сотворить. - -## С чего начать? - -Для GCC: - -
        -
      1. Убедиться, что стоит [Java](http://java.oracle.com)
      2. -
      3. Скачать и распаковать [](http://closure-compiler.googlecode.com/files/compiler-latest.zip), нам нужен файл `compiler.jar`.
      4. -
      5. Сжать файл `my.js`: `java -jar compiler.jar --charset UTF-8 --js my.js --js_output_file my.min.js`
      6. -
      - -Обратите внимание на флаг `--charset` для GCC. Без него русские буквы будут закодированы во что-то типа `\u1234`. - -Google Closure Compiler также содержит [песочницу](http://closure-compiler.appspot.com/home) для тестирования сжатия и [веб-сервис](https://developers.google.com/closure/compiler/docs/gettingstarted_api?hl=ru), на который код можно отправлять для сжатия. Но скачать файл обычно гораздо проще, поэтому его редко где используют. - -Для UglifyJS: - -
        -
      1. Убедиться, что стоит [Node.js](http://nodejs.org)
      2. -
      3. Поставить `npm install -g uglify-js`.
      4. -
      5. Сжать файл `my.js`: `uglifyjs my.js -o my.min.js`
      6. -
      - -## Что делает минификатор? - -Все современные минификаторы работают следующим образом: -
        -
      1. Разбирают JavaScript-код в синтаксическое дерево. - -Также поступает любой интерпретатор JavaScript перед тем, как его выполнять. Но затем, вместо исполнения кода...
      2. -
      3. Бегают по этому дереву, анализируют и оптимизируют его.
      4. -
      5. Записывают из синтаксического дерева получившийся код.
      6. -
      - -## Как выглядит дерево? - -Посмотреть синтаксическое дерево можно, запустив компилятор со специальным флагом. - -Для GCC есть даже способ вывести его: - -
        -
      1. Сначала сгенерируем дерево в формате [DOT](http://en.wikipedia.org/wiki/DOT_language): - -``` -java -jar compiler.jar --js my.js --use_only_custom_externs --print_tree >my.dot -``` - -Здесь флаг `--print_tree` выводит дерево, а `--use_only_custom_externs` убирает лишнюю служебную информацию. -
      2. -
      3. Файл в этом формате используется в различных программах для графопостроения. - -Чтобы превратить его в обычную картинку, подойдёт утилита `dot` из пакета [Graphviz](http://www.graphviz.org/): - -``` -// конвертировать в формат png -dot -Tpng my.dot -o my.png - -// конвертировать в формат svg -dot -Tsvg my.dot -o my.svg -``` - -
      4. -
      - -Пример кода `my.js`: - -```js -function User(name) { - - this.sayHi = function() { - alert( name ); - }; - -} -``` - -Результат, получившееся из `my.js` дерево: - - - -В узлах-эллипсах на иллюстрации выше стоит тип, например `FUNCTION` (функция) или `NAME` (имя переменной). Комментарии к ним на русском языке добавлены мной вручную. - -Кроме него к каждому узлу привязаны конкретные данные. Сжиматель умеет ходить по этому дереву и менять его, как пожелает. - -[smart header="Комментарии JSDoc"] -Обычно когда код превращается в дерево -- из него естественным образом исчезают комментарии и пробелы. Они не имеют значения при выполнении, поэтому игнорируются. - -Но Google Closure Compiler добавляет в дерево информацию из *комментариев JSDoc*, т.е. комментариев вида `/** ... */`, например: - -```js -*!* -/** - * Номер минимальной поддерживаемой версии IE - * @const - * @type {number} - */ -*/!* -var minIEVersion = 8; -``` - -Такие комментарии не создают новых узлов дерева, а добавляются в качестве информации к существующем. В данном случае -- к переменной `minIEVersion`. - -В них может содержаться информация о типе переменной (`number`) и другая, которая поможет сжимателю лучше оптимизировать код (`const` -- константа). - -[/smart] - -## Оптимизации - -Сжиматель бегает по дереву, ищет "паттерны" -- известные ему структуры, которые он знает, как оптимизировать, и обновляет дерево. - -В разных минификаторах реализован разный набор оптимизаций, сами оптимизации применяются в разном порядке, поэтому результаты работы могут отличаться. В примерах ниже даётся результат работы GCC. - - -
      -
      Объединение и сжатие констант
      -
      -До оптимизации: - -```js -function test(a, b) { - run(a, 'my' + 'string', 600 * 600 * 5, 1 && 0, b && 0) -} -``` - -После: - -```js -//+ no-beautify -function test(a,b){run(a,"mystring",18E5,0,b&&0)}; -``` - -
        -
      • `'my' + 'string'` -> `"mystring"`.
      • -
      • `600 * 600 * 5` -> `18E5` (научная форма числа, для краткости).
      • -
      • `1 && 0` -> `0`.
      • -
      • `b && 0` -> без изменений, т.к. результат зависит от `b`.
      • -
      -
      -
      Укорачивание локальных переменных
      -
      -До оптимизации: - -```js -function sayHi(*!*name*/!*, *!*message*/!*) { - alert(name +" сказал: " + message); -} -``` - -После оптимизации: - -```js -//+ no-beautify -function sayHi(a,b){alert(a+" сказал: "+b)}; -``` - -
        -
      • Локальная переменная заведомо доступна только внутри функции, поэтому обычно её переименование безопасно (необычные случаи рассмотрим далее).
      • -
      • Также переименовываются локальные функции.
      • -
      • Вложенные функции обрабатываются корректно.
      • -
      -
      -
      Объединение и удаление локальных переменных
      -
      -До оптимизации: - -```js -function test(nodeId) { - var elem = document.getElementsById(nodeId); - var parent = elem.parentNode; - alert( parent ); -} -``` - -После оптимизации GCC: - -```js -//+ no-beautify -function test(a){a=document.getElementsById(a).parentNode;alert(a)}; -``` - -
        -
      • Локальные переменные были переименованы.
      • -
      • Лишние переменные убраны. Для этого сжиматель создаёт вспомогательную внутреннюю структуру данных, в которой хранятся сведения о "пути использования" каждой переменной. Если одна переменная заканчивает свой путь и начинает другая, то вполне можно дать им одно имя.
      • -
      • Кроме того, операции `elem = getElementsById` и `elem.parentNode` объединены, но это уже другая оптимизация.
      • -
      -
      -
      Уничтожение недостижимого кода, разворачивание `if`-веток
      -
      - -До оптимизации: - -```js -function test(node) { - var parent = node.parentNode; - - if (0) { - alert( "Привет с параллельной планеты" ); - } else { - alert( "Останется только один" ); - } - - return; - - alert( 1 ); -} -``` - -После оптимизации: - -```js -//+ no-beautify -function test(){alert("Останется только один")} -``` - -
        -
      • Если переменная присваивается, но не используется, она может быть удалена. В примере выше эта оптимизация была применена к переменной `parent`, а затем и к параметру `node`.
      • -
      • Заведомо ложная ветка `if(0) { .. }` убрана, заведомо истинная -- оставлена. - -То же самое будет с условиями в других конструкциях, например `a = true ? c : d` превратится в `a = c`.
      • -
      • Код после `return` удалён как недостижимый.
      • -
      -
      -
      Переписывание синтаксических конструкций
      -
      -До оптимизации: - -```js -var i = 0; -while (i++ < 10) { - alert( i ); -} - -if (i) { - alert( i ); -} - -if (i == '1') { - alert( 1 ); -} else if (i == '2') { - alert( 2 ); -} else { - alert( i ); -} -``` - -После оптимизации: - -```js -//+ no-beautify -for(var i=0;10>i++;)alert(i);i&&alert(i);"1"==i?alert(1):"2"==i?alert(2):alert(i); -``` - -
        -
      • Конструкция `while` переписана в `for`.
      • -
      • Конструкция `if (i) ...` переписана в `i&&...`.
      • -
      • Конструкция `if (cond) ... else ...` была переписана в `cond ? ... : ...`.
      • -
      -
      -
      Инлайнинг функций
      -
      -*Инлайнинг функции* -- приём оптимизации, при котором функция заменяется на своё тело. - -До оптимизации: - -```js -function sayHi(message) { - - var elem = createMessage('div', message); - showElement(elem); - - function createMessage(tagName, message) { - var el = document.createElement(tagName); - el.innerHTML = message; - return el; - } - - function showElement(elem) { - document.body.appendChild(elem); - } -} -``` - -После оптимизации (переводы строк также будут убраны): - -```js -function sayHi(b) { - var a = document.createElement("div"); - a.innerHTML = b; - document.body.appendChild(a) -}; -``` - -
        -
      • Вызовы функций `createMessage` и `showElement` заменены на тело функций. В данном случае это возможно, так как функции используются всего по разу.
      • -
      • Эта оптимизация применяется не всегда. Если бы каждая функция использовалась много раз, то с точки зрения размера выгоднее оставить их "как есть".
      • -
      -
      -
      Инлайнинг переменных
      -
      Переменные заменяются на значение, если оно заведомо известно. - -До оптимизации: - -```js -(function() { - var isVisible = true; - var hi = "Привет вам из JavaScript"; - - window.sayHi = function() { - if (isVisible) { - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - alert( hi ); - } - } - -})(); -``` - -После оптимизации: - -```js -(function() { - window.sayHi = function() { - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - alert( "Привет вам из JavaScript" ); - }; - } -})(); -``` - -
        -
      • Переменная `isVisible` заменена на `true`, после чего `if` стало возможным убрать.
      • -
      • Переменная `hi` заменена на строку.
      • -
      - -Казалось бы -- зачем менять `hi` на строку? Ведь код стал ощутимо длиннее! - -...Но всё дело в том, что минификатор знает, что дальше код будет сжиматься при помощи gzip. Во всяком случае, все правильно настроенные сервера так делают. - -[Алгоритм работы gzip](http://www.gzip.org/algorithm.txt) заключается в том, что он ищет повторы в данных и выносит их в специальный "словарь", заменяя на более короткий идентификатор. Архив как раз и состоит из словаря и данных, в которых дубликаты заменены на идентификаторы. - -Если вынести строку обратно в переменную, то получится как раз частный случай такого сжатия -- взяли `"Привет вам из JavaScript"` и заменили на идентификатор `hi`. Но gzip справляется с этим лучше, поэтому эффективнее будет оставить именно строку. Gzip сам найдёт дубликаты и сожмёт их. - -Плюс такого подхода станет очевиден, если сжать gzip оба кода -- до и после минификации. Минифицированный gzip-сжатый код в итоге даст меньший размер. -
      - -
      Разные мелкие оптимизации
      -
      Кроме основных оптимизаций, описанных выше, есть ещё много мелких: - -
        -
      • Убираются лишние кавычки у ключей - -```js -//+ no-beautify -{"prop" : "val" } => {prop:"val"} -``` - -
      • -
      • Упрощаются простые вызовы `Array/Object` - -```js -//+ no-beautify -a = new Array() => a = [] -o = new Object() => o = {} -``` - -Эта оптимизация предполагает, что `Array` и `Object` не переопределены программистом. Для включения её в UglifyJS нужен флаг `--unsafe`. -
      • -
      • ...И еще некоторые другие мелкие изменения кода...
      • -
      - - -
      -
      - -## Подводные камни - -Описанные оптимизации, в целом, безопасны, но есть ряд подводных камней. - -### Конструкция with - -Рассмотрим код: - -```js -//+ no-beautify -function changePosition(style) { - var position, test; - -*!* - with (style) { - position = 'absolute'; - } -*/!* -} -``` - -Куда будет присвоено значение `position = 'absolute'`? - -Это неизвестно до момента выполнения: если свойство `position` есть в `style` -- то туда, а если нет -- то в локальную переменную. - -Можно ли в такой ситуации заменить локальную переменную на более короткую? Очевидно, нет: - -```js -//+ no-beautify -function changePosition(style) { - var a, b; - -*!* - with (style) { // а что, если в style нет такого свойства? - position = 'absolute';// куда будет осуществлена запись? в window.position? - } -*/!* -} -``` - -Такая же опасность для сжатия кроется в использованном `eval`. Ведь `eval` может обращаться к локальным переменным: - -```js -//+ no-beautify -function f(code) { - var myVar; - - eval(code); // а что, если будет присвоение eval("myVar = ...") ? - - alert(myVar); -``` - -Получается, что при наличии `eval` мы не имеем права переименовывать локальные переменные. Причём (!), если функция является вложенноой, то и во внешних функциях тоже. - -А ведь сжатие переменных -- очень важная оптимизация. Как правило, она уменьшает размер сильнее всего. - -Что делать? Разные минификаторы поступают по-разному. - -
        -
      • UglifyJS -- не будет переименовывать переменные. Так что наличие `with/eval` сильно повлияет на степень сжатие кода.
      • -
      • GCC -- всё равно сожмёт локальные переменные. Это, конечно же, может привести к ошибкам, причём в сжатом коде, отлаживать который не очень-то удобно. Поэтому он выдаст предупреждение о наличии опасной конструкции.
      • -
      - -Ни тот ни другой вариант нас, по большому счёту, не устраивают. - -**Для того, чтобы код сжимался хорошо и работал правильно, не используем `with` и `eval`.** - -Либо, если уж очень надо использовать -- делаем это с оглядкой на поведение минификатора, чтобы не было проблем. - -### Условная компиляция IE10- - -В IE10- поддерживалось [условное выполнение JavaScript](http://msdn.microsoft.com/en-us/library/121hztk3.aspx). - -Синтаксис: `/*@cc_on код */`. - -Такой код выполнится в IE10-, например: - -```js -//+ run -var isIE /*@cc_on =true@*/ ; - -alert( isIE ); // true в IE10- -``` - -Можно хитро сделать, чтобы комментарий остался, например так: - -```js -//+ run -var isIE = new Function('', '/*@cc_on return true@*/')(); - -alert( isIE ); // true в IE. -``` - - -...Однако, с учётом того, что в современных IE11+ эта компиляция не работает в любом случае, лучше избавиться от неё вообще. - -В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным. - diff --git a/6-optimize/3-minification/my.svg b/6-optimize/3-minification/my.svg deleted file mode 100644 index a04ec2c4..00000000 --- a/6-optimize/3-minification/my.svg +++ /dev/null @@ -1 +0,0 @@ -ASTnode0BLOCKnode1SCRIPTnode0->node1node0->node1UNCONDRETURNRETURNnode0->RETURNSYN_BLOCKnode2FUNCTIONфункцияnode1->node2node1->RETURNUNCONDnode3NAMEимяnode2->node3node4PARAM_LISTсписок параметровnode2->node4node6BLOCKтело функцииnode2->node6UNCONDnode2->node6node5NAMEимяnode4->node5node7EXPR_RESULTвыражениеnode6->node7UNCONDnode6->node7node8ASSIGNприсвоитьnode7->node8node7->RETURNUNCONDnode9GETPROPполучить свойствоnode8->node9node12FUNCTIONфункцияnode8->node12node10THISnode9->node10node11STRINGстрока-константаnode9->node11node13NAMEnode12->node13node14PARAM_LISTnode12->node14node15BLOCKnode12->node15node16EXPR_RESULTnode15->node16node17CALLвызов функцииnode16->node17node18NAMEимя(функции)node17->node18node19NAMEимя(параметр)node17->node19 \ No newline at end of file diff --git a/6-optimize/4-better-minification/article.md b/6-optimize/4-better-minification/article.md deleted file mode 100644 index 5220f678..00000000 --- a/6-optimize/4-better-minification/article.md +++ /dev/null @@ -1,172 +0,0 @@ -# Улучшаем сжатие кода - -Здесь мы обсудим разные приёмы, которые используются, чтобы улучшить сжатие кода. -[cut] -## Больше локальных переменных - -Например, код jQuery обёрнут в функцию, запускаемую "на месте". - -```js -(function(window, undefined) { - // ... - var jQuery = ... - - window.jQuery = jQuery; // сделать переменную глобальной - -})(window); -``` - -Переменные `window` и `undefined` стали локальными. Это позволит сжимателю заменить их на более короткие. - -## ООП без прототипов - -Приватные переменные будут сжаты и заинлайнены. - -Например, этот код хорошо сожмётся: - -```js -function User(firstName, lastName) { - var fullName = firstName + ' ' + lastName; - - this.sayHi = function() { - showMessage(fullName); - } - - function showMessage(msg) { - alert( '**' + msg + '**' ); - } -} -``` - -..А этот -- плохо: - -```js -function User(firstName, lastName) { - this._firstName = firstName; - this._lastName = lastName; -} - -User.prototype.sayHi = function() { - this._showMessage(this._fullName); -} - - -User.prototype._showMessage = function(msg) { - alert( '**' + msg + '**' ); -} -``` - -Сжимаются только локальные переменные, свойства объектов не сжимаются, поэтому эффект от сжатия для второго кода будет совсем небольшим. - -При этом, конечно, нужно иметь в виду общий стиль ООП проекта, достоинства и недостатки такого подхода. - -## Сжатие под платформу, define - -Можно делать разные сборки в зависимости от платформы (мобильная/десктоп) и браузера. - -Ведь не секрет, что ряд функций могут быть реализованы по разному, в зависимости от того, поддерживает ли среда выполнения нужную возможность. - -### Способ 1: локальная переменная - -Проще всего это сделать локальной переменной в модуле: - -```js -(function($) { - -*!* - /** @const */ - var platform = 'IE'; -*/!* - - // ..... - - if (platform == 'IE') { - alert( 'IE' ); - } else { - alert( 'NON-IE' ); - } - -})(jQuery); -``` - -Нужное значение директивы можно вставить при подготовке JavaScript к сжатию. - -Сжиматель заинлайнит её и оптимизирует соответствующие IE. - -### Способ 2: define - -UglifyJS и GCC позволяют задать значение глобальной переменной из командной строки. - -В GCC эта возможность доступна лишь в "продвинутом режиме" работы оптимизатора, который мы рассмотрим далее (он редко используется). - -Удобнее в этом плане устроен UglifyJS. В нём можно указать флаг `-d SYMBOL[=VALUE]`, который заменит все переменные `SYMBOL` на указанное значение `VALUE`. Если `VALUE` не указано, то оно считается равным `true`. - -Флаг не работает, если переменная определена явно. - -Например, рассмотрим код: - -```js -// my.js -if (isIE) { - alert( "Привет от IE" ); -} else { - alert( "Не IE :)" ); -} -``` - -Сжатие вызовом `uglifyjs -d isIE my.js` даст: - -```js -alert( "Привет от IE" ); -``` - -..Ну а чтобы код работал в обычном окружении, нужно определить в нём значение переменной по умолчанию. Это обычно делается в каком-то другом файле (на весь проект), так как если объявить `var isIE` в этом, то флаг `-d isIE` не сработает. - -Но можно и "хакнуть" сжиматель, объявив переменную так: - -```js -// объявит переменную при отсутствии сжатия -// при сжатии не повредит -window.isIE = window.isIE || getBrowserVersion(); -``` - -## Убираем вызовы console.* - -Минификатор имеет в своём распоряжении дерево кода и может удалить ненужные вызовы. - -Для UglifyJS это делают опции компилятора: -
        -
      • `drop_debugger` -- убирает вызовы `debugger`.
      • -
      • `drop_console` -- убирает вызовы `console.*`.
      • -
      - -Можно написать и дополнительную функцию преобразования, которая убирает другие вызовы, например для `log.*`: - -```js -var uglify = require('uglify-js'); -var pro = uglify.uglify; - -function ast_squeeze_console(ast) { - var w = pro.ast_walker(), - walk = w.walk, - scope; - return w.with_walkers({ - "stat": function(stmt) { - if (stmt[0] === "call" && stmt[1][0] == "dot" && stmt[1][1] instanceof Array && stmt[1][1][0] == 'name' && stmt[1][1][1] == "log") { - return ["block"]; - } - return ["stat", walk(stmt)]; - }, - "call": function(expr, args) { - if (expr[0] == "dot" && expr[1] instanceof Array && expr[1][0] == 'name' && expr[1][1] == "console") { - return ["atom", "0"]; - } - } - }, function() { - return walk(ast); - }); -}; -``` - -Эту функцию следует вызвать на результате `parse`, и она пройдётся по дереву и удалит все вызовы `log.*`. - diff --git a/6-optimize/5-gcc-advanced-optimization/article.md b/6-optimize/5-gcc-advanced-optimization/article.md deleted file mode 100644 index f2a84d14..00000000 --- a/6-optimize/5-gcc-advanced-optimization/article.md +++ /dev/null @@ -1,530 +0,0 @@ -# GCC: продвинутые оптимизации - -Продвинутый режим оптимизации google closure compiler включается опцией --compilation_level ADVANCED_OPTIMIZATIONS. - -Слово "продвинутый" (advanced) здесь, пожалуй, не совсем подходит. Было бы более правильно назвать его "супер-агрессивный-ломающий-ваш-неподготовленный-код-режим". Кардинальное отличие применяемых оптимизаций от обычных (simple) -- в том, что они небезопасны. - -Чтобы им пользоваться -- надо уметь это делать. -[cut] - -## Основной принцип продвинутого режима - -
        -
      • Если в обычном режиме переименовываются только локальные переменные внутри функций, то в "продвинутом" -- на более короткие имена заменяется все.
      • -
      • Если в обычном режиме удаляется недостижимый код после return, то в продвинутом -- вообще весь код, который не вызывается в явном виде.
      • -
      - -Например, если запустить продвинутую оптимизацию на таком коде: - -```js -// my.js -function test(node) { - node.innerHTML = "newValue" -} -``` - -Строка запуска компилятора: - -``` -java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js my.js -``` - -...То результат будет -- пустой файл. Google Closure Compiler увидит, что функция test не используется, и с чистой совестью вырежет ее. - -А в следующем скрипте функция сохранится: - -```js -function test(n) { - alert( "this is my test number " + n ); -} -test(1); -test(2); -``` - -После сжатия: - -```js -function a(b) { - alert("this is my test number " + b) -} -a(1); -a(2); -``` - -Здесь в скрипте присутствует явный вызов функции, поэтому она сохранилась. - -Конечно, есть способы, чтобы сохранить функции, вызов которых происходит вне скрипта, и мы их обязательно рассмотрим. - -**Продвинутый режим сжатия не предусматривает сохранения глобальных переменных. Он переименовывает, инлайнит, удаляет вообще все символы, кроме зарезервированных.** - -Иначе говоря, продвинутый режим (ADVANCED_OPTIMIZATIONS), в отличие от простого (SIMPLE_OPTIMIZATIONS -- по умолчанию), вообще не заботится о доступности кода извне и сохранении ссылочной целостности относительно внешних скриптов. - -Единственное, что он гарантирует -- это внутреннюю ссылочную целостность, и то -- при соблюдении ряда условий и практик программирования. - -Собственно, за счет такого агрессивного подхода и достигается дополнительный эффект оптимизации и сжатия скриптов. - -[summary] -То есть, продвинутый режим - это не просто "улучшенный обычный", а принципиально другой, небезопасный и обфусцирующий подход к сжатию. - -Этот режим является "фирменной фишкой" Google Closure Compiler, недоступной при использовании других компиляторов. -[/summary] - -Для того, чтобы эффективно сжимать Google Closure Compiler в продвинутом режиме, нужно понимать, что и как он делает. Это мы сейчас обсудим. - -### Сохранение ссылочной целостности - -Чтобы использовать сжатый скрипт, мы должны иметь возможность вызывать функции под теми именами, которые им дали. - -То есть, перед нами стоит задача *сохранения ссылочной целостности*, которая заключается в том, чтобы обеспечить доступность нужных функций для обращений по исходному имени извне скрипта. - -Существует два способа сохранения внешней ссылочной целостности: экстерны и экспорты. Мы в подробностях рассмотрим оба, но перед этим необходимо упомянуть о модулях -- другой важнейшей возможности GCC. - -### Модули - -При сжатии GCC можно указать одновременно много JavaScript-файлов. "Эка невидаль, " -- скажете вы, и будете правы. Да, пока что ничего особого. - -Но в дополнение к этому можно явно указать, какие исходные файлы сжать в какие файлы результата. То есть, разбить итоговую сборку на модули. - -Так что страницы могут грузить модули по мере надобности. Например, по умолчанию -- главный, а дополнительная функциональность -- загружаться лишь там, где она нужна. - -Для такой сборки используется флаг компилятора `--module имя:количество файлов`. - -Например: - -``` -java -jar compiler.jar --js base.js --js main.js --js admin.js --module -first:2 --module second:1:first -``` - -Эта команда создаст модули: first.js и second.js. - -Первый модуль, который назван "first", создан из объединённого и оптимизированного кода первых двух файлов (`base.js` и `main.js`). - -Второй модуль, который назван "second", создан из `admin.js` -- это следующий аргумент `--js` после включенных в первый модуль. - -Второй модуль в нашем случае зависит от первого. Флаг `--module second:1:first` как раз означает, что модуль `second` будет создан из одного файла после вошедших в предыдущий модуль (`first`) и зависит от модуля `first`. - -А теперь -- самое вкусное. - -**Ссылочная целостность между всеми получившимися файлами гарантируется.** - -Если в одном функция `doFoo` заменена на `b`, то и в другом тоже будет использоваться `b`. - -Это означает, что проблем между JS-файлами не будет. Они могут свободно вызывать друг друга без экспорта, пока находятся в единой модульной сборке. - -### Экстерны - -Экстерн (extern) -- имя, которое числится в специальном списке компилятора. Он должен быть определен вне скрипта, в файле экстернов. - -**Компилятор никогда не переименовывает экстерны.** - -Например: - -```js -document.onkeyup = function(event) { - alert(event.type) -} -``` - -После продвинутого сжатия: - -```js -document.onkeyup = function(a) { - alert(a.type) -} -``` - -Как видите, переименованной оказалась только переменная `event`. Такое переименование заведомо безопасно, т.к. `event` -- локальная переменная. - -Почему компилятор не тронул остального? Попробуем другой вариант: - -```js -document.blabla = function(event) { - alert(event.megaProperty) -} -``` - -После компиляции: - -```js -document.a = function(a) { - alert(a.b) -} -``` - -Теперь компилятор переименовал и blabla и megaProperty. - -Дело в том, что названия, использованные до этого, были во внутреннем списке экстернов компилятора. Этот список охватывает основные объекты браузеров и находится (под именем externs.zip) в корне архива compiler.jar. - -**Компилятор переименовывает имя списка экстернов только когда так названа локальная переменная.** - -Например: - -```js -window.resetNode = function(node) { - var innerHTML = "test"; - node.innerHTML = innerHTML; -} -``` - -На выходе: - -```js -window.a = function(a) { - a.innerHTML = "test" -}; -``` - -Как видите, внутренняя переменная innerHTML не просто переименована - она заинлайнена (заменена на значение). Так как переменная локальна, то любые действия внутри функции с ней безопасны. - -А свойство innerHTML не тронуто, как и объект window -- так как они в списке экстернов и не являются локальными переменными. - -Это приводит к следующему побочному эффекту. Иногда свойства, которые следовало бы сжать, не сжимаются. Например: - -```js -window['User'] = function(name, type, age) { - this.name = name - this.type = type - this.age = age -} -``` - -После сжатия: - -```js -window.User = function(a, b, c) { - this.name = a; - this.type = b; - this.a = c -}; -``` - -Как видно, свойство age сжалось, а name и type -- нет. Это побочный эффект экстернов: name и type -- в списке объектов браузера, и компилятор просто старается не наломать дров. - -Поэтому отметим еще одно полезное правило оптимизации: - -**Названия своих свойств не должны совпадать с зарезервированными словами (экстернами). Тогда они будут хорошо сжиматься.** - -Для задания списка экстернов их достаточно перечислить в файле и указать этот файл флагом --externs <файл экстернов.js>. - -При перечислении объектов в файле экстернов - объявляйте их и перечисляйте свойства. Все эти объявления никуда не идут, они используются только для создания списка, который обрабатывается компилятором. - -Например, файл `myexterns.js`: - -```js -var dojo = {} -dojo._scopeMap; -``` - -Использование такого файла при сжатии (опция --externs myexterns.js) приведет к тому, что все обращения к символам dojo и к dojo._scopeMap будут не сжаты, а оставлены "как есть". - - -### Экспорт - -*Экспорт* -- программный ход, основанный на следующем правиле поведения компилятора. - -**Компилятор заменяет обращения к свойствам через кавычки на точку, и при этом не трогает название свойства.** - -Например, window['User'] превратится в window.User, но не дальше. - -Таким образом можно *"экспортировать"* нужные функции и объекты: - -```js -function SayWidget(elem) { - this.elem = elem - this.init() -} -window['SayWidget'] = SayWidget; -``` - -На выходе: - -```js -function a(b) { - this.a = b; - this.b() -} -window.SayWidget = a; -``` - -Обратим внимание -- сама функция SayWidget была переименована в a. Но затем -- экспортирована как window.SayWidget, и таким образом доступна внешним скриптам. - -Добавим пару методов в прототип: - -```js -function SayWidget(elem) { - this.elem = elem; - this.init(); -} - -SayWidget.prototype = { - init: function() { - this.elem.style.display = 'none' - }, - - setSayHandler: function() { - this.elem.onclick = function() { - alert("hi") - }; - } -} - -window['SayWidget'] = SayWidget; -SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler; -``` - -После сжатия: - -```js -//+ no-beautify -function a(b) { - this.a = b; - this.b() -} -a.prototype = {b:function() { - this.a.style.display = "none" -}, c:function() { - this.a.onclick = function() { - alert("hi") - } -}}; -window.SayWidget = a; -a.prototype.setSayHandler = a.prototype.c; -``` - -Благодаря строке - -```js -SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler -``` - -метод setSayHandler экспортирован и доступен для внешнего вызова. - -Сама строка экспорта выглядит довольно глупо. По виду -- присваиваем свойство самому себе. - -Но логика сжатия GCC работает так, что такая конструкция является экспортом. Справа переименование свойства setSayHandler происходит, а слева -- нет. - -[smart header="Планируйте жизнь после сжатия"] - -Рассмотрим следующий код: - -```js -window['Animal'] = function() { - this.blabla = 1; - this['blabla'] = 2; -} -``` - -После сжатия: - -```js -window.Animal = function() { - this.a = 1; - this.blabla = 2 -}; -``` - -Как видно, первое обращение к свойству blabla сжалось, а второе (как и все аналогичные) -- преобразовалось в синтаксис через точку. -В результате получили некорректное поведение кода. - -Так что, используя продвинутый режим оптимизации, планируйте поведение кода после сжатия. - -**Если где-то возможно обращение к свойствам через квадратные скобки по полному имени -- такое свойство должно быть экспортировано.** -[/smart] - -### goog.exportSymbol и goog.exportProperty - -В библиотеке [Google Closure Library](https://developers.google.com/closure/library/) для экспорта есть специальная функция goog.exportSymbol. Вызывается так: - -```js -goog.exportSymbol('my.SayWidget', SayWidget) -``` - -Эта функция по сути работает также, как и рассмотренная выше строка с присвоением свойства, но при необходимости создает нужные объекты. - -Она аналогична коду: - -```js -window['my'] = window['my'] || {} -window['my']['SayWidget'] = SayWidget -``` - -То есть, если путь к объекту не существует -- exportSymbol создаст нужные пустые объекты. - -Функция goog.exportProperty экспортирует свойство объекта: - -```js -goog.exportProperty(SayWidget.prototype, 'setSayHandler', SayWidget.prototype.setSayHandler) -``` - -Строка выше - то же самое, что и: - -```js -SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler -``` - -Зачем они нужны, если все можно сделать простым присваиванием? - -Основная цель этих функций -- во взаимодействии с Google Closure Compiler. Они дают информацию компилятору об экспортах, которую он может использовать. - -Например, есть недокументированная внутренняя опция externExportsPath, которая генерирует из всех экспортов файл экстернов. Таким образом можно распространять откомпилированный JavaScript-файл как внешнюю библиотеку, с файлом экстернов для удобного внешнего связывания. - -Кроме того, экспорт через эти функциями удобен и нагляден. - -Если вы используете продвинутый режим оптимизации, то можно взять их из файла base.js Google Closure Library. Можно и подключить этот файл целиком -- оптимизатор при продвинутом сжатии вырежет из него почти всё лишнее, так что overhead будет минимальным. - -### Отличия экспорта от экстерна - -Между экспортом и экстерном есть кое-что общее. И то и другое дает возможность доступа к объектам под исходным именем, до переименования. - -Но, в остальном, это совершенно разные вещи. - -
      - - - - - - - - - - - - - - - - - - - - -
      ЭкстернЭкспорт
      Служит для тотального запрета на переименование всех обращений к свойству. -Задумано для сохранения обращений к стандартным объектам браузера, внешним библиотекам.Служит для открытия доступа к свойству извне под указанным именем. -Задумано для открытия внешнего интерфейса к сжатому скрипту.
      Работает со свойством, объявленным вне скрипта. -Вы не можете объявить новое свойство в скрипте и сделать его экстерном.Создает ссылку на свойство, объявленное в скрипте.
      Если window - экстерн, то все обращения к window в скрипте останутся как есть.Если user экспортируется, то создается только одна ссылка под полным именем, а все остальные обращения будут сокращены.
      - - -## Стиль разработки - -Посмотрим, как сжиматель поведёт себя на следующем, типичном, объявлении библиотеки: - -```js -(function(window, undefined) { - - // пространство имен и локальная перменная для него - var MyFramework = window.MyFramework = {}; - - // функция фреймворка, доступная снаружи - MyFramework.publicOne = function() { - makeElem(); - }; - - // приватная функция фреймворка - function makeElem() { - var div = document.createElement('div'); - document.body.appendChild(div); - } - - // еще какая-то функция - MyFramework.publicTwo = function() {}; - -})(window); - -// использование -MyFramework.publicOne(); -``` - -Результат компиляции в обычном режиме: - -```js -//+ no-beautify -// java -jar compiler.jar --js myframework.js --formatting PRETTY_PRINT -(function(a) { - a = a.MyFramework = {}; - a.publicOne = function() { - var a = document.createElement("div"); - document.body.appendChild(a) - }; - a.publicTwo = function() { - } -})(window); -MyFramework.publicOne(); -``` - -Это -- примерно то, что мы ожидали. Неиспользованный метод `publicTwo` остался, локальные свойства переименованы и заинлайнены. - -А теперь продвинутый режим: - -```js -// --compilation_level ADVANCED_OPTIMIZATIONS -window.a = {}; -MyFramework.b(); -``` - -Оно не работает! Компилятор попросту не разобрался, что и как вызывается, и превратил рабочий JS-файл в один сплошной баг. - -В зависимости от версии GCC у вас может быть и что-то другое. - -Всё дело в том, что такой стиль объявления нетипичен для инструментов, которые в самом Google разрабатываются и сжимаются этим минификатором. - -Типичный правильный стиль: - -```js -// пространство имен и локальная перменная для него -var MyFramework = {}; - -MyFrameWork._makeElem = function() { - var div = document.createElement('div'); - document.body.appendChild(div); -}; - -MyFramework.publicOne = function() { - MyFramework._makeElem(); -}; - -MyFramework.publicTwo = function() {}; - -// использование -MyFramework.publicOne(); -``` - -Обычное сжатие здесь будет бесполезно, а вот продвинутый режим идеален: - -```js -// в зависимости от версии GCC результат может отличаться -MyFrameWork.a = function() { - var a = document.createElement("div"); - document.body.appendChild(a) -}; -MyFrameWork.a(); -``` - -Google Closure Compiler не только разобрался в структуре и удалил лишний метод - он заинлайнил функции, чтобы итоговый размер получился минимальным. - -Как говорится, преимущества налицо. - -## Резюме - -Продвинутый режим оптимизации сжимает, оптимизирует и, при возможности, удаляет все свойства и методы, за исключением экстернов. - -Это является принципиальным отличием, по сравнению с другими упаковщиками. - -Отказ от сохранения внешней ссылочной целостности с одной стороны позволяет увеличить уровень сжатия, но требует поддержки со стороны разработчика. - -Основная проблема этого сжатия -- усложнение разработки. Добавляется дополнительный уровень возможных проблем: сжатие. Конечно, можно отлаживать и сжатый код, для этого придуманы [Source Maps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/), но клиентская разработка и без того достаточно сложна. - -Поэтому его используют редко. - -Как правило, есть две причины для использования продвинутого режима: -
        -
      1. **Обфускация кода.** - -Если в коде после обычного сжатия ещё как-то можно разобраться, то после продвинутого -- уже нет. Всё переименовано и заинлайнено. В теории это, конечно, возможно, но "порог входа" в такой код несоизмеримо выше. - -Судя по виду скриптов на сайтах, созданных Google, сам Google жмет свои скрипты именно продвинутым режимом оптимизации. И библиотека Google Closure Library тоже рассчитана на него.
      2. -
      3. **Хорошие сжатие виджетов, счётчиков.** - -Небольшой код, который отдаётся наружу, может быть сжат в продвинутом режиме. Так как он небольшой -- все ошибки можно легко исправить, а продвинутый режим гарантирует наилучшее сжатие.
      4. -
      diff --git a/6-optimize/6-gcc-check-types/article.md b/6-optimize/6-gcc-check-types/article.md deleted file mode 100644 index 8c625359..00000000 --- a/6-optimize/6-gcc-check-types/article.md +++ /dev/null @@ -1,177 +0,0 @@ -# GCC: статическая проверка типов - -Google Closure Compiler, как и любой кошерный компилятор, старается проверить правильность кода и предупредить о возможных ошибках. - -Первым делом он, разумеется, проверяет структуру кода и сразу же выдает такие ошибки как пропущенная скобка или лишняя запятая. - -Но, кроме этого, он умеет проверять типы переменных, используя как свои собственные знания о встроенных javascript-функциях и преобразованиях типов, -так и информацию о типах из JSDoc, указываемую javascript-разработчиком. - -Это обеспечивает то, чем так гордятся компилируемые языки -- статическую проверку типов, что позволяет избежать лишних ошибок во время выполнения. -[cut] - -Для вывода предупреждений при проверки типов используется флаг `--jscomp_warning checkTypes`. - -## Задание типа при помощи аннотации - -Самый очевидный способ задать тип -- это использовать аннотацию. Полный список аннотаций вы найдете в документации. - -В следующем примере параметр id функции f1 присваивается переменной boolVar другого типа: - -```js -/** @param {number} id */ -function f(id) { - /** @type {boolean} */ - var boolVar; - - boolVar = id; // (!) -} -``` - -Компиляция с флагом `--jscomp_warning checkTypes` выдаст предупреждение: - -``` -f.js:6: WARNING - assignment -found : number -required: boolean - boolVar = id; // (!) - ^ -``` - -Действительно: произошло присвоение значения типа number переменной типа boolean. - -Типы отслеживаются по цепочке вызовов. - -Еще пример, на этот раз вызов функции с некорректным параметром: - -```js -/** @param {number} id */ -function f1(id) { - f2(id); // (!) -} - -/** @param {string} id */ -function f2(id) {} -``` - -Такой вызов приведёт к предупреждению со стороны минификатора: - -``` -f2.js:3: WARNING - actual parameter 1 of f2 does not match formal parameter -found : number -required: string - f2(id); // (!) - ^ -``` - -Действительно, вызов функции f2 произошел с числовым типом вместо строки. - -**Отслеживание приведений и типов идёт при помощи графа взаимодействий и выведению (infer) типов, который строит GCC по коду.** - -## Знания о преобразовании типов - -Google Closure Compiler знает, как операторы javascript преобразуют типы. Такой код уже не выдаст ошибку: - -```js -/** @param {number} id */ -function f1(id) { - /** @type {boolean} */ - var boolVar; - - boolVar = !!id -} -``` - -Действительно - переменная преобразована к типу boolean двойным оператором НЕ. -А код boolVar = 'test-'+id выдаст ошибку, т.к. конкатенация со строкой дает тип string. - -## Знание о типах встроенных функций, объектные типы - -Google Closure Compiler содержит описания большинства встроенных объектов и функций javascript вместе с типами параметров и результатов. - -Например, объектный тип Node соответствует узлу DOM. - -Пример некорректного кода: - -```js -/** @param {Node} node */ -function removeNode(node) { - node.parentNode.removeChild(node) -} -document.onclick = function() { - removeNode("123") -} -``` - -Выдаст предупреждение - -``` -f3.js:7: WARNING - actual parameter 1 of removeNode does not match formal parameter -found : string -required: (Node|null) - removeNode("123") - ^ -``` - -Обратите внимание - в этом примере компилятор выдает required: Node|null. Это потому, что указание объектного типа (не элементарного) подразумевает, что в функцию может быть передан null. - -В следующем примере тип указан жестко, без возможности обнуления: - -```js -*!* -/** @param {!Node} node */ -*/!* -function removeNode(node) { - node.parentNode.removeChild(node) -} -``` - -Восклицательный знак означает, что параметр обязатален. - -Найти описания встроенных типов и объектов javascript вы можете в файле экстернов: externs.zip находится в корне архива compiler.jar. - -## Интеграция с проверками типов из Google Closure Library - -В Google Closure Library есть функции проверки типов: goog.isArray, goog.isDef, goog.isNumber и т.п. - -Google Closure Compiler знает о них и понимает, что внутри следующего if переменная может быть только функцией: - -```js -var goog = { - isFunction: function(f) { - return typeof f == 'function' - } -} - -if (goog.isFunction(func)) { - func.apply(1, 2) -} -``` - -Сжатие с проверкой выдаст предупреждение: - -``` -f.js:6: WARNING - actual parameter 2 of Function.apply does not match formal parameter -found : number -required: (Object|null|undefined) - func.apply(1, 2) - ^ ^ -``` - -То есть, компилятор увидел, что код, использующий func находится в `if (goog.isFunction(func))` и сделал соответствующий вывод, что это в этой ветке `func` является функцией, а значит вызов `func.apply(1,2)` ошибочен (второй аргумент не может быть числом). - -Дело тут именно в интеграции с Google Closure Library. Если поменять `goog` на `g` -- предупреждения не будет. - -## Резюме - -Из нескольких примеров, которые мы рассмотрели, должна быть понятна общая логика проверки типов. - -Соответствующие различным типам и ограничениям на типы аннотации вы можете найти в Документации Google. В частности, возможно указание нескольких возможных типов, типа undefined и т.п. - -Также можно указывать количество и тип параметров функции, ключевого слова this, объявлять классы, приватные методы и интерфейсы. - -Проверка типов javascript, предоставляемая Google Closure Compiler -- пожалуй, самая продвинутая из существующих на сегодняшний день. - -C ней аннотации, документирующие типы и параметры, становятся не просто украшением, а реальным средством проверки, уменьшающим количество ошибок на production. - -Очень подробно проверка типов описана в книге [Closure: The Definitive Guide](http://www.ozon.ru/context/detail/id/6089988/), автора Michael Bolin. \ No newline at end of file diff --git a/6-optimize/7-gcc-closure-library/article.md b/6-optimize/7-gcc-closure-library/article.md deleted file mode 100644 index 0c02e685..00000000 --- a/6-optimize/7-gcc-closure-library/article.md +++ /dev/null @@ -1,180 +0,0 @@ -# GCC: интеграция с Google Closure Library - -Google Closure Compiler содержит ряд специальных возможностей для интеграции с Google Closure Library. - -Здесь важны две вещи. -
        -
      1. Для их использования возможно использовать минимум от Google Closure Library. Например, взять одну или несколько функций из библиотеки.
      2. -
      3. GCC -- расширяемый компилятор, можно добавить к нему свои "фазы оптимизации" для интеграции с другими инструментами и фреймворками.
      4. -
      -[cut] - -Интеграция с Google Closure Library подключается флагом --process_closure_primitives, который по умолчанию установлен в true. То есть, она включена по умолчанию. - -Этот флаг запускает специальный проход компилятора, описанный классом ProcessClosurePrimitives и подключает дополнительную проверку типов ClosureReverseAbstractInterpreter. - -Мы рассмотрим все действия, которые при этом происходят, а также некоторые опции, которые безопасным образом используют символы Google Closure Library без объявления флага. - -## Преобразование основных символов - -Следующие действия описаны в классе ProcessClosurePrimitives. - -### Замена константы COMPILED - -В Google Closure Library есть переменная: - -```js -/** - * @define {boolean} ... - */ -var COMPILED = false; -``` - -Проход ProcessClosurePrimitives переопределяет ее в true и использует это при оптимизациях, удаляя ветки кода, не предназначены для запуска на production. - -Такие функции существуют, например, в ядре Google Closure Library. К ним в первую очередь относятся вызовы, предназначенные для сборки и проверки зависимостей. Они содержат код, обрамленный проверкой COMPILED, например: - -```js -goog.require = function(rule) { - // ... - if (!COMPILED) { - // основное тело функции - } -} -``` - -Аналогично может поступить и любой скрипт, даже без использования Google Closure Library: - -```js -/** @define {boolean} */ -var COMPILED = false - -Framework = {} - -Framework.sayCompiled = function() { - if (!COMPILED) { - alert("Not compressed") - } else { - alert("Compressed") - } -} -``` - -Для того, чтобы сработало, нужно сжать в продвинутом режиме: - -```js -Framework = {}; -Framework.sayCompiled = Framework.a = function() { - alert( "Compressed" ); -}; -``` - -Компилятор переопределил COMPILED в true и произвел соответствующие оптимизации. - -### Автоподстановка локали - -В Google Closure Compiler есть внутренняя опция locale - -Эта опция переопределяет переменную goog.LOCALE на установленную при компиляции. - -Для использования опции locale, на момент написания статьи, ее нужно задать в Java коде компилятора, т.к. соответствующего флага нет. - -Как и COMPILED, константу goog.LOCALE можно и использовать в своем коде без библиотеки Google Closure Library. - -### Проверка зависимостей - -Директивы goog.provide, goog.require, goog.addDependency обрабатываются особым образом. - -Все зависимости проверяются, а сами директивы проверки -- удаляются из сжатого файла. - -### Экспорт символов - -Вызов goog.exportSymbol задаёт экспорт символа. - -Если подробнее, то код goog.exportSymbol('a',myVar) эквивалентен -`window['a'] = myVar`. - - -### Автозамена классов CSS - -Google Closure Library умеет преобразовывать классы CSS на более короткие по списку, который задаётся при помощи `goog.setCssNameMapping`. - -Например, следующая функция задает такой список. - -```js - goog.setCssNameMapping({ - "goog-menu": "a", - "goog-menu-disabled": "a-b", - "CSS_LOGO": "b", - "hidden": "c" - }); -``` - -Тогда следующий вызов преобразуется в "a a-b": - -```js -goog.getCssName('goog-menu') + ' ' + goog.getCssName('goog-menu', 'disabled') -``` - -Google Closure Compiler производит соответствующие преобразования в сжатом файле и удаляет вызов setCssNameMapping из кода. - -Чтобы это сжатие работало, в HTML/CSS классы тоже должны сжиматься. По всей видимости, в приложениях Google это и происходит, но соответствующие инструменты закрыты от публики. - -### Генерация списка экстернов - -При объявлении внутренней опции externExportsPath, содержащей путь к файлу, в этот файл будут записаны все экспорты, описанные через goog.exportSymbol/goog.exportProperty. - -В дальнейшем этот файл может быть использован как список экстернов для компиляции. - -Эта опция может быть полезна для создания внешних библиотек, распространяемых со списком экстернов. - -Для её использования нужна своя обёртка вокруг компилятора на Java. Соответствующий проход компилятора описан в классе ExternExportsPass. - -### Проверка типов - -В Google Closure Library есть ряд функций для проверки типов. Например: goog.isArray, goog.isString, goog.isNumber, goog.isDef и т.п. - -Компилятор использует их для проверки типов, более подробно см. [](/gcc-check-types) - -Эта логика описана в классе ClosureReverseAbstractInterpreter. Названия функций, определяющих типы, жестко прописаны в Java-коде, поменять их на свои без модификации исходников нельзя. - -### Автогенерация экспортов из аннотаций - -Для этого в Google Closure Compiler есть внутренняя опция generateExports. - -Эта недокументированная опция добавляет проход компилятора, описанный классом GenerateExports. - -Он читает аннотации @export и создает из них экспортирующие вызовы goog.exportSymbol/exportProperty. Название экспортирующих функций находится в классе соглашений кодирования, каким по умолчанию является GoogleCodingConvention. - -Например: - -```js -/** @export */ -function Widget() {} - /** @export */ -Widget.prototype.hide = function() { - this.elem.style.display = 'none' -} -``` - -После компиляции в продвинутом режиме: - -```js -function a() {} -goog.d("Widget", a); -a.prototype.a = function() { - this.b.style.display = "none" -}; -goog.c(a.prototype, "hide", a.prototype.a); -``` - -Свойства благополучно экспортированы. Удобно. - -### Резюме - -Google Closure Compiler содержит дополнительные фичи, облегчающие интеграцию с Google Closure Library. Некоторые из них весьма полезны, но требуют создания своего Java-файла, который ставит внутренние опции. - -При обработке символов компилятор не смотрит, подключена ли библиотека, он находит обрабатывает их просто по именам. Поэтому вы можете использовать свою реализацию соответствующих функций. - -Google Closure Compiler можно легко расширить, добавив свои опции и проходы оптимизатора, для интеграции с вашими инструментами. - diff --git a/6-optimize/8-memory-removechild-innerhtml/article.md b/6-optimize/8-memory-removechild-innerhtml/article.md deleted file mode 100644 index 5882124d..00000000 --- a/6-optimize/8-memory-removechild-innerhtml/article.md +++ /dev/null @@ -1,147 +0,0 @@ -# Очистка памяти при removeChild/innerHTML - -Управление памятью в случае с DOM работает по сути так же, как и с обычными JavaScript-объектами. Пока объект достижим -- он остаётся в памяти. - -Но есть и особенности, поскольку DOM весь переплетён ссылками. -[cut] -## Пример -Для примера рассмотрим следующий HTML: - -```html - - - -
      -
        -
      • Список
      • -
      - Сосед -
      - - - -``` - -Его DOM (показаны только основные ссылки): - - - -## Удаление removeChild - -Операция `removeChild` разрывает все связи удаляемым узлом и его родителем. - -Поэтому, если удалить `DIV` из `BODY`, то всё поддерево под `DIV` станет недостижимым и будет удалено. - -А что происходит, если на какой-то элемент внутри удаляемого поддерева есть ссылка? - -Например, `UL` сохранён в переменную `list`: - -```js -var list = document.getElementsByTagName('UL')[0]; -document.body.removeChild(document.body.children[0]); -``` - -В этом случае, так как из этого `UL` можно по ссылкам добраться до любого другого места DOM, то получается, что все объекты по-прежнему достижимы и должны остаться в памяти: - - - -То есть, DOM-объекты при использовании `removeChild` работают по той же логике, что и обычные объекты. - -## Удаление через innerHTML - -А вот удаление через очистку `elem.innerHTML="..."` браузеры интерпретируют по-разному. - -По идее, при присвоении `elem.innerHTML=html` из DOM должны удаляться предыдущие узлы и добавляться новые, из указанного `html`. Но стандарт ничего не говорит о том, что делать с узлами после удаления. И тут разные браузеры имеют разное мнение. - -Посмотрим, что произойдёт с DOM-структурой при очистке `BODY`, если на какой-либо элемент есть ссылка. - -```js -var list = document.getElementsByTagName('UL')[0]; -document.body.innerHTML = ""; -``` - -Обращаю внимание -- связь разрывается только между `DIV` и `BODY`, т.е. на верхнем уровне, а `list` -- это произвольный элемент. - -Чтобы увидеть, что останется в памяти, а что нет -- запустим код: - -```html - -
      -
        -
      • Список
      • -
      - Сосед -
      - - -``` - -Как ни странно, браузеры ведут себя по-разному: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      `parentNode``nextSibling``children.length`
      Chrome/Safari/Opera`null``null``1`
      Firefoxузел DOMузел DOM`1`
      IE 11-`null``null``0`
      - -Иными словами, браузеры ведут себя с различной степенью агрессивности по отношению к элементам. - -
      -
      Firefox
      -
      Главный пацифист. Оставляет всё, на что есть ссылки, т.е. элемент, его родителя, соседей и детей, в точности как при `removeChild`.
      -
      Chrome/Safari/Opera
      -
      Считают, что раз мы задали ссылку на `UL`, то нам нужно только это поддерево, а остальные узлы (соседей, родителей) можно удалить.
      -
      Internet Explorer
      -
      Как ни странно, самый агрессивный. Удаляет вообще всё, кроме узла, на который есть ссылка. Это поведение одинаково для всех версий IE.
      -
      - -На иллюстрации ниже показано, какую часть DOM оставит каждый из браузеров: - - -## Итого - -Если на какой-то DOM-узел есть ссылка, то: - -
        -
      • При использовании `removeChild` на родителе (или на этом узле, не важно) все узлы, достижимые из данного, остаются в памяти. - -То есть, фактически, в памяти может остаться большая часть дерева DOM. Это даёт наибольшую свободу в коде, но может привести к большим "утечкам памяти" из-за сохранения данных, которые реально не нужны.
      • -
      • При удалении через `innerHTML` браузеры ведут себя с различной степенью агрессивности. Кросс-браузерно гарантировано одно: сам узел, на который есть ссылка, останется в памяти. - -Поэтому обращаться к соседям и детям узла, предок которого удалён через присвоение `innerHTML`, нельзя.
      • -
      - - - - diff --git a/6-optimize/8-memory-removechild-innerhtml/html-innerhtml.png b/6-optimize/8-memory-removechild-innerhtml/html-innerhtml.png deleted file mode 100644 index e606be2a..00000000 Binary files a/6-optimize/8-memory-removechild-innerhtml/html-innerhtml.png and /dev/null differ diff --git a/6-optimize/8-memory-removechild-innerhtml/html-list.png b/6-optimize/8-memory-removechild-innerhtml/html-list.png deleted file mode 100644 index 1ffd8ace..00000000 Binary files a/6-optimize/8-memory-removechild-innerhtml/html-list.png and /dev/null differ diff --git a/6-optimize/8-memory-removechild-innerhtml/html.png b/6-optimize/8-memory-removechild-innerhtml/html.png deleted file mode 100644 index ca8da064..00000000 Binary files a/6-optimize/8-memory-removechild-innerhtml/html.png and /dev/null differ diff --git a/7-frames-and-windows/6-clickjacking/article.md b/7-frames-and-windows/6-clickjacking/article.md index f6efd27c..18cb7cc5 100644 --- a/7-frames-and-windows/6-clickjacking/article.md +++ b/7-frames-and-windows/6-clickjacking/article.md @@ -63,12 +63,11 @@ iframe { /* iframe с сайта-жертвы */ [codetabs src="clickjacking" height=200] -Итак, все, что нужно для проведения атаки -- это правильно расположить iframe на вредоносной странице. В большинстве случаев это делается средствами HTML/CSS. +Итак, все, что нужно для проведения атаки -- это правильно расположить iframe на вредоносной странице, так чтобы кнопка с Facebook оказалась над "Жми тут!". В большинстве случаев это возможно и делается обычным CSS-позиционированием. [smart header="С клавиатурой так не сделаешь"] Атака назвается "Clickjacking", то есть "угон клика", так как события клавиатуры "угнать" гораздо труднее. - -Посетителя можно заставить сфокусироваться на `` с невидимого `