En tant que développeur passé d'autres langages de programmation (C#/Java) à Javascript, en cours d'apprentissage de Javascript, le principe de fonctionnement de la méthode setTimeout() est une partie que j'ai rencontrée et qui n'est pas facile à comprendre. Cet article tente de le combiner avec d'autres langages de programmationL'implémentation, à partir du modèle de boucle d'événements setTimeout
La méthode setTimeout() n'est pas définie par la spécification ecmascript, mais est une fonction fournie par le BOM. Voir la définition de w3school de la méthode setTimeout(). La méthode setTimeout() est utilisée pour appeler une fonction ou calculer une expression après un nombre spécifié de millisecondes.
Syntaxe setTimeout(fn, millisec), où fn représente le code à exécuter, qui peut être une chaîne contenant du code JavaScript ou une fonction. Le deuxième paramètre millisec est le temps exprimé en millisecondes, indiquant la durée pendant laquelle fn doit être retardé.
Après avoir appelé la méthode setTimeout(), la méthode renvoie un numéro. Ce numéro est un identifiant unique du code d'exécution prévu, qui peut être utilisé pour annuler l'appel du timeout.
Au début, mon utilisation de setTimeout() était relativement simple et je n'avais pas une compréhension approfondie de son mécanisme de fonctionnement jusqu'à ce que je voie le code suivant
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
Dans ma compréhension initiale de setTimeout(), le délai était fixé à 500 ms, donc la sortie devrait être Temps écoulé : 500 ms. Parce que dans une compréhension intuitive, le moteur d'exécution Javascript doit être un processus d'exécution séquentiel de haut en bas lors de l'exécution du code ci-dessus, et la fonction setTimeout est exécutée avant l'instruction while. Mais en fait, après que le code ci-dessus ait été exécuté plusieurs fois, la sortie est retardée d'au moins 1 000 ms.
Rappelant mon expérience passée dans l'apprentissage de Java, le setTimeout() de Javascript mentionné ci-dessus m'a dérouté. Java a plusieurs implémentations d'API pour setTimeout. Nous prenons ici le package java.util.Timer comme exemple. Utilisez Timer pour implémenter la logique ci-dessus en Java. Après plusieurs exécutions, le résultat est Temps écoulé : 501 ms.
import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { public static void main(String[] args) { // TODO Auto-generated method stub long start = System.currentTimeMillis(); Timer timer = new Timer(); timer.schedule(new MyTask(start), 500); while (System.currentTimeMillis() - start < 1000) {}; } } class MyTask extends TimerTask { private long t; public MyTask(long start) { // TODO Auto-generated constructor stub t=start; } @Override public void run() { // TODO Auto-generated method stub long end = System.currentTimeMillis(); System.out.println("Time elapsed:"+(end - this.t)+ "ms"); } }
Avant d'expliquer pourquoi cette différence se produit dans setTimeout(), parlons d'abord du principe d'implémentation de java.util.Timer.
Les éléments clés du code ci-dessus sont les classes Timer, TimerTask et la méthode planning de la classe Timer. Vous pouvez comprendre son implémentation en lisant le code source correspondant.
Timer : classe de planification d'une tâche Task, comme la tâche TimerTask, il s'agit d'une classe API permettant aux utilisateurs d'organiser le plan d'exécution de la tâche via la méthode de planification. Cette classe effectue la planification des tâches via la file d'attente des tâches TaskQueue et la classe TimerThread.
TimerTask : implémente l'interface Runnable, indiquant que chaque tâche est un thread indépendant et permet aux utilisateurs de personnaliser leurs propres tâches via la méthode run().
TimerThread : Hérité de Thread, c'est la classe qui exécute réellement la tâche.
TaskQueue : une structure de données qui stocke les tâches de tâche. Elle est implémentée en interne par un tas minimum. Chaque membre du tas est une TimeTask. Chaque tâche est triée par la valeur de l'attribut nextExecutionTime de TimerTask. de la file d'attente, de sorte que le premier implémente.
Après avoir examiné l'implémentation de setTimeout() par Java.util.Timer, revenons à la méthode setTimeout() de Javascript et voyons pourquoi la sortie précédente ne correspond pas aux attentes.
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
En lisant le code, il n'est pas difficile de voir que la méthode setTimeout() est exécutée avant la boucle while(). Elle déclare qu'elle "espère" exécuter la fonction anonyme une fois après 500 ms. de la fonction anonyme, se trouve dans la méthode setTimeout(). Elle prend effet immédiatement après l'exécution. La boucle while dans la dernière ligne du code continuera à s'exécuter pendant 1 000 ms. Le temps de retard de la sortie de la fonction anonyme enregistrée via la méthode setTimeout() est toujours supérieur à 1 000 ms, indiquant que l'appel réel à cette fonction anonyme est bloqué par la boucle while() L'appel réel est dans La boucle while() est en fait exécutée après le blocage.
Dans Java.util.Timer, la solution aux tâches planifiées est implémentée via le multi-threading. L'objet de tâche est stocké dans la file d'attente des tâches et un thread de planification dédié termine l'exécution de la tâche dans un nouveau sous-thread. Lors de l'enregistrement d'une tâche asynchrone via la méthode planning(), le thread de planification commence immédiatement à travailler dans le thread enfant et le thread principal ne bloque pas l'exécution de la tâche.
Il s'agit d'une différence majeure entre Javascript et les langages tels que Java/C#, c'est-à-dire le mécanisme monothread de Javascript. Dans l'environnement de navigateur existant, le moteur d'exécution Javascript est monothread. Les instructions et méthodes du thread principal bloqueront l'exécution des tâches planifiées. Le moteur d'exécution n'exécutera les tâches planifiées qu'après avoir exécuté les instructions du thread principal. is La période de temps peut être supérieure au délai défini lors de l'enregistrement de la tâche. À ce stade, les mécanismes de Javascript et de Java/C# sont très différents.
Comment fonctionne setTimeout() dans un moteur Javascript monothread ? Nous mentionnerons ici le modèle de boucle d'événements dans le noyau du navigateur. Pour faire simple, en dehors du moteur d'exécution Javascript, il existe une file d'attente de tâches. Lorsque la méthode setTimeout() est appelée dans le code, la méthode de délai enregistrée sera transmise aux autres modules du noyau du navigateur (en prenant webkit comme support). Par exemple, il s'agit du module webcore) Traitement, lorsque la méthode de retard atteint la condition de déclenchement, c'est-à-dire lorsque le temps de retard défini est atteint, cette méthode de retard est ajoutée à la file d'attente des tâches. Ce processus est géré par d'autres modules du noyau du navigateur et est indépendant du thread principal du moteur d'exécution. Une fois l'exécution de la méthode du thread principal terminée et le moteur d'exécution atteint l'état inactif, il obtiendra séquentiellement les tâches de la tâche. file d'attente pour l'exécution. Ce processus est une boucle continue. Le processus est appelé modèle de boucle d'événements.
En se référant aux informations contenues dans un discours, le modèle de boucle d'événements ci-dessus peut être décrit par la figure suivante.
Lorsque le thread principal du moteur d'exécution Javascript s'exécute, un tas et une pile sont générés. Le code du programme entre dans la pile un par un et attend son exécution. Lorsque la méthode setTimeout() est appelée, c'est-à-dire lorsque la méthode WebAPIs sur le côté droit de la figure est appelée, le module correspondant du noyau du navigateur démarre. traitement de la méthode de retard. Lorsque la méthode de retard atteint la condition de déclenchement, la méthode est ajoutée à la file d'attente des tâches pour le rappel, tant que le code dans la pile du moteur d'exécution est exécuté, le thread principal lira la file d'attente des tâches et exécutera ces rappels. fonctions qui remplissent les conditions de déclenchement en séquence.
Utilisez des exemples dans le discours pour illustrer davantage
En prenant le code de l'image comme exemple, lorsque le moteur d'exécution commence à exécuter le code ci-dessus, cela équivaut à ajouter une méthode main() à la pile d'exécution. Lorsque vous continuez à démarrer console.log('Hi'), la méthode log('Hi') est poussée sur la pile. La méthode console.log est une méthode courante prise en charge par le noyau du kit Web, et non la méthode impliquée dans les WebAPI dans le figure précédente, donc la méthode de connexion ici (« Salut ») est immédiatement retirée de la pile et exécutée par le moteur.
Une fois l'instruction console.log('Hi') exécutée, la méthode log() est extraite de la pile et Hi est affiché. Le moteur continue de fonctionner et ajoute setTimeout(callback,5000) à la pile d'exécution. La méthode setTimeout() appartient à la méthode des WebAPI dans le modèle de boucle d'événements. Lorsque le moteur extrait la méthode setTimeout() de la pile pour exécution, il transmet la fonction d'exécution retardée au module correspondant, c'est-à-dire le module timer. sur le côté droit de la figure pour le traitement.
Lorsque le moteur d'exécution extrait setTimeout de la pile pour exécution, il transmet la méthode de traitement des retards au module de minuterie du webkit, puis continue immédiatement à traiter le code suivant, donc log('SJS') est ajouté à la pile d'exécution, et puis log('SJS' ) est exécuté hors de la pile et SJS est affiché. Une fois que le moteur d'exécution a exécuté console.log('SJS'), le programme est traité et la méthode main() est également retirée de la pile.
À ce moment, 5 secondes après l'exécution de la méthode setTimeout, le module de minuterie détecte que la méthode de traitement du retard atteint la condition de déclenchement, il ajoute donc la méthode de traitement du retard à la file d'attente des tâches. À ce stade, la pile d'exécution du moteur d'exécution est vide, le moteur commence donc à interroger pour vérifier s'il y a des tâches dans la file d'attente des tâches qui doivent être exécutées. Il détecte que la méthode de retard a atteint la condition d'exécution, et ajoute donc. la méthode de retard à la pile d’exécution. Le moteur a découvert que la méthode delay appelait la méthode log(), il a donc poussé la méthode log() sur la pile. Ensuite, la pile d'exécution est extraite l'une après l'autre, sortie là-bas et efface la pile d'exécution.
Après avoir effacé la pile d'exécution, le moteur d'exécution continuera à interroger la file d'attente des tâches pour vérifier s'il reste des tâches pouvant être exécutées.
到这里已经可以彻底理解下面代码的执行流程,执行引擎先将setTimeout()方法入栈被执行,执行时将延时方法交给内核相应模块处理。引擎继续处理后面代码,while语句将引擎阻塞了1秒,而在这过程中,内核timer模块在0.5秒时已将延时方法添加到任务队列,在引擎执行栈清空后,引擎将延时方法入栈并处理,最终输出的时间超过预期设置的时间。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
前面事件循环模型图中提到的WebAPIs部分,提到了DOM事件,AJAX调用和setTimeout方法,图中简单的把它们总结为WebAPIs,而且他们同样都把回调函数添加到任务队列等待引擎执行。这是一个简化的描述,实际上浏览器内核对DOM事件、AJAX调用和setTimeout方法都有相应的模块来处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块,html的解析,css样式的计算等都由webcore实现。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现,这里还是继续以setTimeout为例,看下timer模块的实现。
Timer类是webkit 内核的一个必需的基础组件,通过阅读源码可以全面理解其原理,本文对其简化,分析其执行流程。
通过setTimeout()方法注册的延时方法,被传递给webcore组件timer模块处理。timer中关键类为TheadTimers类,其包含两个重要成员,TimerHeap任务队列和SharedTimer方法调度类。延时方法被封装为timer对象,存储在TimerHeap中。和Java.util.Timer任务队列一样,TimerHeap同样采用最小堆的数据结构,以nextFireTime作为关键字排序。SharedTimer作为TimerHeap调度类,在timer对象到达触发条件时,通过浏览器平台相关的接口,将延时方法添加到事件循环模型中提到的任务队列中。
TimerHeap采用最小堆的数据结构,预期延时时间最小的任务最先被执行,同时,预期延时时间相同的两个任务,其执行顺序是按照注册的先后顺序执行。
var start = new Date; setTimeout(function(){ console.log('fn1'); }, 20); setTimeout(function(){ console.log('fn2'); }, 30); setTimeout(function(){ console.log('another fn2'); }, 30); setTimeout(function(){ console.log('fn3'); }, 10); console.log('start while'); while (new Date - start < 1000) {}; console.log('end while');
上述代码输出依次为
start while end while fn3 fn1 fn2 another fn2
1.《Javascript异步编程》
2.JavaScript 运行机制详解:再谈Event Loophttp://www.ruanyifeng.com/blog/2014/10/event-loop.html
3.Philip Roberts: Help, I'm stuck in an event-loop.https://vimeo.com/96425312
4.How JavaScript Timers Work.http://ejohn.org/blog/how-javascript-timers-work/
5.How WebKit’s event model works.http://brrian.tumblr.com/post/13951629341/how-webkits-event-model-works
6.Timer实现.http://blog.csdn.net/shunzi__1984/article/details/6193023
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!