На прошлой неделе я был в своем офисе, пытаясь написать простую блокировку в Angular 1.

Почему вы используете Angular 1?

Как всем известно, Angular 1, вероятно, печально известен своей ужасной производительностью при обработке привязки данных и отладке. Должны ли мы переходить на Angular 5 — это отдельная история.

Зачем JS нужны блокировки? Разве JS не однопоточный?

Итак, на прошлой неделе я имел дело с очередью сообщений и хотел убедиться, что очередь может быть скопирована и очищена до того, как в нее будут помещены новые сообщения. Я не хочу, чтобы какие-либо новые сообщения поступали до того, как я очистил очередь. Мой старший сказал мне, что Javascript является однопоточным, поэтому нам не следует об этом беспокоиться. Но я сомневаюсь.

Javascript не является однопоточным. На самом деле он может быть управляемым событиями. Одновременно несколько строк кода могут выполняться вместе (вроде), чтобы реагировать на разные события.

Докажите: установите точку останова в Google Chrome внутри прослушивателя событий нажатия кнопки. Нажмите кнопку на веб-сайте с обновлением в реальном времени (или опросом). Продолжайте нажимать F10. Вместо того, чтобы проверять код JS построчно, вы можете увидеть, как он прыгает то тут, то там. Это потому, что JS может запускаться любым пользовательским событием, и я считаю, что JS может достичь (своего рода) многопоточности за счет разделения времени.

В ПОРЯДКЕ. вернуться к нашим делам. Вот почему нам нужна блокировка JS.

Первая попытка блокировки: убедитесь, что работает только одна функция

Моя первая попытка блокировки хочет только проверить, обновляются ли данные. Если да, это сообщит функции выполнить еще один запуск на основе новых данных.

var threads = {};
var lock = function (id, cb) {
  var release = function () {
    threads[id].locked = false;
    if (threads[id].waiting) {
      threads[id].waiting = false;
      lock(id, cb);
    }
  }
  if (!(id in threads))
    threads[id] = { locked: false, waiting: false };
  if (!threads[id].locked) {
    threads[id].locked = true;
    cb(release);
  } else {
    threads[id].waiting = true;
  }
}

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

Вторая попытка блокировки: сохранить данные с тайм-аутом

Я думаю, это решение, которое вы можете найти во многих репозиториях Github — блокировка с $timeout или, в терминах ванильного JS, setTimeout.

var promiseThreads = {};
var getPromiseThread = function (id) {
  if (!(id in promiseThreads)) promiseThreads[id] = 0;
  return !(promiseThreads[id] || ((promiseThreads[id] = 1) - 1));
}
var lockPromise = function(id) {
  var d = $q.defer();
  $timeout(function() {
    if (getPromiseThread(id)) d.resolve();
    else d.reject();
  }, 500);
  var p = d.promise;
  p.finally(function() {
    promiseThreads[id] = 0;
  });
  return p;
};

Я думаю, мне следует немного объяснить следующее:
!(promiseThreads[id] || ((promiseThreads[id] = 1) - 1))
потому что я хочу запустить следующий фрагмент в одну строку, чтобы этот код JS можно было запускать автоматически.

if (promiseThreads[id] == 1)    // thread is occupied
  return false;
else {   // thread is empty
  promiseThreads[id] = 1;
  return true;
}

Но проблема возникает, когда ваш сайт работает с обновлением в реальном времени. Когда веб-сайт нуждается в частом обновлении, $timeout отнимает много времени и становится узким местом вашего рабочего процесса пользовательского интерфейса. Будет постоянно отображаться предупреждение Chrome: alert: $timeout takes 254 ms, и вы знаете, что это нехороший знак, особенно когда ваш клиент может видеть это в консоли разработчика Google Chrome. Ург.

Третья попытка блокировки: сохранить функцию в массив

Затем мой старший напомнил мне о самом простом и легком способе работы с блокировками — сохранить функцию в ванильном JS-массиве.

var threads = {}
var lock = function(id, cb) {
  if (!(id in threads)) threads[id] = {lock: false, queue: []};
  threads[id].queue.push(cb);
  if (threads[id].lock) return;
  threads[id].lock = true;
  while (threads[id].queue.length > 0)
    threads[id].queue.shift()();
  threads[id].lock = false;
}

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

Не уверен, что это принесет пользу кому-либо. Напишите комментарий, если у вас есть еще отличные идеи.