
По умолчанию Javascript является синхронным языком. Это означает, что все операторы и функции выполняются один за другим в заранее определенном порядке. Javascript ведет себя так, потому что он имеет только один поток выполнения. Другие языки, такие как Java, предоставляют многопоточную среду выполнения, в которой есть основной поток, а также другие потоки, которые могут быть созданы во время выполнения для параллельного выполнения задач. Следовательно, асинхронность на этих языках довольно проста и понятна.
Однако, поскольку Javascript предоставляет нам только один поток выполнения, нам нужно понимать, как могут выполняться определенные функции, которые кажутся асинхронными, например функция setTimeout. Но прежде чем мы это сделаем, давайте посмотрим, как работает поток однопоточного выполнения.
Однопотоковое исполнение
Возьмем пример этой простой программы
Выход:
1 2
В строке 1 программа сохраняет объявление функции в переменной «sayOne». Обратите внимание, что он сохраняет только объявление функции, но еще не вызывает его. Итак, на данный момент ни один из его кодов фактически не запущен, и, следовательно, строка 2 еще не будет выполнена. В строке 5 он сохраняет определение другой функции в переменной «sayTwo» (но еще не вызывает его). В строке 9 он вызывает функцию sayOne. На этом этапе выполняется сохраненное определение функции sayOne, в результате чего выполняется строка 2 программы и значение «1» выводится на консоль. Точно так же в строке 10 программа вызывает функцию sayTwo, что приводит к выполнению строки 6, которая выводит на консоль значение «2».
Вышеупомянутый процесс выполнения кажется довольно простым и легким для понимания. Javascript выполняет программу построчно и выполняет их в указанном порядке. Однако, как вы могли заметить, программа на самом деле не выполняется построчно, и из-за вызовов функций происходят некоторые скачки в порядке выполнения строк. Мы увидим это позже в этой статье. Также следует отметить, что Javascript не перейдет к следующей строке выполнения, пока не будет выполнена предыдущая строка.
Например, предположим, что функция sayOne имеет сложный код, выполнение которого занимает много времени (например, секунда). В этом случае в строке 9 программа будет ждать полного выполнения функции sayOne, прежде чем перейти к строке 10, чтобы выполнить там функцию sayTwo. Это связано с тем, что, как мы указывали ранее, Javascript работает в одном потоке выполнения, который используется всеми функциями. Следовательно, программа ожидает, пока текущая функция не будет полностью выполнена, прежде чем двигаться дальше.
Давайте немного подправим приведенный выше код и попытаемся понять, что происходит дальше. Давайте добавим третью функцию под названием «sayThree».
Давайте теперь вызовем эту функцию в коде
Как видите, мы вызвали функцию sayThree (которая выводит на консоль цифру «3») внутри тела функции sayOne. Следовательно, что, по вашему мнению, произойдет в строке 14 при выполнении вышеуказанной программы при вызове функции sayOne? Будет ли консоль сначала записывать «1», а затем переходить к выполнению кода sayThree, или она приостанавливает свое выполнение, вызывает функцию sayThree, подождите, пока она завершено, а затем перейти к окончательной печати «1» на консоли?
Что ж, вывод вышеуказанной программы будет
3 1 2
Чтобы понять это, нам нужно понять, как Javascript поддерживает порядок функций внутри.
Стек вызовов
В Javascript есть что-то, называемое стеком вызовов, для отслеживания порядка выполнения функций. Стек вызовов, как следует из названия, представляет собой стек. Следовательно, элементы, добавленные в эту стопку, будут выходить из нее в порядке последний пришел - первый ушел.
В приведенной выше программе, когда программа достигает строки 14, программа Javascript видит, что должна быть вызвана функция sayOne. Когда это происходит, он добавляет функцию SayOne в стек вызовов. Итак, стек на данный момент выглядит так
| | | | | | | | | sayOne() | |____________| Call stack
Эта функция остается в стеке вызовов и извлекается только после завершения ее выполнения. Как работает Javascript, он всегда сначала выполняет функцию наверху стека, затем выталкивает ее из стека, а затем переходит к следующей функции в стеке. Следовательно, программа теперь «перескакивает» в выполнение функции наверху стека, то есть функции sayOne. Выполнение sayOne начинается со строки 2, где программа видит, что должна быть вызвана функция sayThree. Итак, Javascript добавляет эту функцию в стек вызовов. Обновленный стек вызовов теперь выглядит так
| | | | | | | sayThree() | | sayOne() | |_____________| Call stack
Затем программа переходит к выполнению функции наверху стека, которая является функцией sayThree. Теперь выполняется код функции sayThree, в которой строка 11 выводит на консоль цифру «3». Затем программа видит, что она достигла конца функции sayThree, и, следовательно, выталкивает ее из стека. Итак, стек теперь выглядит так
| | | | | | | | | sayOne() | |_____________| Call stack
Затем Javascript видит, что функция sayOne находится наверху стека, и, следовательно, возвращается к тому месту, где она остановилась при выполнении. Строка 3 выводит на консоль цифру «1». Еще раз, по достижении конца функции sayOne, Javascript выталкивает ее из стека, которая затем выглядит как
| | | | | | | | | | |_____________| Call stack
Увидев, что стек пуст, программа Javascript затем переходит к тому месту, где она остановилась при первоначальном выполнении, то есть к строке 15, где она видит, что вызывается функция sayTwo. Как вы уже догадались, sayTwo добавляется в стек
| | | | | | | | | sayTwo() | |_____________| Call stack
Поскольку это самая верхняя функция в стеке, выполнение программы переходит к sayTwo, где в строке 7 на консоль выводится значение «2». По достижении конца функции она извлекается из стека, и стек вызовов становится пустым. Больше нет строк для запуска, и, следовательно, программа завершается.
Пока мы обсуждали стек вызовов, вы могли быть немного озадачены тем, как Javascript «перескакивает» во время своего выполнения на функцию наверху стека вызовов. Как Javascript узнает, когда новая функция добавляется в стек или когда функция удаляется из стека, и, следовательно, ему необходимо возобновить выполнение следующей функции в стеке?
Об этом и многом другом я расскажу в Части 2 этой серии.