Maison >interface Web >js tutoriel >Explication détaillée des exemples de mécanismes d'exécution js
Pour comprendre le mécanisme d'exécution de JavaScript, vous devez avoir une compréhension approfondie de plusieurs points : le mécanisme monothread de JavaScript, la file d'attente des tâches (tâches synchrones et tâches asynchrones), les événements et les fonctions de rappel, les minuteries et la boucle d'événements.
L'une des fonctionnalités du langage JavaScript (également le cœur du langage) est le single-thread. En termes simples, un seul thread signifie que ne peut faire qu'une seule chose à la fois Lorsqu'il y a plusieurs tâches, elles ne peuvent être accomplies que dans un ordre avant d'exécuter la suivante.
Le monothreading de JavaScript est lié à son objectif de langage. En tant que langage de script de navigateur, l'objectif principal de JavaScript est de compléter l'interaction de l'utilisateur et de faire fonctionner le DOM. Cela détermine qu'il ne peut être qu'un seul thread, sinon cela entraînera des problèmes de synchronisation complexes.
Imaginez que JavaScript ait deux threads en même temps. Un thread doit ajouter du contenu à un certain nœud DOM, et l'opération de l'autre thread consiste à supprimer le nœud. Alors, qui devrait le navigateur. utiliser? ?
Ainsi, afin d'éviter toute complexité, JavaScript est monothread depuis sa naissance.
Afin d'améliorer l'utilisation du processeur, HTML5 propose le standard Web Worker, qui permet aux scripts JavaScript de créer plusieurs threads, mais les threads enfants sont entièrement contrôlés par le thread principal et ne sont pas autorisés à faire fonctionner le DOMAINE. Cette norme ne modifie donc pas la nature monothread de JavaScript.
Terminer les tâches les unes après les autres signifie que les tâches à accomplir doivent être mises en file d'attente, alors pourquoi doivent-elles être mises en file d'attente ?
Habituellement, il y a deux raisons de faire la queue :
La charge de calcul de la tâche est trop importante et le processeur est occupé ; >Tâche Les éléments requis ne sont pas prêts et l'exécution ne peut donc pas continuer, ce qui entraîne l'inactivité du processeur et l'attente des périphériques d'entrée et de sortie (périphériques d'E/S).
Par exemple, certaines tâches nécessitent qu'Ajax obtienne des données avant de pouvoir être exécutées
À partir de là, les concepteurs de JavaScript ont également réalisé qu'à ce stade, il est tout à fait possible d'exécuter les tâches qui sont prêtes plus tard pour améliorer l'efficacité opérationnelle, c'est-à-dire suspendre les tâches en attente et les mettre de côté, puis les exécuter après obtenir ce qui est nécessaire. C'est comme lorsque l'autre partie part pendant un moment lorsque vous répondez au téléphone et qu'il y a un autre appel, vous raccrochez donc l'appel en cours, attendez la fin de cet appel, puis vous reconnectez à l'appel précédent. Par conséquent, les concepts de synchronisation et d'asynchrone sont apparus et les tâches ont été divisées en deux types, l'une est une tâche synchrone (Synchronous) et l'autre est une tâche asynchrone (Asynchronous).
Tâches synchrones : les tâches qui doivent être exécutées sont mises en file d'attente sur le thread principal, l'une après l'autre. Une fois la précédente terminée, la suivante est exécutée
Tâche asynchrone : les tâches qui ne sont pas exécutées immédiatement mais qui doivent être exécutées sont stockées dans la "file d'attente des tâches". La "file d'attente des tâches" informera le thread principal quand quelle tâche asynchrone peut être exécutée, puis le La tâche entrera dans le thread principal et sera exécutée.
Toutes les exécutions synchrones peuvent être considérées comme des exécutions asynchrones sans tâches asynchrones
Plus précisément, l'exécution asynchrone est la suivante :
(1) Toutes les tâches synchrones sont exécutées sur le thread principal, formant une
C'est-à-dire que toutes les tâches qui peuvent être exécutées immédiatement sont mises en file d'attente sur le thread principal et exécutées les unes après les autres.
(2) En plus du fil de discussion principal, il existe également une "file d'attente des tâches". Tant que la tâche asynchrone a des résultats en cours d'exécution, un événement est placé dans la « file d'attente des tâches ».
(3) Une fois que toutes les tâches de synchronisation dans la « pile d'exécution » sont terminées, le système lira la « file d'attente des tâches » pour voir quels événements s'y trouvent. Les tâches asynchrones correspondantes mettent fin à l'état d'attente et entrent dans la pile d'exécution pour commencer l'exécution.
(4) Le fil principal répète continuellement les trois étapes ci-dessus.
Alors comment savez-vous que la pile d'exécution du thread principal est vide ? Il existe un processus de surveillance dans le moteur js, qui vérifiera en permanence si la pile d'exécution du thread principal est vide, une fois vide, elle ira dans la file d'attente des événements pour vérifier s'il y a une fonction en attente d'appel.
diagramme guide pour illustrer le fil de discussion principal et la file d'attente des tâches.
Si le contenu de la carte est exprimé en mots :Les tâches synchrones et asynchrones entrent respectivement dans différents "lieux" d'exécution, entrant de manière synchrone dans le thread principal, et entrant de manière asynchrone dans la table des événements et en enregistrant les fonctions.
Lorsque l'opération spécifiée est terminée, la table des événements déplacera cette fonction dans la file d'attente des événements.
La tâche dans le thread principal est vide après l'exécution. Elle ira dans la file d'attente des événements pour lire la fonction correspondante et entrera dans le thread principal pour l'exécution.
Le processus ci-dessus sera répété en continu, ce qui est souvent appelé boucle d'événement.
La "file d'attente des tâches" est une file d'attente d'événements (peut également être comprise comme une file d'attente de messages), IO Lorsque l'appareil termine une tâche, un événement sera ajouté à la « file d'attente des tâches », indiquant que les tâches asynchrones associées peuvent entrer dans la « pile d'exécution ». Ensuite, le thread principal lit la "file d'attente des tâches" pour voir quels événements s'y trouvent.
En plus des événements du périphérique IO, les événements de la « File d'attente des tâches » incluent également certains événements générés par l'utilisateur (tels que les clics de souris, le défilement de page, etc.). Tant que la fonction de rappel est spécifiée, ces événements entreront dans la « file d'attente des tâches » lorsqu'ils se produiront, en attendant que le thread principal les lise.
La soi-disant "fonction de rappel" (callback) est le code qui sera suspendu par le thread principal. Les tâches asynchrones doivent spécifier une fonction de rappel. Lorsque le thread principal commence à exécuter une tâche asynchrone, la fonction de rappel correspondante est exécutée.
La "file d'attente des tâches" est une structure de données premier entré, premier sorti. Les événements classés en premier sont lus en premier par le thread principal. Le processus de lecture du thread principal est fondamentalement automatique. Dès que la pile d'exécution est effacée, le premier événement de la « file d'attente des tâches » entrera automatiquement dans le thread principal. Cependant, si un "timer" est inclus, le thread principal doit d'abord vérifier le temps d'exécution. Certains événements ne peuvent revenir au thread principal qu'après le temps spécifié.
Le thread principal lit les événements de la "file d'attente des tâches". Ce processus est cyclique, donc l'ensemble du mécanisme de fonctionnement est également appelé "Boucle d'événement".
Afin de mieux comprendre l’Event Loop, voici une image du discours de Philip Roberts.
Dans l'image ci-dessus, lorsque le thread principal est en cours d'exécution, il génère un tas (heap) et une pile (stack). Le code dans la pile appelle diverses API externes. , et " Divers événements (clic, chargement, terminé) sont ajoutés à la "file d'attente des tâches". Lorsque le code de la pile est exécuté, le thread principal lira la « file d'attente des tâches » et exécutera les fonctions de rappel correspondant à ces événements dans l'ordre.
Le code dans la pile d'exécution (tâche synchrone) est toujours exécuté avant de lire la "file d'attente des tâches" (tâche asynchrone).
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } })console.log('代码执行结束');
Ce qui précède est un simple ajax
code de requête :
ajax entre dans la table des événements et enregistre la fonction de rappel success
.
Exécuter console.log('代码执行结束')
.
L'événement ajax est terminé et la fonction de rappel success
entre dans la file d'attente des événements.
Le thread principal lit la fonction de rappel success
à partir de la file d'attente des événements et l'exécute.
En plus de placer des événements pour les tâches asynchrones, la "file d'attente des tâches" peut également placer des événements chronométrés, c'est-à-dire spécifier la durée pendant laquelle certains codes seront exécutés après. C'est ce qu'on appelle la fonction timer, qui est un code exécuté régulièrement.
SetTimeout()
et setInterval()
peuvent être utilisés pour enregistrer des fonctions appelées une fois ou à plusieurs reprises après une heure spécifiée. Leurs mécanismes de fonctionnement internes sont exactement les mêmes. La différence est que le code spécifié par le premier est. exécuté une fois, tandis que le code spécifié par ce dernier est exécuté une fois. Sera appelé à plusieurs reprises à des intervalles de millisecondes spécifiés :
setInterval(updateClock, 60000); //60秒调用一次updateClock()
Parce qu'il s'agit de fonctions globales importantes dans JavaScript côté client, elles sont définies comme des méthodes. de l'objet Window.
Mais en tant que fonction générale, cela ne fera rien à la fenêtre.
La méthode setTImeout()
de l'objet Window est utilisée pour implémenter une fonction à exécuter après un nombre spécifié de millisecondes. Il accepte donc deux paramètres, le premier est la fonction de rappel et le second est le nombre de millisecondes pour différer l'exécution. setTimeout()
et setInterval()
renvoient une valeur, qui peut être transmise à clearTimeout()
pour annuler l'exécution de cette fonction.
console.log(1); setTimeout(function(){console.log(2);}, 1000);console.log(3);
Les résultats d'exécution du code ci-dessus sont 1, 3, 2, car setTimeout()
reporte l'exécution de la deuxième ligne à 1000 millisecondes plus tard.
Si le deuxième paramètre de setTimeout()
est défini sur 0, cela signifie qu'une fois le code actuel exécuté (la pile d'exécution est effacée), la fonction de rappel spécifiée sera exécutée immédiatement (intervalle de 0 milliseconde).
setTimeout(function(){console.log(1);}, 0);console.log(2)
Le résultat de l'exécution du code ci-dessus est toujours 2, 1, car le système n'exécutera la fonction de rappel dans la "file d'attente des tâches" qu'après l'exécution de la deuxième ligne.
En bref, le sens de setTimeout(fn,0)
est de spécifier une tâche à exécuter dans le premier temps d'inactivité disponible du thread principal, c'est-à-dire à exécuter le plus tôt possible. Il ajoute un événement à la fin de la « file d'attente des tâches », il ne sera donc exécuté que lorsque la tâche de synchronisation et les événements existants dans la « file d'attente des tâches » auront été traités.
HTML5标准规定了
setTimeout()
的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
需要注意的是,setTimeout()
只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()
指定的时间执行。
由于历史原因,setTimeout()
和setInterval()
的第一个参数可以作为字符串传入。如果这么做,那这个字符串会在指定的超时时间或间隔之后进行求值(相当于执行eval()
)。
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
Node.js的运行机制如下。
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对”任务队列”的理解。
process.nextTick方法可以在当前”执行栈”的尾部—-下一次Event Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子
process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0)// 1// 2// TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前”执行栈”的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前”执行栈”执行。
现在,再看setImmediate。
setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0);
上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2。
令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function (){setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2
上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1–TIMEOUT FIRED–2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。
我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前”执行栈”一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取”事件队列”!
process.nextTick(function foo() {process.nextTick(foo); });
事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。
另外,由于process.nextTick指定的回调函数是在本次”事件循环”触发,而setImmediate指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”)。
除了广义的同步任务和异步任务,任务还有更精细的定义:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick
事件循环,宏任务,微任务的关系如图所示:
按照宏任务和微任务这种分类方式,JS的执行机制是
执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
请看下面的例子:
setTimeout(function(){ console.log('定时器开始啦') }); new Promise(function(resolve){ console.log('马上执行for循环啦'); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); } }).then(function(){ console.log('执行then函数啦') }); console.log('代码执行结束');
首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
遇到 new Promise直接执行,打印”马上执行for循环啦”
遇到then方法,是微任务,将其放到微任务的【队列里】
打印 “代码执行结束”
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印”执行then函数啦”
到此,本轮的event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印”定时器开始啦”
所以最后的执行顺序是【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】
我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) })process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
第一轮事件循环流程分析如下:
整体script作为第一个宏任务进入主线程,遇到console.log
,输出1。
遇到setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。
遇到process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。
遇到Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。
又遇到了setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
* 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
我们发现了process1
和then1
两个微任务。
执行process1
,输出6。
执行then1
,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
首先输出2。接下来遇到了process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
* 第二轮事件循环宏任务结束,我们发现有process2
和then2
两个微任务可以执行。
* 输出3。
* 输出5。
* 第二轮事件循环结束,第二轮输出2,4,3,5。
* 第三轮事件循环开始,此时只剩setTimeout2了,执行。
* 直接输出9。
* 将process.nextTick()
分发到微任务Event Queue中。记为process3
。
* 直接执行new Promise
,输出11。
* 将then
分发到微任务Event Queue中,记为then3
。
宏任务Event Queue | 微任务Event Queue |
---|---|
process3 | |
then3 |
* 第三轮事件循环宏任务执行结束,执行两个微任务process3
和then3
。
* 输出10。
* 输出12。
* 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!