Статьи

JavaScript для разработчиков на C #: обратные вызовы (часть II)

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

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

Array.prototype.mapp = function (process, context) {
var i,
result = [],
last = this.length;
for (i = 0; i < last; i++) {
if (this[i]) {
result[i] = process.call(context, this[i], i);
}
}
return result;
};

var myArray = [2, 3, 5, 7, 11, 13];
myArray[7] = 19; // missing out myArray[6]
console.log(myArray);

var newArray = myArray.mapp(function (element, index) {
return "<" + index.toString() + ": " + element.toString() + ">";
});
console.log(newArray);

 

Поскольку в седьмом элементе отсутствует выражение, this [6] преобразуется в false и блок if не будет выполнен. К сожалению, существует пять других значений, которые может иметь существующий элемент , которые также будут оцениваться как false: false, 0, NaN, пустая строка и null, все из которых могут появиться в реальном массиве. Так что этот первый срез просто не сработает.

Другой способ — явно сравнить с неопределенным, например так:

 if (this[i] !== undefined) {
result[i] = process.call(context, this[i], i);
}

Это работает с обычным предупреждением, что кто-то может установить неопределенное на какое-то фактическое значение. В этом случае мы могли бы использовать (void 0) вместо этого (оператор void возвращает undefined для любого операнда), или мы могли бы ввести (typeof this [i]! == «undefined») в качестве альтернативы.

Юк, как насчет правильной идиомы JavaScript? Мы будем использовать ключевое слово in, это не только для оператора for..in:

    if (i in this) {
result[i] = process.call(context, this[i], i);
}

Там. Гораздо более разборчиво

Теперь мы разобрались, предположим, что отображаемый массив большой или обработка каждого элемента массива длительная. Мы могли бы рискнуть вызвать предупреждение браузера «скрипт занимает слишком много времени», если бы мы просто беспечно использовали метод mapp, несмотря ни на что. Что нам делать?

Доминофото © 2005 Джейсон | больше информации (через: Wylio )Давайте рассмотрим, как можно разделить работу, выполняемую методом mapp. Во-первых, нам следует понять, почему браузер может открыть диалоговое окно с предупреждением о том, что выполнение сценария занимает слишком много времени. По сути, весь код JavaScript на странице выполняется в одном потоке. На самом деле JavaScript не имеет возможности раскручивать другие потоки для выполнения работы. Поскольку один-единственный поток также является потоком пользовательского интерфейса, это означает, что длительный фрагмент кода замораживает весь пользовательский интерфейс страницы. Совсем не очень хороший опыт, поэтому браузеры имеют монитор для проверки того, что события все еще проходят через насос сообщений. Если фрагмент кода занимает слишком много времени, события больше не обрабатываются, и по истечении заданного промежутка времени браузер прерывает интерпретатор и выводит диалоговое окно с предупреждением.

Итак, учитывая все это, как мы можем разделить наш код так, чтобы насос сообщений все еще получал время обработки? Ответ заключается в использовании setTimeout. Эта функция устанавливает функцию, выполняемую по истечении определенного промежутка времени. Вы передаете и функцию для выполнения (это, конечно, обратный вызов) и время ожидания для setTimeout. Что происходит под капотом, так это то, что эти запросы помещаются в очередь, и некоторый процесс помещает обратный вызов в цикл обработки сообщений после истечения времени ожидания. В какой момент, конечно, функция выполняется. (То же самое происходит с вызовами AJAX: когда возвращается вызов AJAX, он переводит обратный вызов в цикл сообщений для выполнения.)

Мы хотели бы разделить нашу обработку на «куски», каждый из которых не займет много времени для выполнения. После завершения каждого блока он ставит в очередь следующий блок для выполнения с помощью setTimeout. Однако используемый тайм-аут будет очень коротким; не большое время, как секунда, но порядка нескольких миллисекунд самое большее. По сути, мы выполняем фрагменты асинхронно, а не последовательно или синхронно. Между ними есть небольшая передышка для цикла сообщений, чтобы делать другие вещи.

Вот новая функция, которая поможет нам.

Function.prototype.delay = function () {
setTimeout(this, 10);
};

Он определен как метод в прототипе функции, поэтому он доступен для всех функций. Все, что он делает — это задерживает выполнение вызываемой функции на 10 миллисекунд. Вот глупый пример этого в действии:

var o = {
count: 10,
tick: function () {
if (o.count--) {
console.log("*");
o.tick.delay();
}
}
};

o.tick();

 

Здесь мы имеем объект со свойством count, изначально установленным в 10, и метод, называемый tick. Этот метод выводит звездочку на консоль, а затем вызывает сам себя, используя метод задержки, который мы только что написали, с задержкой на 10 миллисекунд. Он делает это 10 раз, уменьшая счет до нуля. Если вы запустите это, вы очень быстро получите 10 звездочек одна за другой. Ницца.

Обратите внимание, что первоначальный вызов o.tick возвращается немедленно. Если вы запустите его в Firebug, вы получите такой результат:

*
undefined
*
*
*
*
*
*
*
*
*

Что там делает «неопределенный»? Это Firebug говорит нам, что первоначальный вызов o.tick завершен, и он вернулся неопределенным. Затем мы получаем звездочки из setTimeout, затем setTimeout, затем setTimeout и т. Д., И т. Д., Каждая из которых запускает другую. Суть, которую я хочу донести, заключается в том, что первоначальный вызов функции завершается до того, как задержка срабатывает. Задержка, подобная этой, означает, что мы должны быть осторожны и не предполагать, что вся работа, которую мы пытаемся сделать, выполнена сразу. Если мы хотим знать, когда завершается связанный асинхронный процесс, подобный этому, мы должны предоставить обратный вызов завершения, чтобы он мог быть запущен, когда все будет сделано.

Вернуться к следующей версии нашей функции mapp. Мы создадим новую функцию карты, назовем ее mapAsync, которая будет обрабатывать массив кусками. Чтобы было проще начать, мы сделаем каждый блок достаточно большим для обработки одного элемента. Прежде всего, нам нужно будет добавить новый параметр: обратный вызов завершения, как я упоминал выше (было бы неплохо узнать, когда функция отобразила весь массив). Затем нам нужно определить внутреннюю функцию, которая будет эквивалентна нашему методу тика, описанному выше, и мы должны убедиться, что он знает, как далеко мы попали в массив.

Введите закрытие. Конечно. Я уверен, что вы ожидали этого. Вот код:

Array.prototype.mapAsync = function (process, done, context) {
var i = 0,
result = [],
last = this.length,
self = this,
processAsync = function () {
if (i in self) {
result[i] = process.call(context, self[i], i);
}
if (++i === last) {
done.call(context, result);
}
else {
processAsync.delay();
}
};
processAsync();
};

 

Давай возьмем это медленно. Прежде всего мы объявляем тех же трех местных жителей, что и раньше. Затем я сохраняю значение этой переменной, то есть массива, с которым работает этот метод. (Мне нравится использовать имя self для этой цели.) Затем я объявил функцию, и именно эта функция будет вызываться через наш механизм задержки. Если вы заметите в конце метода mapAsync, я начну с него, впервые вызвав processAsync ().

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

Итак, давайте посмотрим на эту внутреннюю функцию. Прежде всего, он обрабатывает текущий элемент, определяемый зафиксированным значением i. Да, мы создали замыкание с помощью функции mapAsync, и все ее локальные переменные были захвачены и доступны для внутренней функции processAsync. Теперь мы увеличиваем счетчик и, если мы достигли конца исходного массива, мы можем вызвать обратный вызов done, установить правильный контекст (то есть переменную this для done) и передать полученный, сопоставленный массив. Если мы не достигли конца массива, мы снова вызываем processArray, но немного задерживаем его.

Вот код, который проверит это:

var myArray = [2, 3, 5, 7, 11, 13];
myArray[7] = 19; // missing out myArray[6]
console.log(myArray);

myArray.mapAsync(function (element, index) {
return "<" + index.toString() + ": " + element.toString() + ">";
}, function (a) {
console.log(a);
});

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

[2, 3, 5, 7, 11, 13, undefined, 19]
undefined
["<0: 2>", "<1: 3>", "<2: 5>", "<3: 7>", "<4: 11>", "<5: 13>", undefined, "<7: 19>"]

Снова обратите внимание, что результат вызова mapAsync (то есть «undefined») записывается в журнал до регистрации сопоставленного массива.

Если вы подумаете об этом, этот код mapAsync демонстрирует использование многих обратных вызовов: есть process, done и есть processAsync, который передается как обратный вызов функции setTimeout. Да, обратные вызовы в JavaScript используются везде. Привыкайте к ним и используйте анонимные функции для их определения.

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