Eventloop — это не страшно, страшно встретить Promise

JavaScript

Вопросы интервью о Eventloop + Promise примерно разделены на следующие версии: удобная версия, квалифицированная версия, непревзойденная версия, пиковая версия и крайне ненормальная версия. Если предположить, что друзья будут сражаться до последнего вопроса, они будут непобедимы, когда столкнутся с такими проблемами в будущем. Конечно, если интервьюеры смогут придумать более извращенный вариант, я проиграю.

Версия 1: Удобная версия

Тестовый сайт: порядок выполнения в цикле событий, отличие макрозадач от микрозадач.

Tucao: Я этого не понимаю, я ничего не могу поделать, иди домой и учись снова.

setTimeout(()=>{
   console.log(1) 
},0)
Promise.resolve().then(()=>{
   console.log(2) 
})
console.log(3) 

Интервьюеры в этой версии очень дружелюбные.Они только проверяют ваше понимание концепции и понимают макрозадачу (marcotask) и микрозадачу (microtask).Этот вопрос является подвопросом.

Ответ автора: Это проблема Eventloop. После запуска основного скрипта будет очередь микрозадач и очередь макрозадач. Сначала выполняются микрозадачи, а затем макрозадачи.

PS: проблема с концепцией

Иногда встречается версия,Макрозадачи > Микрозадачи > Макрозадачи, здесь мне нужно уточнить понятие, чтобы избежать путаницы. Здесь есть понятие основного скрипта, который представляет собой код, который выполняется в начале (код должен выполняться в начале, справа, иначе откуда берется очередь макрозадач и микрозадач), который определяется как макрозадача (мне нравится использовать основную концепцию сценария, выполняется отдельно, не смешивается с двумя очередями задач), а затем очищается в соответствии с очередью микрозадач и очередью макрозадач, сгенерированных в основном сценарии. время сначала очищается очередь микрозадач, а затем макрозадач.

Версия 2: простая в использовании версия

В этой версии, чтобы проверить понимание Promise, интервьюеры добавят к вопросу некоторый материал:

Тестовый сайт: исполнитель обещаний, а затем метод выполнения

Tucao: Это маленькая яма, и обещания освоены умело, это маленький эпизод из жизни.

setTimeout(()=>{
   console.log(1) 
},0)
let a=new Promise((resolve)=>{
    console.log(2)
    resolve()
}).then(()=>{
   console.log(3) 
}).then(()=>{
   console.log(4) 
})
console.log(5) 

Кажется, что этот вопрос проверяет Eventloop, но на самом деле он проверяет мастерство Promises. Все мы знаем, что then в Promise — это микрозадача, но как выполняется then и является ли исполнитель Promise асинхронным или синхронным?

Демонстрация ошибки: then of Promise — это асинхронный процесс, после выполнения каждого then это новый цикл, поэтому второй then будет выполняться после setTimeout. (Да, это ответ автора в определенный день в определенном году. Дайте, пожалуйста, пистолет, мне очень хочется убить себя в это время.)

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

Детальный анализ

(Если вам это не нравится, вы можете обратиться к моей другой статье,Реализовать обещание с нуля, объяснение внутри легко понять. ) Давайте в качестве примера возьмем реализацию промисов в core-js Babel и взглянем на спецификацию выполнения промисов:

Расположение кода:promise-polyfill

PromiseConstructor = function Promise(executor) {
    //...
    try {
      executor(bind(internalResolve, this, state), bind(internalReject, this, state));
    } catch (err) {
      internalReject(this, state, err);
    }
};

Здесь хорошо видно, что executor в Promise — это немедленно исполняемая функция.

then: function then(onFulfilled, onRejected) {
    var state = getInternalPromiseState(this);
    var reaction = newPromiseCapability(speciesConstructor(this, PromiseConstructor));
    reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true;
    reaction.fail = typeof onRejected == 'function' && onRejected;
    reaction.domain = IS_NODE ? process.domain : undefined;
    state.parent = true;
    state.reactions.push(reaction);
    if (state.state != PENDING) notify(this, state, false);
    return reaction.promise;
},

Тогда есть тогдашняя функция Обещания, которая хорошо виднаreaction.promise, то есть каждый раз после выполнения возвращается новое обещание. То есть текущая очередь микрозадачи (microtask) очищается, но затем она добавляется снова, и следующая волна макрозадач (marcotask) не будет выполняться до тех пор, пока не будет опустошена очередь микрозадачи (microtask).

//state.reactions就是每次then传入的函数
 var chain = state.reactions;
  microtask(function () {
    var value = state.value;
    var ok = state.state == FULFILLED;
    var i = 0;
    var run = function (reaction) {
        //...
    };
    while (chain.length > i) run(chain[i++]);
    //...
  });

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

Так как же реализована очередь микрозадач в браузерах без промисов?

Советы:Polyfill для микрозадач в Babel, если он имеетsetImmediateплатформу функций, используйте ее, если нет, настройте ее и используйте различные, такие как nodejsprocess.nextTick, поддерживается в браузерахpostMessage, либо создав скрипт для реализации микрозадач. В конце концов используется setTimeout, но к микрозадачам это отношения не имеет, а промисы становятся членами макрозадач.

Расширьте свое мышление:

Почему иногда функция - это массив? Иногда это функция?

Мы немного модифицируем вышеприведенные темы, поворачиваем функцию цепного вызова и вызываем THEN. Не говоря уже о различном использовании между этим и цепным вызовом, это отличается только с практической точки зрения. Цепной вызов — это каждый раз новый Promise, то есть метод обратного вызова в каждом THEN принадлежит микрозадаче, и этот отдельный вызов поместит callback-функцию Push в THEN в массив, затем выполнит ее. Другими словами, цепные вызовы могут быть взяты из других оболочек функций в EvenLoop, и они не будут вызываться (только для самых распространенных случаев).

let a=new Promise((resolve)=>{
     console.log(2)
     resolve()
})
a.then(()=>{
    console.log(3) 
})
a.then(()=>{
    console.log(4) 
})
 

В следующем модуле будет подробно объяснено поведение «перехода из очереди» в этой микрозадаче.

Версия 3: чистое синее издание

Эта версия является эволюционной версией предыдущей версии. Функция then предыдущей версии промиса не возвращала промис. Если промис создается в then промиса, каков результат?

Тестовый сайт: расширенное использование обещания, мастерство возврата обещания в то время

Тукао: обещания тоже могут быть адом...

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})

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

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

Ready GO

первый раунд

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

второй раунд

  • текущая задача: выполняется then1 и выводится немедленноthen11и новое обещание2promise2
  • очередь микрозадач: [затем функция нового обещания2, а вторая затем функция обещания1]

третий раунд

  • текущая задача: тогда вывод функции нового обещания2then21а второй затем вывод функции promise1then12.
  • очередь микрозадач: [вторая функция нового обещания2]

четвертый раунд

  • текущая задача: вывод второй, затем функция нового обещания2then23
  • micro task queue: []

END

Окончательные результаты[promise1,then11,promise2,then21,then12,then23].

Вариант версии 1: Что, если then в промисе здесь возвращает промис? ?

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    return new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})

Вот ситуация, когда then in Promise возвращает промис.В центре внимания этого теста находится Promise, а не Eventloop. Легко понять, почемуthen12Будет вthen23После выполнения второй then промиса эквивалентен зависанию на возвращаемом значении последнего then нового промиса.

Вариант версии 2: Если здесь более одного промиса, повлияет ли добавление нового промиса на результат? ?

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
new Promise((resolve,reject)=>{
    console.log("promise3")
    resolve()
}).then(()=>{
    console.log("then31")
})

Улыбка постепенно меняется, и мы тоже можем выстроить очередь в сердцах:

первый раунд

  • текущая задача: обещание1, обещание3
  • micro task queue: [promise1的第一个then,promise3的第一个then]

второй раунд

  • текущая задача: then11, promise2, then31
  • micro task queue: [promise2的第一个then,promise1的第二个then]

третий раунд

  • Текущая задача: Тогда21, затем12
  • micro task queue: [promise2的第二个then]

четвертый раунд

  • current task: then23
  • micro task queue: []

Окончательный вывод: [promise1,promise3,then11,promise2,then31,then21,then12,then23]

Версия 4: Пиковое издание

Тестовый сайт: в режиме async/await влияние на Eventloop.

Слоты: не ведитесь на асинхронность/ожидание, это несложный вопрос.

Думаю, что все видели такую ​​тему.У меня тут очень простое объяснение.Интересно, всем ли интересно.

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}

async  function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 

async/await влияет только на выполнение внутри функции, а не на порядок выполнения вне функции. Другими словами, async1() не блокирует выполнение последующих программ.await async2()Эквивалент Обещания,console.log("async1 end");Эквивалент функции, выполняемой после then предыдущего промиса.

Согласно решению в предыдущей главе, окончательный результат вывода: [script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout]

Если вы разбираетесь в использовании async/await, вы не считаете эту проблему сложной, но если вы не понимаете или немного знаете об этом, то эта проблема — катастрофа.

  • Единственным спорным моментом здесь является приоритет async then и promises then, подробности см. ниже. *

Подробное объяснение приоритета async/await и обещания

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}
async  function async2() {
    console.log( 'async2');
}
// 用于test的promise,看看await究竟在何时执行
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
}).then(function () {
    console.log("promise3");
}).then(function () {
    console.log("promise4");
}).then(function () {
    console.log("promise5");
});

Позвольте сначала задать вам вопрос: если бы вас попросили выполнить полифилл async/await, как бы вы полифилили приведенный выше код? Авторская версия приведена ниже:

function promise1(){
    return new Promise((resolve)=>{
        console.log("async1 start");
        promise2().then(()=>{
            console.log("async1 end");
            resolve()
        })
    })
}
function promise2(){
    return new Promise((resolve)=>{
        console.log( 'async2'); 
        resolve() 
    })
}

По мнению автора,asyncсам по себе являетсяPromise,Потомawaitобязательно следуй за однимPromise, затем создайте две новые функции, каждая из которых возвращает Promise. тогдаfunction promise1нужно подождатьfunction promise2Промис выполняется после завершения промиса, затемthenНемного.

Результаты этой версии:[async1 start,async2,promise1,async1 end,promise2,...], ожидание асинхронности стоит перед промисом.тогда теста, собственно этот результат можно получить и от полифилла автора.

Затем я удивлен, что при использовании собственного async/await результат несовместим с приведенным выше полифиллом! Результат:[async1 start,async2,promise1,promise2,promise3,async1 end,...], так как promise.then каждый раз представляет собой новый раунд микрозадач, async выводится только после 2 раундов микрозадач, а выводится третий раунд микрозадач (для этого см. объяснение версии 3).

/* внезапная тишина */

Вот вставка, async/await считается дорогим, потому что для завершения await требуется 3 раунда микрозадач, поэтому V8 и Nodejs12 начали это исправлять.Подробнее см.Этот тянуть на github

Итак, автор использует другой способ полифилла, я думаю, все прекрасно поняли, что за ожиданием следует промис, но что, если этот промис не является хорошим промисом? Асинхронность — это хорошая асинхронность, обещания — это нехорошо. V8 очень свиреп, для решения этой проблемы добавлены два дополнительных промиса, а исходный код упрощен, что, вероятно, выглядит следующим образом:

// 不太准确的一个描述
function promise1(){
    console.log("async1 start");
    // 暗中存在的promise,笔者认为是为了保证async返回的是一个promise
    const implicit_promise=Promise.resolve()
    // 包含了await的promise,这里直接执行promise2,为了保证promise2的executor是同步的感觉
    const promise=promise2()
    // https://tc39.github.io/ecma262/#sec-performpromisethen
    // 25.6.5.4.1
    // throwaway,为了规范而存在的,为了保证执行的promise是一个promise
    const throwaway= Promise.resolve()
    //console.log(throwaway.then((d)=>{console.log(d)}))
    return implicit_promise.then(()=>{
        throwaway.then(()=>{
            promise.then(()=>{
                console.log('async1 end');
            })
        }) 
    })
}

ps: Чтобы принудительно задержать выполнение двух микрозадач, автор тоже сильно постарался.

Подводя итог: async/await иногда задерживает два раунда микрозадач и выполняется в третьем раунде микрозадач. Основная причина в том, что браузер анализирует этот метод. Чтобы разобрать ожидание, необходимо создать два дополнительных обещания, поэтому он тратит много денег.. Позже, чтобы уменьшить потери, V8 исключил промис и сократил 2 раунда микрозадач, поэтому последняя версия должна быть асинхронной с нулевой стоимостью.

Версия 5: Окончательная метаморфоза

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

Тестовый сайт: событие nodejs + Promise + async/await + буддийский setImmediate

Слоты: я не знаю, какой из них может появиться первым

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}
async  function async2() {
    console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
    console.log("settimeout");
});
async1()
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
setImmediate(()=>{
    console.log("setImmediate")
})
process.nextTick(()=>{
    console.log("process")
})
console.log('script end'); 

Запуск исполнения очереди

первый раунд:

  • текущая задача: «запуск сценария», «запуск async1», «async2», «promise1», «конец сценария»
  • очередь микрозадач: [async,promise.then,process]
  • очередь задач макроса: [setTimeout,setImmediate]

второй раунд

  • текущая задача: процесс, конец async1, обещание.затем
  • очередь микро задач: []
  • очередь задач макроса: [setTimeout,setImmediate]

третий раунд

  • текущая задача: setTimeout, setImmediate
  • очередь микро задач: []
  • очередь задач макросов: []

Окончательные результаты: [script start,async1 start,async2,promise1,script end,process,async1 end,promise2,setTimeout,setImmediate]

Также приоритет между «async1 end», «promise2» зависит от платформы.

Резюме автора по галантерее

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

  • Первый шаг — подтвердить макрозадачи и микрозадачи.

    • Задачи макроса: скрипт, setTimeout, setImmediate, исполнитель в обещании
    • Микрозадачи: promise.then, process.nextTick
  • Второй шаг - анализ "камня преткновения". Не паникуйте, когда появляется async/await. Они могут нажить состояние только на отмеченной функции. Когда эта функция отсутствует, это все еще тренд армии.

  • Третий шаг — сделать разные суждения в соответствии с разными способами использования then в Promise, независимо от того, связано ли оно с цепочкой или вызывается отдельно.

  • Последний шаг запомнить некоторые особые события

    • Например,process.nextTickприоритет надPromise.then

Справочный URL, рекомендуемое чтение:

Как реализовать Async/Await в V8,Более быстрые асинхронные функции и обещания

Что касается спецификации async/await,ecma262

Также есть исходный код babel-polyfill,promise

постскриптум

Hello~Anybody here?

Первоначально автор не хотел писать эту статью, потому что есть ощущение зрения, которое имитирует 5-летний вступительный экзамен в колледж и 3-летнюю симуляцию.Однако интервьюеры слишком жестоки, и они делают все возможное, чтобы " пытать" интервьюеров. Но так как автор полностью освоил использование Eventloop, это скрытое благословение~

Кто-нибудь видел конец? Приходите и поговорите с автором об извращенных проблемах Eventloop+Promise, с которыми вы столкнулись.

Добро пожаловать в перепечатку ~ но, пожалуйста, укажите источник ~ впервые опубликовано в Наггетс ~Eventloop — это не страшно, страшно встретить Promise