Обратные вызовы и преобразователи

Хорошо. Итак, вы хотите написать свою собственную библиотеку обещаний. Вы смотрите его и - черт возьми - они советуют вам исследовать thunks. Что такое преобразователь? Аналогия: это звук, который издает дерево, когда его срубают топором. Прежде чем одно дерево упадет, вы можете работать над другим деревом. Но есть надежда, что каждое дерево упадет последовательно. Дерево 2 не упадет раньше, чем Дерево 1.

По этой аналогии деревья - это медленно выполняющиеся асинхронные операции, такие как чтение с жесткого диска.

КОД: скопируйте и вставьте приведенный ниже код, чтобы имитировать чтение с жесткого диска. Или рубить дерево.

function readFileFromSlowHardDrive(fileLocation, readFileCallback) {
  var hardDrive = {
    ‘file1’: ‘1-uno’,
    ‘file2’: ‘2-dos’,
    ‘file3’: ‘3-tres’
  };
  let waitTime = Math.floor(Math.random() * (9000 - 1000)) + 1000;
  console.log("Requesting file from : " + fileLocation);
  setTimeout(
    () => {
      if (readFileCallback) {
        readFileCallback(hardDrive[fileLocation]);
      }
    }, waitTime
  );
}

Как рубить деревья одно за другим? Ниже наивный пример.

НАИВНЫЙ ПРИМЕР

readFileFromSlowHardDrive('file1', (file_1_Data) => {
  log(file_1_Data)
})
readFileFromSlowHardDrive('file2', (file_2_Data) => {
  log(file_2_Data)
})
readFileFromSlowHardDrive('file3', (file_3_Data) => {
  log(file_3_Data)
})```
=========OUTPUT===========
   
$ Requesting file from : file1
$ Requesting file from : file2
$ Requesting file from : file3
$ 2-dos
$ 1-uno
$ 3-tres

Ох, это нехорошо. «Файл2» удаляется перед «файл1». Мы хотим, чтобы он падал последовательно. Давайте перепишем код с обратными вызовами.

Обратный вызов Адская версия

const performance = {};
performance.now = require('performance-now'); //
let t1, t2
t1 = perfomance.now()/1000
readFileFromSlowHardDrive('file1', (file_1_Data) => {
  console.log(file_1_Data)
  readFileFromSlowHardDrive('file2', (file_2_Data) => {
    console.log(file_2_Data)
    readFileFromSlowHardDrive('file3', (file_3_Data) => {
      log(file_3_Data)
      let t2 = performance.now()/1000
      console.log(`completed in ${t1-t1} seconds`) 
    })
  })
})
=========OUTPUT===========
  
$ Requesting file from : file1
$ 1-uno
$ Requesting file from : file2
$ 2-dos
$ Requesting file from : file3
$ 3-tres
$ completed in 13 sec

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

Вложив свои функции чтения, я гарантирую порядок выполнения, но чтение файла [8s, 2s, 3s] займет 8 + 2 + 3 секунды. Мы хотим как можно скорее начать обработку данных (кипячение воды). Если я зажгу огонь для всех 3 чайников последовательно, но подаю горячую воду из чайника 1, несмотря на то, что чайник 2 и чайник 3 кипели почти 5–6 секунд назад, общее время для всех моих чашек чая займет ничтожные 8 секунд, а не все. 13с.

Окончательный код

const performance = {};
performance.now = require('performance-now');
/**
*summary reads file from slow hardDrive
*/
function readFileFromSlowHardDrive(fileLocation, readFileCallback) {
  var hardDrive = {
   'file1': '1-uno',
   'file2': '2-dos',
   'file3': '3-tres'
  };
  console.log(`
    Requesting file from : ${fileLocation} in ${waitTime/1000}      
    seconds`);
  let waitTime =  Math.floor(Math.random() * (9000 - 1000)) + 1000;
  setTimeout(
    () => {  
      if (readFileCallback) {
        readFileCallback(hardDrive[fileLocation]);
      }
    }, waitTime
  );
}
/**
* summary: creates a function that sets the closure variables in a 
* way that processes the files in sequence
* this is the main juice
*/
function getFile(fileLocation) {
  let stage_2_Data, newCallback;
 
  readFileFromSlowHardDrive(fileLocation, readFileCallback);
  function readFileCallback(stage_1_Data) {
    if (stage_1_Data) stage_2_Data = stage_1_Data;
    if (newCallback) newCallback(stage_1_Data);
  }
  return (
    function(topLevelCallback) {
      if (stage_2_Data) topLevelCallback(stage_2_Data)
      if (topLevelCallback) newCallback = topLevelCallback;   
    }
  );
}
var thunk_1 = getFile('file1');
var thunk_2 = getFile('file2');
var thunk_3 = getFile('file3');
let t1, t2;
t1 = performance.now()/1000;
thunk_1((data) => { //we'll start our discussion here. (LINE 40)
  console.log(data);
  thunk_2((data) => {
    console.log(data);
    thunk_3((data) => {
      console.log(data);
      t2 = perfomance.now()/1000;
      console.log(t2-t1,' seconds');
    })
  })
});
======output=======
Requesting file from : file1 in 8.577 seconds
Requesting file from : file2 in 2.486 seconds
Requesting file from : file3 in 3.288 seconds
1-uno
2-dos
3-tres
8.289574579 sec

8 секунд! (Это то, что мы хотим!)

Для понимания мы сосредоточимся на самой первой выполненной операции, а именно:

var thunk_1 = getFile('file1');

При этом запускается функция getFile, известная как «сначала вскипятить чайник1». Он также содержит первое закрытие. Давайте перепишем getFile с нуля. (Я удалил весь код.)

БАЗОВЫЙ GETFILE

function getFile(fileLocation) {
  let stage_2_Data; // undefined
  readFileFromSlowHardDrive(fileLocation, readFileCallback);***
  function readFileCallback(stage_1_Data) {
    stage_2_Data = stage_1_Data; // not ready yet
  }
// thunk_1 is a function defined below.
  return (function (thunk_1_Callback) { 
    thunk_1_Callback(stage_2_Data);  //more code is needed here.
  );
}

getFile (‘file1’) активирует readFileFromSlowHardDrive. Прежде чем вставлять данные в замыкание, убедитесь, что данные существуют.

function readFileCallback(stage_1_Data) {
    if (stage_1_Data) stage_2_Data = stage_1_Data;
  }

2s: Stage_2_Data успешно сохранена в замыкании thunk2. (‘2-dos’)
3s: Stage_2_Data thunk3 сохраняется (‘3-dos’)
8s: Stage_2_Data thunk1 сохраняется (‘1-uno’)

Чтобы увидеть, какова следующая строка исполнения, мы переписываем обратный вызов пирамиды гибели.

Перезапись обратных вызовов

function thunk_1_Callback(data) {
  console.log(data);
  thunk_2((data) => {
    console.log(data);
    thunk_3((data) => {
      console.log(data);
    })
  })
}
thunk_1(thunk_1_Callback) <----this is the line that gets executed.

Так вызывается thunk1. thunk_1_Callback не может продолжаться, пока thunk_1 не будет завершен. Что снова было thunk_1?

return (function (thunk_1_Callback) { 
    thunk_1_Callback(stage_2_Data);  //more code is needed here.
  );

Убедитесь, что stage2_data существует, прежде чем передавать его thunk_1_Callback.

....
return function(thunk_1_Callback) {
  if (stage_2_Data) {
    thunk_1_Callback(stage_2_Data);
  }
}

Stage_2_Data еще не готов. И не будет на 8 секунд. Нам нужно предоставить правильную точку входа для наших данных, когда они будут готовы. Мы также вставляем эту точку сохранения в наше закрытие. Назовем эту точку сохранения «newCallback». На этом этапе вы не можете контролировать, когда будет вызвана следующая строка выполнения. Это было дано readFileFromSlowHardDrive во время реализации getFile.

СТРЕЛКИ

let newCallback, stage_2_Data;
readFileFromSlowHardDrive('file1', readFileCallback) //already executed. Taking 8 seconds to load
function readFileCallback(stage_1_Data) {
  if (stage_1_Data) {
    stage_2_Data = stage_1_Data;
  } 
  if (newCallback) { 
    newCallback(stage_1_Data); <------------
  }
}
// for simplicity, i have written the callback inside this function as thunk_1_Callback but it should be topLevelCallback since it will change to thunk_2_Callback and so forth as the thunks progress.
return (function (thunk_1_Callback) {
    if (stage_2_Data) {
      thunk_1_Callback(stage_2_Data);
    }  
    if (thunk_1_Callback) {
      newCallback = thunk_1_Callback; <-----------
    }
);

КОД НИЖЕ: просто чтобы напомнить вам, насколько велик thunk_1_Callback.

function thunk_1_Callback((data) => {
  console.log(data);
  thunk_2((data) => {
    console.log(data);
    thunk_3((data) => {
      console.log(data);
    })
  })
}

Когда через 8 секунд данные thunk1 будут готовы, он будет использовать thunk_1_Callback для регистрации данных.

Следующая строка выполнения (переписанная для простоты):

function thunk_2_Callback(data) {
  console.log(data);
  thunk_3((data) => {
    console.log(data);
  });
}
thunk_2(thunk_2_Callback); <----

К этому моменту как thunk_2, так и thunk_3 имеют свои stage2_data, так как thunk_1_Callback / newCallback запускается за 8 секунд. Когда thunk_2 активирован, все, что ему нужно сделать, это активировать thunk_2_Callback (stage_2_data), поскольку оба готовы.

Представьте, если бы время перехода было [1 с, 2 с, 3 с]! или [2s, 5s, 3s]! (Это я оставляю вам.)

Создавая и вызывая преобразователи, можно создать облегченный класс Promise. Может быть.

Это интерпретация упражнения 2 курса Кайла Симпсона по интерфейсным мастерам. Я очень рекомендую это.

Https://frontendmasters.com/courses/rethinking-async-js/

  • Спасибо Стивену Ляо и Форресту Акинсу за чтение бета-версии.