renovations
|
@ -0,0 +1,18 @@
|
|||
Сделаем цикл по узлам `<li>`:
|
||||
|
||||
```js
|
||||
var lis = document.getElementsByTagName('li');
|
||||
|
||||
for (i = 0; i < lis.length; i++) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
В цикле для каждого `lis[i]` можно получить текст, используя свойство `firstChild`. Ведь первым в `<li>` является как раз текстовый узел, содержащий текст названия.
|
||||
|
||||
Также можно получить количество потомков, используя `lis[i].getElementsByTagName('li')`.
|
||||
|
||||
Напишите код с этой подсказкой.
|
||||
|
||||
Если уж не выйдет -- тогда откройте решение.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<ul>
|
||||
<li>Животные
|
||||
<ul>
|
||||
<li>Млекопитающие
|
||||
<ul>
|
||||
<li>Коровы</li>
|
||||
<li>Ослы</li>
|
||||
<li>Собаки</li>
|
||||
<li>Тигры</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Другие
|
||||
<ul>
|
||||
<li>Змеи</li>
|
||||
<li>Птицы</li>
|
||||
<li>Ящерицы</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Рыбы
|
||||
<ul>
|
||||
<li>Аквариумные
|
||||
<ul>
|
||||
<li>Гуппи</li>
|
||||
<li>Скалярии</li>
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
<li>Морские
|
||||
<ul>
|
||||
<li>Морская форель</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
var lis = document.getElementsByTagName('li');
|
||||
|
||||
for (i = 0; i < lis.length; i++) {
|
||||
// получить название из текстового узла
|
||||
var title = lis[i].firstChild.data;
|
||||
|
||||
title = title.trim(); // убрать лишние пробелы с концов
|
||||
|
||||
// получить количество детей
|
||||
var childCount = lis[i].getElementsByTagName('li').length;
|
||||
|
||||
alert(title + ': ' + childCount);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<ul>
|
||||
<li>Животные
|
||||
<ul>
|
||||
<li>Млекопитающие
|
||||
<ul>
|
||||
<li>Коровы</li>
|
||||
<li>Ослы</li>
|
||||
<li>Собаки</li>
|
||||
<li>Тигры</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Другие
|
||||
<ul>
|
||||
<li>Змеи</li>
|
||||
<li>Птицы</li>
|
||||
<li>Ящерицы</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Рыбы
|
||||
<ul>
|
||||
<li>Аквариумные
|
||||
<ul>
|
||||
<li>Гуппи</li>
|
||||
<li>Скалярии</li>
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
<li>Морские
|
||||
<ul>
|
||||
<li>Морская форель</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
// .. ваш код ..
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
14
2-ui/1-document/5-searching-elements-dom/2-tree-info/task.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Дерево
|
||||
|
||||
[importance 5]
|
||||
|
||||
Есть дерево из тегов `<ul>/<li>`.
|
||||
|
||||
Напишите код, который для каждого элемента `<li>` выведет:
|
||||
<ol>
|
||||
<li>Текст непосредственно в нём (без подразделов).</li>
|
||||
<li>Количество вложенных в него элементов `<li>` -- всех, с учётом вложенных.</li>
|
||||
</ol>
|
||||
|
||||
[demo src="solution"]
|
||||
|
5
6-optimize/1-optimize-intro/article.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Введение
|
||||
|
||||
В отличие от ряда других курсов учебника, этот раздел -- не является курсом как таковым. Связное и грамотное изложение темы требует времени, которое я пока не могу ему уделить. Но, надеюсь, смогу в будущем.
|
||||
|
||||
Пока что раздел содержит лишь некоторые статьи-заметки по теме оптимизации, которые, надеюсь, будут вам полезны.
|
513
6-optimize/2-minification/article.md
Normal file
|
@ -0,0 +1,513 @@
|
|||
# Как работают сжиматели JavaScript
|
||||
|
||||
Перед выкладыванием JavaScript на "боевую" машину -- пропускаем его через минификатор (также говорят "сжиматель"), который удаляет пробелы и по-всякому оптимизирует код, уменьшая его размер.
|
||||
|
||||
В этой статье мы посмотрим, как работают современные минификаторы, за счёт чего они укорачивают код и какие с ними возможны проблемы.
|
||||
[cut]
|
||||
|
||||
## Современные сжиматели
|
||||
|
||||
Рассматриваемые в этой статье алгоритмы и подходы относятся к минификаторам последнего поколения.
|
||||
|
||||
Вот их список:
|
||||
|
||||
<ul>
|
||||
<li>[Google Closure Compiler](https://developers.google.com/closure/compiler/) </li>
|
||||
<li>[UglifyJS](https://github.com/mishoo/UglifyJS)</li>
|
||||
<li>[Microsoft AJAX Minifier](http://ajaxmin.codeplex.com/)</li>
|
||||
</ul>
|
||||
Самые широко используемые -- первые два, поэтому будем рассматривать в первую очередь их.
|
||||
|
||||
Наша цель -- понять, как они работают, и что интересного с их помощью можно сотворить.
|
||||
|
||||
## С чего начать?
|
||||
|
||||
Для GCC:
|
||||
|
||||
<ol>
|
||||
<li>Убедиться, что стоит [Java](http://java.oracle.com)</li>
|
||||
<li>Скачать и распаковать [](http://closure-compiler.googlecode.com/files/compiler-latest.zip), нам нужен файл `compiler.jar`.</li>
|
||||
<li>Сжать файл `my.js`: `java -jar compiler.jar --charset UTF-8 --js my.js --js_output_file my.min.js`</li>
|
||||
</ol>
|
||||
|
||||
Обратите внимание на флаг `--charset` для GCC. Без него русские буквы будут закодированы во что-то типа `\u1234`.
|
||||
|
||||
Google Closure Compiler также содержит [песочницу](http://closure-compiler.appspot.com/home) для тестирования сжатия и [веб-сервис](https://developers.google.com/closure/compiler/docs/gettingstarted_api?hl=ru), на который код можно отправлять для сжатия. Но скачать файл обычно гораздо проще, поэтому его редко где используют.
|
||||
|
||||
Для UglifyJS:
|
||||
|
||||
<ol>
|
||||
<li>Убедиться, что стоит [Node.js](http://nodejs.org)</li>
|
||||
<li>Поставить `npm install -g uglify-js`.</li>
|
||||
<li>Сжать файл `my.js`: `uglifyjs my.js -o my.min.js`</li>
|
||||
</ol>
|
||||
|
||||
## Что делает минификатор?
|
||||
|
||||
Все современные минификаторы работают следующим образом:
|
||||
<ol>
|
||||
<li>Разбирают JavaScript-код в синтаксическое дерево.
|
||||
|
||||
Также поступает любой интерпретатор JavaScript перед тем, как его выполнять. Но затем, вместо исполнения кода...</li>
|
||||
<li>Бегают по этому дереву, анализируют и оптимизируют его.</li>
|
||||
<li>Записывают из синтаксического дерева получившийся код.</li>
|
||||
</ol>
|
||||
|
||||
## Как выглядит дерево?
|
||||
|
||||
Посмотреть синтаксическое дерево можно, запустив компилятор со специальным флагом.
|
||||
|
||||
Для GCC есть даже способ вывести его:
|
||||
|
||||
<ol>
|
||||
<li>Сначала сгенерируем дерево в формате [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` убирает лишнюю служебную информацию.
|
||||
</li>
|
||||
<li>Файл в этом формате используется в различных программах для графопостроения.
|
||||
|
||||
Чтобы превратить его в обычную картинку, подойдёт утилита `dot` из пакета [Graphviz](http://www.graphviz.org/):
|
||||
|
||||
```
|
||||
// конвертировать в формат png
|
||||
dot -Tpng my.dot -o my.png
|
||||
|
||||
// конвертировать в формат svg
|
||||
dot -Tsvg my.dot -o my.svg
|
||||
```
|
||||
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
Пример кода `my.js`:
|
||||
|
||||
```js
|
||||
function User(name) {
|
||||
|
||||
this.sayHi = function() {
|
||||
alert( name );
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Результат, получившееся из `my.js` дерево:
|
||||
|
||||
<img src="my.svg">
|
||||
|
||||
В узлах-эллипсах на иллюстрации выше стоит тип, например `FUNCTION` (функция) или `NAME` (имя переменной). Комментарии к ним на русском языке добавлены мной вручную.
|
||||
|
||||
Кроме него к каждому узлу привязаны конкретные данные. Сжиматель умеет ходить по этому дереву и менять его, как пожелает.
|
||||
|
||||
[smart header="Комментарии JSDoc"]
|
||||
Обычно когда код превращается в дерево -- из него естественным образом исчезают комментарии и пробелы. Они не имеют значения при выполнении, поэтому игнорируются.
|
||||
|
||||
Но Google Closure Compiler добавляет в дерево информацию из *комментариев JSDoc*, т.е. комментариев вида `/** ... */`, например:
|
||||
|
||||
```js
|
||||
*!*
|
||||
/**
|
||||
* Номер минимальной поддерживаемой версии IE
|
||||
* @const
|
||||
* @type {number}
|
||||
*/
|
||||
*/!*
|
||||
var minIEVersion = 8;
|
||||
```
|
||||
|
||||
Такие комментарии не создают новых узлов дерева, а добавляются в качестве информации к существующем. В данном случае -- к переменной `minIEVersion`.
|
||||
|
||||
В них может содержаться информация о типе переменной (`number`) и другая, которая поможет сжимателю лучше оптимизировать код (`const` -- константа).
|
||||
|
||||
[/smart]
|
||||
|
||||
## Оптимизации
|
||||
|
||||
Сжиматель бегает по дереву, ищет "паттерны" -- известные ему структуры, которые он знает, как оптимизировать, и обновляет дерево.
|
||||
|
||||
В разных минификаторах реализован разный набор оптимизаций, сами оптимизации применяются в разном порядке, поэтому результаты работы могут отличаться. В примерах ниже даётся результат работы GCC.
|
||||
|
||||
|
||||
<dl>
|
||||
<dt>Объединение и сжатие констант</dt>
|
||||
<dd>
|
||||
До оптимизации:
|
||||
|
||||
```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)};
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>`'my' + 'string'` -> `"mystring"`.</li>
|
||||
<li>`600 * 600 * 5` -> `18E5` (научная форма числа, для краткости).</li>
|
||||
<li>`1 && 0` -> `0`.</li>
|
||||
<li>`b && 0` -> без изменений, т.к. результат зависит от `b`.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Укорачивание локальных переменных</dt>
|
||||
<dd>
|
||||
До оптимизации:
|
||||
|
||||
```js
|
||||
function sayHi(*!*name*/!*, *!*message*/!*) {
|
||||
alert(name +" сказал: " + message);
|
||||
}
|
||||
```
|
||||
|
||||
После оптимизации:
|
||||
|
||||
```js
|
||||
//+ no-beautify
|
||||
function sayHi(a,b){alert(a+" сказал: "+b)};
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Локальная переменная заведомо доступна только внутри функции, поэтому обычно её переименование безопасно (необычные случаи рассмотрим далее).</li>
|
||||
<li>Также переименовываются локальные функции.</li>
|
||||
<li>Вложенные функции обрабатываются корректно.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Объединение и удаление локальных переменных</dt>
|
||||
<dd>
|
||||
До оптимизации:
|
||||
|
||||
```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)};
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Локальные переменные были переименованы. </li>
|
||||
<li>Лишние переменные убраны. Для этого сжиматель создаёт вспомогательную внутреннюю структуру данных, в которой хранятся сведения о "пути использования" каждой переменной. Если одна переменная заканчивает свой путь и начинает другая, то вполне можно дать им одно имя.</li>
|
||||
<li>Кроме того, операции `elem = getElementsById` и `elem.parentNode` объединены, но это уже другая оптимизация.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Уничтожение недостижимого кода, разворачивание `if`-веток</dt>
|
||||
<dd>
|
||||
|
||||
До оптимизации:
|
||||
|
||||
```js
|
||||
function test(node) {
|
||||
var parent = node.parentNode;
|
||||
|
||||
if (0) {
|
||||
alert( "Привет с параллельной планеты" );
|
||||
} else {
|
||||
alert( "Останется только один" );
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
alert( 1 );
|
||||
}
|
||||
```
|
||||
|
||||
После оптимизации:
|
||||
|
||||
```js
|
||||
//+ no-beautify
|
||||
function test(){alert("Останется только один")}
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Если переменная присваивается, но не используется, она может быть удалена. В примере выше эта оптимизация была применена к переменной `parent`, а затем и к параметру `node`.</li>
|
||||
<li>Заведомо ложная ветка `if(0) { .. }` убрана, заведомо истинная -- оставлена.
|
||||
|
||||
То же самое будет с условиями в других конструкциях, например `a = true ? c : d` превратится в `a = c`.</li>
|
||||
<li>Код после `return` удалён как недостижимый.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Переписывание синтаксических конструкций</dt>
|
||||
<dd>
|
||||
До оптимизации:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Конструкция `while` переписана в `for`.</li>
|
||||
<li>Конструкция `if (i) ...` переписана в `i&&...`.</li>
|
||||
<li>Конструкция `if (cond) ... else ...` была переписана в `cond ? ... : ...`.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Инлайнинг функций</dt>
|
||||
<dd>
|
||||
*Инлайнинг функции* -- приём оптимизации, при котором функция заменяется на своё тело.
|
||||
|
||||
До оптимизации:
|
||||
|
||||
```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)
|
||||
};
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Вызовы функций `createMessage` и `showElement` заменены на тело функций. В данном случае это возможно, так как функции используются всего по разу.</li>
|
||||
<li>Эта оптимизация применяется не всегда. Если бы каждая функция использовалась много раз, то с точки зрения размера выгоднее оставить их "как есть".</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Инлайнинг переменных</dt>
|
||||
<dd>Переменные заменяются на значение, если оно заведомо известно.
|
||||
|
||||
До оптимизации:
|
||||
|
||||
```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" );
|
||||
};
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Переменная `isVisible` заменена на `true`, после чего `if` стало возможным убрать.</li>
|
||||
<li>Переменная `hi` заменена на строку.</li>
|
||||
</ul>
|
||||
|
||||
Казалось бы -- зачем менять `hi` на строку? Ведь код стал ощутимо длиннее!
|
||||
|
||||
...Но всё дело в том, что минификатор знает, что дальше код будет сжиматься при помощи gzip. Во всяком случае, все правильно настроенные сервера так делают.
|
||||
|
||||
[Алгоритм работы gzip](http://www.gzip.org/algorithm.txt) заключается в том, что он ищет повторы в данных и выносит их в специальный "словарь", заменяя на более короткий идентификатор. Архив как раз и состоит из словаря и данных, в которых дубликаты заменены на идентификаторы.
|
||||
|
||||
Если вынести строку обратно в переменную, то получится как раз частный случай такого сжатия -- взяли `"Привет вам из JavaScript"` и заменили на идентификатор `hi`. Но gzip справляется с этим лучше, поэтому эффективнее будет оставить именно строку. Gzip сам найдёт дубликаты и сожмёт их.
|
||||
|
||||
Плюс такого подхода станет очевиден, если сжать gzip оба кода -- до и после минификации. Минифицированный gzip-сжатый код в итоге даст меньший размер.
|
||||
</dd>
|
||||
|
||||
<dt>Разные мелкие оптимизации</dt>
|
||||
<dd>Кроме основных оптимизаций, описанных выше, есть ещё много мелких:
|
||||
|
||||
<ul>
|
||||
<li>Убираются лишние кавычки у ключей
|
||||
|
||||
```js
|
||||
//+ no-beautify
|
||||
{"prop" : "val" } => {prop:"val"}
|
||||
```
|
||||
|
||||
</li>
|
||||
<li>Упрощаются простые вызовы `Array/Object`
|
||||
|
||||
```js
|
||||
//+ no-beautify
|
||||
a = new Array() => a = []
|
||||
o = new Object() => o = {}
|
||||
```
|
||||
|
||||
Эта оптимизация предполагает, что `Array` и `Object` не переопределены программистом. Для включения её в UglifyJS нужен флаг `--unsafe`.
|
||||
</li>
|
||||
<li>...И еще некоторые другие мелкие изменения кода...</li>
|
||||
</ul>
|
||||
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
## Подводные камни
|
||||
|
||||
Описанные оптимизации, в целом, безопасны, но есть ряд подводных камней.
|
||||
|
||||
### Конструкция 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` мы не имеем права переименовывать локальные переменные. Причём (!), если функция является вложенноой, то и во внешних функциях тоже.
|
||||
|
||||
А ведь сжатие переменных -- очень важная оптимизация. Как правило, она уменьшает размер сильнее всего.
|
||||
|
||||
Что делать? Разные минификаторы поступают по-разному.
|
||||
|
||||
<ul>
|
||||
<li>UglifyJS -- не будет переименовывать переменные. Так что наличие `with/eval` сильно повлияет на степень сжатие кода.</li>
|
||||
<li>GCC -- всё равно сожмёт локальные переменные. Это, конечно же, может привести к ошибкам, причём в сжатом коде, отлаживать который не очень-то удобно. Поэтому он выдаст предупреждение о наличии опасной конструкции.</li>
|
||||
</ul>
|
||||
|
||||
Ни тот ни другой вариант нас, по большому счёту, не устраивают.
|
||||
|
||||
**Для того, чтобы код сжимался хорошо и работал правильно, не используем `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+ эта компиляция не работает в любом случае, лучше избавиться от неё вообще.
|
||||
|
||||
В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным.
|
||||
|
1
6-optimize/2-minification/my.svg
Normal file
After Width: | Height: | Size: 13 KiB |
172
6-optimize/3-better-minification/article.md
Normal file
|
@ -0,0 +1,172 @@
|
|||
# Улучшаем сжатие кода
|
||||
|
||||
Здесь мы обсудим разные приёмы, которые используются, чтобы улучшить сжатие кода.
|
||||
[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 это делают опции компилятора:
|
||||
<ul>
|
||||
<li>`drop_debugger` -- убирает вызовы `debugger`.</li>
|
||||
<li>`drop_console` -- убирает вызовы `console.*`.</li>
|
||||
</ul>
|
||||
|
||||
Можно написать и дополнительную функцию преобразования, которая убирает другие вызовы, например для `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.*`.
|
||||
|
317
6-optimize/4-memory-leaks/article.md
Normal file
|
@ -0,0 +1,317 @@
|
|||
# Утечки памяти
|
||||
|
||||
*Утечки памяти* происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.
|
||||
|
||||
Обычно это происходит автоматически ([](/memory-management)). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:
|
||||
|
||||
[cut]
|
||||
<ol>
|
||||
<li>Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.</li>
|
||||
<li>Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включенным, чтобы не закрывать браузер с кучей вкладок. Приходит утром -- а браузер съел всю память <strike>и рухнул</strike> и сильно тормозит.</li>
|
||||
</ol>
|
||||
Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре 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 = '<table><tr><td>1</td></tr></table>';
|
||||
|
||||
*!*
|
||||
elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке
|
||||
// при том, что мы даже не сохраняем значение в переменную
|
||||
*/!*
|
||||
|
||||
elem.removeChild(elem.firstChild); // удалить таблицу (*)
|
||||
// alert(elem.childNodes.length) // выдал бы 0, elem очищен, всё честно
|
||||
}
|
||||
```
|
||||
|
||||
Полный пример (IE8):
|
||||
|
||||
[codetabs src="leak-ie8-table"]
|
||||
|
||||
Особенности:
|
||||
<ul>
|
||||
<li>Если убрать отмеченную строку, то утечки не будет.</li>
|
||||
<li>Если заменить строку `(*)` на `elem.innerHTML = ''`, то память будет очищена, т.к. этот способ работает по-другому, нежели просто `removeChild` (см. главу [](/memory-management)).</li>
|
||||
<li>Утечка произойдёт не только при доступе к `rows`, но и к другим свойствам, например `elem.firstChild.tBodies[0]`.</li>
|
||||
</ul>
|
||||
|
||||
Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией:
|
||||
|
||||
```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);
|
||||
}
|
||||
```
|
||||
|
||||
Как вы думаете, почему? Если вы внимательно читали то, что написано выше, то имеете информацию для ответа на этот вопрос..
|
||||
|
||||
Посмотрим, какая структура памяти создается при каждом запуске:
|
||||
|
||||
<img src="leak-xhr.svg">
|
||||
|
||||
Когда запускается асинхронный запрос `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);
|
||||
}
|
||||
```
|
||||
|
||||
<img src="xhr2.png">
|
||||
|
||||
Теперь циклической ссылки нет -- и не будет утечки.
|
||||
|
||||
|
||||
## Объемы утечек памяти
|
||||
|
||||
Объем "утекающей" памяти может быть небольшим. Тогда это почти не ощущается. Но так как замыкания ведут к сохранению переменных внешних функций, то одна функция может тянуть за собой много чего ещё.
|
||||
|
||||
Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера).
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
## Поиск и устранение утечек памяти
|
||||
|
||||
### Проверка на утечки
|
||||
|
||||
Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку.
|
||||
|
||||
<ul>
|
||||
<li>**Необходимо помнить, что браузер может очистить память не сразу когда объект стал недостижим, а чуть позже.** Например, сборщик мусора может ждать, пока не будет достигнут определенный лимит использования памяти, или запускаться время от времени.</li>
|
||||
|
||||
Поэтому если вы думаете, что нашли проблему и тестовый код, запущенный в цикле, течёт -- подождите примерно минуту, добейтесь, чтобы памяти ело стабильно и много. Тогда будет понятно, что это не особенность сборщика мусора.</li><li>**Если речь об IE, то надо смотреть "Виртуальную память" в списке процессов, а не только обычную "Память".** Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).</li>
|
||||
<li>Для простоты отладки, если есть подозрение на утечку конкретных объектов, в них добавляют большие свойства-маркеры. Например, подойдет фрагмент текста: `new Array(999999).join('leak')`.</li>
|
||||
</ul>
|
||||
|
||||
### Настройка браузера
|
||||
|
||||
Утечки могут возникать из-за расширений браузера, взимодействющих со страницей. Еще более важно, что **утечки могут быть следствием конфликта двух браузерных расширений** Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно.
|
||||
|
||||
Чтобы понять, в расширениях дело или нет, нужно отключить их:
|
||||
|
||||
<ol>
|
||||
<li>Отключить Flash.</li>
|
||||
<li>Отключить анивирусную защиту, проверку ссылок и другие модули и дополнения.</li>
|
||||
<li>Отключить плагины. Отключить ВСЕ плагины.
|
||||
<ul>
|
||||
<li>
|
||||
Для IE есть параметр коммандной строки:
|
||||
|
||||
```
|
||||
"C:\Program Files\Internet Explorer\iexplore.exe" -extoff
|
||||
```
|
||||
|
||||
Кроме того необходимо отключить сторонние расширения в свойствах IE.
|
||||
|
||||
<img src="ie9_disable1.png">
|
||||
<img src="ie9_disable2.png">
|
||||
|
||||
</li>
|
||||
<li>Firefox необходимо запускать с чистым профилем. Используйте следующую команду для запуска менеджера профилей и создания чистого пустого профиля:
|
||||
|
||||
```
|
||||
firefox --profilemanager
|
||||
```
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
## Инструменты
|
||||
|
||||
Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти.
|
||||
|
||||
<img src="chrome.png">
|
||||
|
||||
Можем посмотреть, сколько памяти и куда он использует.
|
||||
|
||||
Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается.
|
||||
|
||||
Замечательная статья на эту тему есть в документации: [Chrome Developer Tools: Heap Profiling](http://code.google.com/chrome/devtools/docs/heap-profiling.html).
|
||||
|
||||
Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: *Удача!*
|
||||
|
||||
<img src="goodluck.png">
|
BIN
6-optimize/4-memory-leaks/chrome.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
6-optimize/4-memory-leaks/goodluck.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
6-optimize/4-memory-leaks/ie1.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
6-optimize/4-memory-leaks/ie2.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
6-optimize/4-memory-leaks/ie9_disable1.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
6-optimize/4-memory-leaks/ie9_disable2.png
Normal file
After Width: | Height: | Size: 18 KiB |
35
6-optimize/4-memory-leaks/leak-ie8-2.view/index.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<script>
|
||||
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
|
||||
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
|
||||
function leak() {
|
||||
// Создаём новый DIV, добавляем к BODY
|
||||
var elem = document.createElement('div');
|
||||
document.body.appendChild(elem);
|
||||
|
||||
elem.__expando = {
|
||||
bigAss: new Array(1000000).join('lalala'),
|
||||
method: function() {} // создаётся круговая ссылка через замыкание
|
||||
};
|
||||
|
||||
// Удалить элемент из DOM. Браузер должен очистить память.
|
||||
elem.parentElement.removeChild(elem);
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
|
||||
<button onclick="leak()">Создать утечку!</button>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
34
6-optimize/4-memory-leaks/leak-ie8-table.view/index.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
var elem = document.createElement('div'); // любой элемент
|
||||
|
||||
// Течёт в настоящем IE8, Standards Mode
|
||||
// Не течёт в других IE. Не течёт в IE9 в режиме совместимости с IE8
|
||||
function leak() {
|
||||
|
||||
for (var i = 0; i < 2000; i++) {
|
||||
|
||||
elem.innerHTML = '<table><tr><td>1</td></tr></table>';
|
||||
|
||||
elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке
|
||||
// при том, что мы даже без сохраненяем значение в переменную
|
||||
|
||||
elem.removeChild(elem.firstChild); // удалить таблицу
|
||||
// elem.innerHTML = '' очистил бы память, он работает по-другому, см. главу "управление памятью"
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
|
||||
<button onclick="leak()">Создать утечку!</button>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
40
6-optimize/4-memory-leaks/leak-ie8-widget.view/index.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<script>
|
||||
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
|
||||
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
|
||||
function leak() {
|
||||
// Создаём новый DIV, добавляем к BODY
|
||||
var elem = document.createElement('div');
|
||||
document.body.appendChild(elem);
|
||||
|
||||
// Записываем в свойство ссылку на объект
|
||||
var menu = new Menu(elem);
|
||||
elem.menu = menu;
|
||||
|
||||
// Удалить элемент из DOM. Браузер должен очистить память.
|
||||
elem.parentElement.removeChild(elem);
|
||||
}
|
||||
|
||||
function Menu(elem) {
|
||||
elem.onclick = function() {};
|
||||
this.bigAss = new Array(1000000).join('lalala');
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
|
||||
<button onclick="leak()">Создать утечку!</button>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
37
6-optimize/4-memory-leaks/leak-ie8-xhr.view/index.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Страница создаёт объект <code>XMLHttpRequest</code> каждые 50мс.</p>
|
||||
|
||||
<p>Нажмите на кнопку и смотрите на память, она течёт в IE<9.</p>
|
||||
|
||||
<button onclick="setInterval(leak, 50);">Запустить</button>
|
||||
|
||||
<script>
|
||||
function leak() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', '?' + Math.random(), true);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||
document.getElementById('test').innerHTML++;
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div>Количество запросов: <span id="test">0</span></div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
39
6-optimize/4-memory-leaks/leak-ie8.view/index.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<script>
|
||||
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
|
||||
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
|
||||
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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
|
||||
<button onclick="leak()">Создать утечку!</button>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
37
6-optimize/4-memory-leaks/leak-xhr-2.svg
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="488px" height="246px" viewBox="0 0 488 246" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>leak-xhr-2.svg</title>
|
||||
<desc>Created with bin/sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="leak-xhr-2.svg" sketch:type="MSArtboardGroup">
|
||||
<rect id="Rectangle-2" stroke="#CFCE95" stroke-width="2" fill="#F5F5D9" sketch:type="MSShapeGroup" x="50" y="39" width="117" height="38"></rect>
|
||||
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="331" y="39" width="117" height="38"></rect>
|
||||
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="50" y="167" width="398" height="38"></rect>
|
||||
<text id="xhr" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#727155">
|
||||
<tspan x="97" y="63">xhr</tspan>
|
||||
</text>
|
||||
<text id="function" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#727155">
|
||||
<tspan x="359.210938" y="63">function</tspan>
|
||||
</text>
|
||||
<text id="LexicalEnvironment" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#924565">
|
||||
<tspan x="180.224609" y="190">LexicalEnvironment</tspan>
|
||||
</text>
|
||||
<path d="M110.5,93.5 L110.5,144.5" id="Line" stroke="#B8BAC1" stroke-width="2" stroke-linecap="square" fill="#B8BAC1" sketch:type="MSShapeGroup" transform="translate(110.500000, 119.000000) scale(1, -1) translate(-110.500000, -119.000000) "></path>
|
||||
<path id="Line-decoration-1" d="M110.5,144.5 C111.55,140.72 112.45,137.48 113.5,133.7 C111.4,133.7 109.6,133.7 107.5,133.7 C108.55,137.48 109.45,140.72 110.5,144.5 C110.5,144.5 110.5,144.5 110.5,144.5 Z" stroke="#B8BAC1" stroke-width="2" stroke-linecap="square" fill="#B8BAC1"></path>
|
||||
<path d="M187,61 L311.169243,61" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
|
||||
<path id="Line-decoration-1" d="M311,61 C307.22,59.95 303.98,59.05 300.2,58 C300.2,60.1 300.2,61.9 300.2,64 C303.98,62.95 307.22,62.05 311,61 C311,61 311,61 311,61 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
|
||||
<path d="M381.5,113.5 L381.5,144.5" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
|
||||
<path id="Line-decoration-1" d="M381.5,144.5 C382.55,140.72 383.45,137.48 384.5,133.7 C382.4,133.7 380.6,133.7 378.5,133.7 C379.55,137.48 380.45,140.72 381.5,144.5 C381.5,144.5 381.5,144.5 381.5,144.5 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
|
||||
<text id="onreadystatechange" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
|
||||
<tspan x="181.5" y="48">onreadystatechange</tspan>
|
||||
</text>
|
||||
<text id="[[Scope]]" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
|
||||
<tspan x="346" y="102">[[Scope]]</tspan>
|
||||
</text>
|
||||
<path d="M99.5,110.5 L122.845236,133.845236" id="Line" stroke="#CB1E31" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M122,110.5 L98.6547642,133.845236" id="Line-2" stroke="#CB1E31" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
43
6-optimize/4-memory-leaks/leak-xhr.svg
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="488px" height="308px" viewBox="0 0 488 308" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>leak-xhr.svg</title>
|
||||
<desc>Created with bin/sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="leak-xhr.svg" sketch:type="MSArtboardGroup">
|
||||
<rect id="Rectangle-2" stroke="#CFCE95" stroke-width="2" fill="#F5F5D9" sketch:type="MSShapeGroup" x="50" y="101" width="117" height="38"></rect>
|
||||
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="331" y="101" width="117" height="38"></rect>
|
||||
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="50" y="229" width="398" height="38"></rect>
|
||||
<text id="xhr" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#727155">
|
||||
<tspan x="97" y="125">xhr</tspan>
|
||||
</text>
|
||||
<text id="function" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#727155">
|
||||
<tspan x="359.210938" y="125">function</tspan>
|
||||
</text>
|
||||
<text id="LexicalEnvironment" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#924565">
|
||||
<tspan x="180.224609" y="252">LexicalEnvironment</tspan>
|
||||
</text>
|
||||
<path d="M110.5,154.5 L110.5,185.5" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup" transform="translate(110.500000, 170.000000) scale(1, -1) translate(-110.500000, -170.000000) "></path>
|
||||
<path id="Line-decoration-1" d="M110.5,185.5 C111.55,181.72 112.45,178.48 113.5,174.7 C111.4,174.7 109.6,174.7 107.5,174.7 C108.55,178.48 109.45,181.72 110.5,185.5 C110.5,185.5 110.5,185.5 110.5,185.5 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
|
||||
<path d="M187,123 L311.169243,123" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
|
||||
<path id="Line-decoration-1" d="M311,123 C307.22,121.95 303.98,121.05 300.2,120 C300.2,122.1 300.2,123.9 300.2,126 C303.98,124.95 307.22,124.05 311,123 C311,123 311,123 311,123 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
|
||||
<path d="M381.5,175.5 L381.5,206.5" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
|
||||
<path id="Line-decoration-1" d="M381.5,206.5 C382.55,202.72 383.45,199.48 384.5,195.7 C382.4,195.7 380.6,195.7 378.5,195.7 C379.55,199.48 380.45,202.72 381.5,206.5 C381.5,206.5 381.5,206.5 381.5,206.5 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
|
||||
<path d="M112.5,46.5 L112.5,77.5" id="Line" stroke="#8A704D" stroke-width="2" stroke-linecap="square" fill="#8A704D" sketch:type="MSShapeGroup"></path>
|
||||
<path id="Line-decoration-1" d="M112.5,77.5 C113.55,73.72 114.45,70.48 115.5,66.7 C113.4,66.7 111.6,66.7 109.5,66.7 C110.55,70.48 111.45,73.72 112.5,77.5 C112.5,77.5 112.5,77.5 112.5,77.5 Z" stroke="#8A704D" stroke-width="2" stroke-linecap="square" fill="#8A704D"></path>
|
||||
<text id="property" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
|
||||
<tspan x="78" y="208">property</tspan>
|
||||
</text>
|
||||
<text id="onreadystatechange" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
|
||||
<tspan x="181.5" y="110">onreadystatechange</tspan>
|
||||
</text>
|
||||
<text id="внутренняя-ссылка" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
|
||||
<tspan x="47" y="37">внутренняя ссылка</tspan>
|
||||
</text>
|
||||
<text id="[[Scope]]" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
|
||||
<tspan x="346" y="164">[[Scope]]</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
144
6-optimize/5-memory-leaks-jquery/article.md
Normal file
|
@ -0,0 +1,144 @@
|
|||
|
||||
# Утечки памяти при использовании jQuery
|
||||
|
||||
В jQuery для хранения обработчиков событий и других вспомогательных данных, связанных с DOM-элементами, используется внутренний объект, который в jQuery 1 доступен через <a href="http://api.jquery.com/jQuery.data/">$.data</a>.
|
||||
|
||||
В 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)` делает следующее:
|
||||
|
||||
<ol>
|
||||
<li>Элемент получает уникальный идентификатор, если у него такого еще нет:
|
||||
|
||||
```js
|
||||
elem[jQuery.expando] = id = ++jQuery.uuid; // средствами jQuery
|
||||
```
|
||||
|
||||
`jQuery.expando` -- это случайная строка, сгенерированная jQuery один раз при входе на страницу. Уникальное свойство, чтобы ничего важного не перезаписать.</li>
|
||||
<li>...А сами данные сохраняются в специальном объекте `jQuery.cache`:
|
||||
|
||||
```js
|
||||
//+ no-beautify
|
||||
jQuery.cache[id]['prop'] = { anything: "любой объект" };
|
||||
```
|
||||
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
Когда данные считываются из элемента:
|
||||
|
||||
<ol>
|
||||
<li>Уникальный идентификатор элемента извлекается из `id = elem[ jQuery.expando]`.
|
||||
<li>Данные считываются из `jQuery.cache[id]`.</li>
|
||||
</ol>
|
||||
|
||||
Смысл этого API в том, что DOM-элемент никогда не ссылается на JavaScript объект напрямую. Задействуется идентификатор, а сами данные хранятся в `jQuery.cache`. Утечек в IE не будет.
|
||||
|
||||
К тому же все данные известны библиотеке, так что можно клонировать с ними и т.п.
|
||||
|
||||
Как побочный эффект -- возникает утечка памяти, если элемент удален из DOM без дополнительной очистки.
|
||||
|
||||
## Примеры утечек в jQuery
|
||||
|
||||
Следующая функция `leak` создает jQuery-утечку во всех браузерах:
|
||||
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
|
||||
<div id="data"></div>
|
||||
|
||||
<script>
|
||||
function leak() {
|
||||
|
||||
*!*
|
||||
$('<div/>')
|
||||
.html(new Array(1000).join('text'))
|
||||
.click(function() {})
|
||||
.appendTo('#data');
|
||||
|
||||
document.getElementById('data').innerHTML = '';
|
||||
*/!*
|
||||
|
||||
}
|
||||
|
||||
var interval = setInterval(leak, 10)
|
||||
</script>
|
||||
|
||||
Утечка идёт...
|
||||
|
||||
<input type="button" onclick="clearInterval(interval)" value="stop" />
|
||||
```
|
||||
|
||||
|
||||
Утечка происходит потому, что обработчик события в jQuery хранится в данных элемента. В строке `(*)` элемент удален очисткой родительского `innerHTML`, но в `jQuery.cache` данные остались.
|
||||
|
||||
Более того, система обработки событий в jQuery устроена так, что вместе с обработчиком в данных хранится и ссылка на элемент, так что в итоге оба -- и обработчик и элемент -- остаются в памяти вместе со всем замыканием!
|
||||
|
||||
Ещё более простой пример утечки:
|
||||
|
||||
Этот код также создает утечку:
|
||||
|
||||
```js
|
||||
function leak() {
|
||||
$('<div/>')
|
||||
.click(function() {})
|
||||
}
|
||||
```
|
||||
|
||||
...То есть, мы создаём элемент, вешаем на него обработчик... И всё.
|
||||
|
||||
Такой код ведёт к утечке памяти как раз потому, что элемент `<div>` создан, но нигде не размещен :). После выполнения функции ссылка на него теряется. Но обработчик события `click` уже сохранил данные в `jQuery.cache`, которые застревают там навсегда.
|
||||
|
||||
## Используем jQuery без утечек
|
||||
|
||||
Чтобы избежать утечек, описанных выше, для удаления элементов используйте функции jQuery API, а не чистый JavaScript.
|
||||
|
||||
Методы <a href="http://api.jquery.com/remove/">remove()</a>, <a href="http://api.jquery.com/empty">empty()</a> и <a href="http://api.jquery.com/html">html()</a> проверяют дочерние элементы на наличие данных и очищают их. Это несколько замедляет процедуру удаления, но зато освобождается память.
|
||||
|
||||
К счастью обнаружить такие утечки легко. Проверьте размер `$.cache`. Если он большой и растет, то изучите кэш, посмотрите, какие записи остаются и почему.
|
||||
|
||||
## Улучшение производительности jQuery
|
||||
|
||||
У способа организации внутренних данных, применённого в jQuery, есть важный побочный эффект.
|
||||
|
||||
Функции, удаляющие элементы, также должны удалить и связанные с ними внутренние данные. Для этого нужно для каждого удаляемого элемента проверить -- а нет ли чего во внутреннем хранилище? И, если есть -- удалить.
|
||||
|
||||
Представим, что у нас есть большая таблица `<table>`, и мы хотим обновить её содержимое на новое. Вызов `$('table').html(новые данные)` перед вставкой новых данных аккуратно удалит старые: пробежит по всем ячейкам и проверит внутреннее хранилище.
|
||||
|
||||
Если это большая таблица, то обработчики, скорее всего, стоят не на ячейках, а на самом элементе `<table>`, то есть используется делегирование. А, значит, тратить время на проверку всех подэлементов ни к чему.
|
||||
|
||||
Но jQuery-то об этом не знает!
|
||||
|
||||
Чтобы "грязно" удалить элемент, без чистки, мы можем сделать это через "обычные" DOM-вызовы или воспользоваться методом <a href="http://api.jquery.com/detach">detach()</a>. Его официальное назначение -- в том, чтобы убрать элемент из DOM, но сохранить возможность для вставки (и, соответственно, оставить на нём все данные). А неофициальное -- быстро убрать элемент из DOM, без чистки.
|
||||
|
||||
Возможен и промежуточный вариант: никто не мешает сделать `elem.detach()` и поместить вызов `elem.remove()` в `setTimeout`. В результате очистка будет происходить асинхронно и незаметно.
|
||||
|
||||
## Итого
|
||||
|
||||
<ul>
|
||||
<li>Утечки памяти при использовании jQuery возможны, если через DOM-методы удалять элементы, к которым привязаны данные или обработчики.</li>
|
||||
<li>Чтобы утечки не было, достаточно убедиться, что элемент удаляется при помощи методов jQuery.</li>
|
||||
<li>Побочный эффект -- при удалении элементов jQuery должна проверить наличие данных для них. Это сильно замедляет процесс удаления большого поддерева DOM.</li>
|
||||
<li>Если мы значем, что обработчиков и данных нет -- гораздо быстрее удалять элементы при помощи вызова `detach` или обычного DOM.</li>
|
||||
</ul>
|
||||
|
||||
|
147
6-optimize/6-memory-removechild-innerhtml/article.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Очистка памяти при removeChild/innerHTML
|
||||
|
||||
Управление памятью в случае с DOM работает по сути так же, как и с обычными JavaScript-объектами. Пока объект достижим -- он остаётся в памяти.
|
||||
|
||||
Но есть и особенности, поскольку DOM весь переплетён ссылками.
|
||||
[cut]
|
||||
## Пример
|
||||
Для примера рассмотрим следующий HTML:
|
||||
|
||||
```html
|
||||
<html>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<ul>
|
||||
<li>Список</li>
|
||||
</ul>
|
||||
Сосед
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
||||
|
||||
Его DOM (показаны только основные ссылки):
|
||||
|
||||
<img src="html.png">
|
||||
|
||||
## Удаление removeChild
|
||||
|
||||
Операция `removeChild` разрывает все связи удаляемым узлом и его родителем.
|
||||
|
||||
Поэтому, если удалить `DIV` из `BODY`, то всё поддерево под `DIV` станет недостижимым и будет удалено.
|
||||
|
||||
А что происходит, если на какой-то элемент внутри удаляемого поддерева есть ссылка?
|
||||
|
||||
Например, `UL` сохранён в переменную `list`:
|
||||
|
||||
```js
|
||||
var list = document.getElementsByTagName('UL')[0];
|
||||
document.body.removeChild(document.body.children[0]);
|
||||
```
|
||||
|
||||
В этом случае, так как из этого `UL` можно по ссылкам добраться до любого другого места DOM, то получается, что все объекты по-прежнему достижимы и должны остаться в памяти:
|
||||
|
||||
<img src="html-list.png">
|
||||
|
||||
То есть, 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
|
||||
<!--+ run -->
|
||||
<div>
|
||||
<ul>
|
||||
<li>Список</li>
|
||||
</ul>
|
||||
Сосед
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var list = document.getElementsByTagName('ul')[0];
|
||||
document.body.innerHTML = ''; // удалили DIV
|
||||
|
||||
alert( list.parentNode ); // цела ли ссылка UL -> DIV ?
|
||||
alert( list.nextSibling ); // живы ли соседи UL ?
|
||||
alert( list.children.length ); // живы ли потомки UL ?
|
||||
</script>
|
||||
```
|
||||
|
||||
Как ни странно, браузеры ведут себя по-разному:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th></th>
|
||||
<th>`parentNode`</th>
|
||||
<th>`nextSibling`</th>
|
||||
<th>`children.length`</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Chrome/Safari/Opera</td>
|
||||
<td>`null`</td>
|
||||
<td>`null`</td>
|
||||
<td>`1`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Firefox</td>
|
||||
<td>узел DOM</td>
|
||||
<td>узел DOM</td>
|
||||
<td>`1`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IE 11-</td>
|
||||
<td>`null`</td>
|
||||
<td>`null`</td>
|
||||
<td>`0`</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Иными словами, браузеры ведут себя с различной степенью агрессивности по отношению к элементам.
|
||||
|
||||
<dl>
|
||||
<dt>Firefox</dt>
|
||||
<dd>Главный пацифист. Оставляет всё, на что есть ссылки, т.е. элемент, его родителя, соседей и детей, в точности как при `removeChild`.</dd>
|
||||
<dt>Chrome/Safari/Opera</dt>
|
||||
<dd>Считают, что раз мы задали ссылку на `UL`, то нам нужно только это поддерево, а остальные узлы (соседей, родителей) можно удалить.</dd>
|
||||
<dt>Internet Explorer</dt>
|
||||
<dd>Как ни странно, самый агрессивный. Удаляет вообще всё, кроме узла, на который есть ссылка. Это поведение одинаково для всех версий IE.</dd>
|
||||
</dl>
|
||||
|
||||
На иллюстрации ниже показано, какую часть DOM оставит каждый из браузеров:
|
||||
<img src="html-innerhtml.png">
|
||||
|
||||
## Итого
|
||||
|
||||
Если на какой-то DOM-узел есть ссылка, то:
|
||||
|
||||
<ul>
|
||||
<li>При использовании `removeChild` на родителе (или на этом узле, не важно) все узлы, достижимые из данного, остаются в памяти.
|
||||
|
||||
То есть, фактически, в памяти может остаться большая часть дерева DOM. Это даёт наибольшую свободу в коде, но может привести к большим "утечкам памяти" из-за сохранения данных, которые реально не нужны.</li>
|
||||
<li>При удалении через `innerHTML` браузеры ведут себя с различной степенью агрессивности. Кросс-браузерно гарантировано одно: сам узел, на который есть ссылка, останется в памяти.
|
||||
|
||||
Поэтому обращаться к соседям и детям узла, предок которого удалён через присвоение `innerHTML`, нельзя.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
BIN
6-optimize/6-memory-removechild-innerhtml/html-innerhtml.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
6-optimize/6-memory-removechild-innerhtml/html-list.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
6-optimize/6-memory-removechild-innerhtml/html.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
530
6-optimize/7-gcc-advanced-optimization/article.md
Normal file
|
@ -0,0 +1,530 @@
|
|||
# GCC: продвинутые оптимизации
|
||||
|
||||
Продвинутый режим оптимизации google closure compiler включается опцией <code>--compilation_level ADVANCED_OPTIMIZATIONS.</code>
|
||||
|
||||
Слово "продвинутый" (advanced) здесь, пожалуй, не совсем подходит. Было бы более правильно назвать его "супер-агрессивный-ломающий-ваш-неподготовленный-код-режим". Кардинальное отличие применяемых оптимизаций от обычных (simple) -- в том, что они небезопасны.
|
||||
|
||||
Чтобы им пользоваться -- надо уметь это делать.
|
||||
[cut]
|
||||
|
||||
## Основной принцип продвинутого режима
|
||||
|
||||
<ul>
|
||||
<li>Если в обычном режиме переименовываются только локальные переменные внутри функций, то в "продвинутом" -- на более короткие имена заменяется все.</li>
|
||||
<li>Если в обычном режиме удаляется недостижимый код после <code>return</code>, то в продвинутом -- вообще весь код, который не вызывается в явном виде.</li>
|
||||
</ul>
|
||||
|
||||
Например, если запустить продвинутую оптимизацию на таком коде:
|
||||
|
||||
```js
|
||||
// my.js
|
||||
function test(node) {
|
||||
node.innerHTML = "newValue"
|
||||
}
|
||||
```
|
||||
|
||||
Строка запуска компилятора:
|
||||
|
||||
```
|
||||
java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js my.js
|
||||
```
|
||||
|
||||
...То результат будет -- пустой файл. Google Closure Compiler увидит, что функция <code>test</code> не используется, и с чистой совестью вырежет ее.
|
||||
|
||||
А в следующем скрипте функция сохранится:
|
||||
|
||||
```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)
|
||||
}
|
||||
```
|
||||
|
||||
Теперь компилятор переименовал и <code>blabla</code> и <code>megaProperty</code>.
|
||||
|
||||
Дело в том, что названия, использованные до этого, были во внутреннем списке экстернов компилятора. Этот список охватывает основные объекты браузеров и находится (под именем <code>externs.zip</code>) в корне архива <code>compiler.jar</code>.
|
||||
|
||||
**Компилятор переименовывает имя списка экстернов только когда так названа локальная переменная.**
|
||||
|
||||
Например:
|
||||
|
||||
```js
|
||||
window.resetNode = function(node) {
|
||||
var innerHTML = "test";
|
||||
node.innerHTML = innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
На выходе:
|
||||
|
||||
```js
|
||||
window.a = function(a) {
|
||||
a.innerHTML = "test"
|
||||
};
|
||||
```
|
||||
|
||||
Как видите, внутренняя переменная <code>innerHTML</code> не просто переименована - она заинлайнена (заменена на значение). Так как переменная локальна, то любые действия внутри функции с ней безопасны.
|
||||
|
||||
А свойство <code>innerHTML</code> не тронуто, как и объект <code>window</code> -- так как они в списке экстернов и не являются локальными переменными.
|
||||
|
||||
Это приводит к следующему побочному эффекту. Иногда свойства, которые следовало бы сжать, не сжимаются. Например:
|
||||
|
||||
```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
|
||||
};
|
||||
```
|
||||
|
||||
Как видно, свойство <code>age</code> сжалось, а <code>name</code> и <code>type</code> -- нет. Это побочный эффект экстернов: <code>name</code> и <code>type</code> -- в списке объектов браузера, и компилятор просто старается не наломать дров.
|
||||
|
||||
Поэтому отметим еще одно полезное правило оптимизации:
|
||||
|
||||
**Названия своих свойств не должны совпадать с зарезервированными словами (экстернами). Тогда они будут хорошо сжиматься.**
|
||||
|
||||
Для задания списка экстернов их достаточно перечислить в файле и указать этот файл флагом <code>--externs <файл экстернов.js></code>.
|
||||
|
||||
При перечислении объектов в файле экстернов - объявляйте их и перечисляйте свойства. Все эти объявления никуда не идут, они используются только для создания списка, который обрабатывается компилятором.
|
||||
|
||||
Например, файл `myexterns.js`:
|
||||
|
||||
```js
|
||||
var dojo = {}
|
||||
dojo._scopeMap;
|
||||
```
|
||||
|
||||
Использование такого файла при сжатии (опция <code>--externs myexterns.js</code>) приведет к тому, что все обращения к символам <code>dojo</code> и к <code>dojo._scopeMap</code> будут не сжаты, а оставлены "как есть".
|
||||
|
||||
|
||||
### Экспорт
|
||||
|
||||
*Экспорт* -- программный ход, основанный на следующем правиле поведения компилятора.
|
||||
|
||||
**Компилятор заменяет обращения к свойствам через кавычки на точку, и при этом не трогает название свойства.**
|
||||
|
||||
Например, <code>window['User']</code> превратится в <code>window.User</code>, но не дальше.
|
||||
|
||||
Таким образом можно *"экспортировать"* нужные функции и объекты:
|
||||
|
||||
```js
|
||||
function SayWidget(elem) {
|
||||
this.elem = elem
|
||||
this.init()
|
||||
}
|
||||
window['SayWidget'] = SayWidget;
|
||||
```
|
||||
|
||||
На выходе:
|
||||
|
||||
```js
|
||||
function a(b) {
|
||||
this.a = b;
|
||||
this.b()
|
||||
}
|
||||
window.SayWidget = a;
|
||||
```
|
||||
|
||||
Обратим внимание -- сама функция <code>SayWidget</code> была переименована в <code>a</code>. Но затем -- экспортирована как <code>window.SayWidget</code>, и таким образом доступна внешним скриптам.
|
||||
|
||||
Добавим пару методов в прототип:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
метод <code>setSayHandler</code> экспортирован и доступен для внешнего вызова.
|
||||
|
||||
Сама строка экспорта выглядит довольно глупо. По виду -- присваиваем свойство самому себе.
|
||||
|
||||
Но логика сжатия GCC работает так, что такая конструкция является экспортом. Справа переименование свойства <code>setSayHandler</code> происходит, а слева -- нет.
|
||||
|
||||
[smart header="Планируйте жизнь после сжатия"]
|
||||
|
||||
Рассмотрим следующий код:
|
||||
|
||||
```js
|
||||
window['Animal'] = function() {
|
||||
this.blabla = 1;
|
||||
this['blabla'] = 2;
|
||||
}
|
||||
```
|
||||
|
||||
После сжатия:
|
||||
|
||||
```js
|
||||
window.Animal = function() {
|
||||
this.a = 1;
|
||||
this.blabla = 2
|
||||
};
|
||||
```
|
||||
|
||||
Как видно, первое обращение к свойству <code>blabla</code> сжалось, а второе (как и все аналогичные) -- преобразовалось в синтаксис через точку.
|
||||
В результате получили некорректное поведение кода.
|
||||
|
||||
Так что, используя продвинутый режим оптимизации, планируйте поведение кода после сжатия.
|
||||
|
||||
**Если где-то возможно обращение к свойствам через квадратные скобки по полному имени -- такое свойство должно быть экспортировано.**
|
||||
[/smart]
|
||||
|
||||
### goog.exportSymbol и goog.exportProperty
|
||||
|
||||
В библиотеке [Google Closure Library](https://developers.google.com/closure/library/) для экспорта есть специальная функция <code>goog.exportSymbol</code>. Вызывается так:
|
||||
|
||||
```js
|
||||
goog.exportSymbol('my.SayWidget', SayWidget)
|
||||
```
|
||||
|
||||
Эта функция по сути работает также, как и рассмотренная выше строка с присвоением свойства, но при необходимости создает нужные объекты.
|
||||
|
||||
Она аналогична коду:
|
||||
|
||||
```js
|
||||
window['my'] = window['my'] || {}
|
||||
window['my']['SayWidget'] = SayWidget
|
||||
```
|
||||
|
||||
То есть, если путь к объекту не существует -- <code>exportSymbol</code> создаст нужные пустые объекты.
|
||||
|
||||
Функция <code>goog.exportProperty</code> экспортирует свойство объекта:
|
||||
|
||||
```js
|
||||
goog.exportProperty(SayWidget.prototype, 'setSayHandler', SayWidget.prototype.setSayHandler)
|
||||
```
|
||||
|
||||
Строка выше - то же самое, что и:
|
||||
|
||||
```js
|
||||
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
|
||||
```
|
||||
|
||||
Зачем они нужны, если все можно сделать простым присваиванием?
|
||||
|
||||
Основная цель этих функций -- во взаимодействии с Google Closure Compiler. Они дают информацию компилятору об экспортах, которую он может использовать.
|
||||
|
||||
Например, есть недокументированная внутренняя опция <code>externExportsPath</code>, которая генерирует из всех экспортов файл экстернов. Таким образом можно распространять откомпилированный JavaScript-файл как внешнюю библиотеку, с файлом экстернов для удобного внешнего связывания.
|
||||
|
||||
Кроме того, экспорт через эти функциями удобен и нагляден.
|
||||
|
||||
Если вы используете продвинутый режим оптимизации, то можно взять их из файла base.js Google Closure Library. Можно и подключить этот файл целиком -- оптимизатор при продвинутом сжатии вырежет из него почти всё лишнее, так что overhead будет минимальным.
|
||||
|
||||
### Отличия экспорта от экстерна
|
||||
|
||||
Между экспортом и экстерном есть кое-что общее. И то и другое дает возможность доступа к объектам под исходным именем, до переименования.
|
||||
|
||||
Но, в остальном, это совершенно разные вещи.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Экстерн</th>
|
||||
<th>Экспорт</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Служит для тотального запрета на переименование всех обращений к свойству.
|
||||
Задумано для сохранения обращений к стандартным объектам браузера, внешним библиотекам.</td>
|
||||
<td>Служит для открытия доступа к свойству извне под указанным именем.
|
||||
Задумано для открытия внешнего интерфейса к сжатому скрипту.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Работает со свойством, объявленным вне скрипта.
|
||||
Вы не можете объявить новое свойство в скрипте и сделать его экстерном.</td>
|
||||
<td>Создает ссылку на свойство, объявленное в скрипте.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Если <code>window</code> - экстерн, то все обращения к <code>window</code> в скрипте останутся как есть.</td>
|
||||
<td>Если <code>user</code> экспортируется, то создается только одна ссылка под полным именем, а все остальные обращения будут сокращены.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
## Стиль разработки
|
||||
|
||||
Посмотрим, как сжиматель поведёт себя на следующем, типичном, объявлении библиотеки:
|
||||
|
||||
```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/), но клиентская разработка и без того достаточно сложна.
|
||||
|
||||
Поэтому его используют редко.
|
||||
|
||||
Как правило, есть две причины для использования продвинутого режима:
|
||||
<ol>
|
||||
<li>**Обфускация кода.**
|
||||
|
||||
Если в коде после обычного сжатия ещё как-то можно разобраться, то после продвинутого -- уже нет. Всё переименовано и заинлайнено. В теории это, конечно, возможно, но "порог входа" в такой код несоизмеримо выше.
|
||||
|
||||
Судя по виду скриптов на сайтах, созданных Google, сам Google жмет свои скрипты именно продвинутым режимом оптимизации. И библиотека Google Closure Library тоже рассчитана на него.</li>
|
||||
<li>**Хорошие сжатие виджетов, счётчиков.**
|
||||
|
||||
Небольшой код, который отдаётся наружу, может быть сжат в продвинутом режиме. Так как он небольшой -- все ошибки можно легко исправить, а продвинутый режим гарантирует наилучшее сжатие.</li>
|
||||
</ol>
|
177
6-optimize/8-gcc-check-types/article.md
Normal file
|
@ -0,0 +1,177 @@
|
|||
# GCC: статическая проверка типов
|
||||
|
||||
Google Closure Compiler, как и любой кошерный компилятор, старается проверить правильность кода и предупредить о возможных ошибках.
|
||||
|
||||
Первым делом он, разумеется, проверяет структуру кода и сразу же выдает такие ошибки как пропущенная скобка или лишняя запятая.
|
||||
|
||||
Но, кроме этого, он умеет проверять типы переменных, используя как свои собственные знания о встроенных javascript-функциях и преобразованиях типов,
|
||||
так и информацию о типах из JSDoc, указываемую javascript-разработчиком.
|
||||
|
||||
Это обеспечивает то, чем так гордятся компилируемые языки -- статическую проверку типов, что позволяет избежать лишних ошибок во время выполнения.
|
||||
[cut]
|
||||
|
||||
Для вывода предупреждений при проверки типов используется флаг `--jscomp_warning checkTypes`.
|
||||
|
||||
## Задание типа при помощи аннотации
|
||||
|
||||
Самый очевидный способ задать тип -- это использовать аннотацию. Полный список аннотаций вы найдете в <a href="http://code.google.com/intl/ru/closure/compiler/docs/js-for-compiler.html">документации</a>.
|
||||
|
||||
В следующем примере параметр <code>id</code> функции <code>f1</code> присваивается переменной <code>boolVar</code> другого типа:
|
||||
|
||||
```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; // (!)
|
||||
^
|
||||
```
|
||||
|
||||
Действительно: произошло присвоение значения типа <code>number</code> переменной типа <code>boolean</code>.
|
||||
|
||||
Типы отслеживаются по цепочке вызовов.
|
||||
|
||||
Еще пример, на этот раз вызов функции с некорректным параметром:
|
||||
|
||||
```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); // (!)
|
||||
^
|
||||
```
|
||||
|
||||
Действительно, вызов функции <code>f2</code> произошел с числовым типом вместо строки.
|
||||
|
||||
**Отслеживание приведений и типов идёт при помощи графа взаимодействий и выведению (infer) типов, который строит GCC по коду.**
|
||||
|
||||
## Знания о преобразовании типов
|
||||
|
||||
Google Closure Compiler знает, как операторы javascript преобразуют типы. Такой код уже не выдаст ошибку:
|
||||
|
||||
```js
|
||||
/** @param {number} id */
|
||||
function f1(id) {
|
||||
/** @type {boolean} */
|
||||
var boolVar;
|
||||
|
||||
boolVar = !!id
|
||||
}
|
||||
```
|
||||
|
||||
Действительно - переменная преобразована к типу boolean двойным оператором НЕ.
|
||||
А код <code>boolVar = 'test-'+id</code> выдаст ошибку, т.к. конкатенация со строкой дает тип <code>string</code>.
|
||||
|
||||
## Знание о типах встроенных функций, объектные типы
|
||||
|
||||
Google Closure Compiler содержит описания большинства встроенных объектов и функций javascript вместе с типами параметров и результатов.
|
||||
|
||||
Например, объектный тип <code>Node</code> соответствует узлу 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")
|
||||
^
|
||||
```
|
||||
|
||||
Обратите внимание - в этом примере компилятор выдает <code>required: Node|null</code>. Это потому, что указание объектного типа (не элементарного) подразумевает, что в функцию может быть передан <code>null</code>.
|
||||
|
||||
В следующем примере тип указан жестко, без возможности обнуления:
|
||||
|
||||
```js
|
||||
*!*
|
||||
/** @param {!Node} node */
|
||||
*/!*
|
||||
function removeNode(node) {
|
||||
node.parentNode.removeChild(node)
|
||||
}
|
||||
```
|
||||
|
||||
Восклицательный знак означает, что параметр обязатален.
|
||||
|
||||
Найти описания встроенных типов и объектов javascript вы можете в файле экстернов: <code>externs.zip</code> находится в корне архива <code>compiler.jar</code>.
|
||||
|
||||
## Интеграция с проверками типов из Google Closure Library
|
||||
|
||||
В Google Closure Library есть функции проверки типов: <code>goog.isArray</code>, <code>goog.isDef</code>, <code>goog.isNumber</code> и т.п.
|
||||
|
||||
Google Closure Compiler знает о них и понимает, что внутри следующего <code>if</code> переменная может быть только функцией:
|
||||
|
||||
```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)
|
||||
^ ^
|
||||
```
|
||||
|
||||
То есть, компилятор увидел, что код, использующий <code>func</code> находится в `if (goog.isFunction(func))` и сделал соответствующий вывод, что это в этой ветке `func` является функцией, а значит вызов `func.apply(1,2)` ошибочен (второй аргумент не может быть числом).
|
||||
|
||||
Дело тут именно в интеграции с Google Closure Library. Если поменять `goog` на `g` -- предупреждения не будет.
|
||||
|
||||
## Резюме
|
||||
|
||||
Из нескольких примеров, которые мы рассмотрели, должна быть понятна общая логика проверки типов.
|
||||
|
||||
Соответствующие различным типам и ограничениям на типы аннотации вы можете найти в <a href="http://code.google.com/intl/ru/closure/compiler/docs/js-for-compiler.html">Документации Google</a>. В частности, возможно указание нескольких возможных типов, типа <code>undefined</code> и т.п.
|
||||
|
||||
Также можно указывать количество и тип параметров функции, ключевого слова <code>this</code>, объявлять классы, приватные методы и интерфейсы.
|
||||
|
||||
Проверка типов javascript, предоставляемая Google Closure Compiler -- пожалуй, самая продвинутая из существующих на сегодняшний день.
|
||||
|
||||
C ней аннотации, документирующие типы и параметры, становятся не просто украшением, а реальным средством проверки, уменьшающим количество ошибок на production.
|
||||
|
||||
Очень подробно проверка типов описана в книге [Closure: The Definitive Guide](http://www.ozon.ru/context/detail/id/6089988/), автора Michael Bolin.
|
180
6-optimize/9-gcc-closure-library/article.md
Normal file
|
@ -0,0 +1,180 @@
|
|||
# GCC: интеграция с Google Closure Library
|
||||
|
||||
Google Closure Compiler содержит ряд специальных возможностей для интеграции с Google Closure Library.
|
||||
|
||||
Здесь важны две вещи.
|
||||
<ol>
|
||||
<li>Для их использования возможно использовать минимум от Google Closure Library. Например, взять одну или несколько функций из библиотеки.</li>
|
||||
<li>GCC -- расширяемый компилятор, можно добавить к нему свои "фазы оптимизации" для интеграции с другими инструментами и фреймворками.</li>
|
||||
</ol>
|
||||
[cut]
|
||||
|
||||
Интеграция с Google Closure Library подключается флагом <code>--process_closure_primitives</code>, который по умолчанию установлен в <code>true</code>. То есть, она включена по умолчанию.
|
||||
|
||||
Этот флаг запускает специальный проход компилятора, описанный классом <code>ProcessClosurePrimitives</code> и подключает дополнительную проверку типов <code>ClosureReverseAbstractInterpreter</code>.
|
||||
|
||||
Мы рассмотрим все действия, которые при этом происходят, а также некоторые опции, которые безопасным образом используют символы Google Closure Library без объявления флага.
|
||||
|
||||
## Преобразование основных символов
|
||||
|
||||
Следующие действия описаны в классе <code>ProcessClosurePrimitives</code>.
|
||||
|
||||
### Замена константы <code>COMPILED</code>
|
||||
|
||||
В Google Closure Library есть переменная:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @define {boolean} ...
|
||||
*/
|
||||
var COMPILED = false;
|
||||
```
|
||||
|
||||
Проход <code>ProcessClosurePrimitives</code> переопределяет ее в <code>true</code> и использует это при оптимизациях, удаляя ветки кода, не предназначены для запуска на production.
|
||||
|
||||
Такие функции существуют, например, в ядре Google Closure Library. К ним в первую очередь относятся вызовы, предназначенные для сборки и проверки зависимостей. Они содержат код, обрамленный проверкой <code>COMPILED</code>, например:
|
||||
|
||||
```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" );
|
||||
};
|
||||
```
|
||||
|
||||
Компилятор переопределил <code>COMPILED</code> в <code>true</code> и произвел соответствующие оптимизации.
|
||||
|
||||
### Автоподстановка локали
|
||||
|
||||
В Google Closure Compiler есть внутренняя опция <code>locale</code>
|
||||
|
||||
Эта опция переопределяет переменную <code>goog.LOCALE</code> на установленную при компиляции.
|
||||
|
||||
Для использования опции <code>locale</code>, на момент написания статьи, ее нужно задать в Java коде компилятора, т.к. соответствующего флага нет.
|
||||
|
||||
Как и <code>COMPILED</code>, константу <code>goog.LOCALE</code> можно и использовать в своем коде без библиотеки Google Closure Library.
|
||||
|
||||
### Проверка зависимостей
|
||||
|
||||
Директивы <code>goog.provide</code>, <code>goog.require</code>, <code>goog.addDependency</code> обрабатываются особым образом.
|
||||
|
||||
Все зависимости проверяются, а сами директивы проверки -- удаляются из сжатого файла.
|
||||
|
||||
### Экспорт символов
|
||||
|
||||
Вызов <code>goog.exportSymbol</code> задаёт экспорт символа.
|
||||
|
||||
Если подробнее, то код <code>goog.exportSymbol('a',myVar)</code> эквивалентен
|
||||
`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 производит соответствующие преобразования в сжатом файле и удаляет вызов <code>setCssNameMapping</code> из кода.
|
||||
|
||||
Чтобы это сжатие работало, в HTML/CSS классы тоже должны сжиматься. По всей видимости, в приложениях Google это и происходит, но соответствующие инструменты закрыты от публики.
|
||||
|
||||
### Генерация списка экстернов
|
||||
|
||||
При объявлении внутренней опции <code>externExportsPath</code>, содержащей путь к файлу, в этот файл будут записаны все экспорты, описанные через <code>goog.exportSymbol</code>/<code>goog.exportProperty</code>.
|
||||
|
||||
В дальнейшем этот файл может быть использован как список экстернов для компиляции.
|
||||
|
||||
Эта опция может быть полезна для создания внешних библиотек, распространяемых со списком экстернов.
|
||||
|
||||
Для её использования нужна своя обёртка вокруг компилятора на Java. Соответствующий проход компилятора описан в классе <code>ExternExportsPass</code>.
|
||||
|
||||
### Проверка типов
|
||||
|
||||
В Google Closure Library есть ряд функций для проверки типов. Например: <code>goog.isArray</code>, <code>goog.isString</code>, <code>goog.isNumber</code>, <code>goog.isDef</code> и т.п.
|
||||
|
||||
Компилятор использует их для проверки типов, более подробно см. [](/gcc-check-types)
|
||||
|
||||
Эта логика описана в классе <code>ClosureReverseAbstractInterpreter</code>. Названия функций, определяющих типы, жестко прописаны в Java-коде, поменять их на свои без модификации исходников нельзя.
|
||||
|
||||
### Автогенерация экспортов из аннотаций
|
||||
|
||||
Для этого в Google Closure Compiler есть внутренняя опция <code>generateExports</code>.
|
||||
|
||||
Эта недокументированная опция добавляет проход компилятора, описанный классом <code>GenerateExports</code>.
|
||||
|
||||
Он читает аннотации <code>@export</code> и создает из них экспортирующие вызовы <code>goog.exportSymbol/exportProperty</code>. Название экспортирующих функций находится в классе соглашений кодирования, каким по умолчанию является <code>GoogleCodingConvention</code>.
|
||||
|
||||
Например:
|
||||
|
||||
```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 можно легко расширить, добавив свои опции и проходы оптимизатора, для интеграции с вашими инструментами.
|
||||
|