Maison  >  Article  >  interface Web  >  Résumé des différences entre les boucles d'événements de navigateur et de nœud (Event Loop)

Résumé des différences entre les boucles d'événements de navigateur et de nœud (Event Loop)

不言
不言avant
2019-01-15 09:35:122405parcourir

Cet article vous présente un résumé des différences entre les navigateurs et les boucles d'événements de Node (Event Loop). Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il vous sera utile.

Dans cet article, nous présenterons le principe de l'implémentation asynchrone dans JS et comprendrons qu'Event Loop est en fait différent dans les navigateurs et Node.

1. Threads et processus

1 Concept

On dit souvent que JS est exécuté dans un seul thread, ce qui signifie qu'il n'y a qu'un seul thread principal dans un processus, alors qu'est-ce qu'un thread exactement ? Qu'est-ce qu'un processus ?

La déclaration officielle est la suivante : Le processus est la plus petite unité d'allocation de ressources CPU ; le thread est la plus petite unité de planification du CPU. Ces deux phrases ne sont pas faciles à comprendre. Regardons d'abord l'image :

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

  • Le processus est comme l'usine de L'image. Avoir des ressources d'usine séparées et exclusives.

  • Les fils de discussion sont comme les ouvriers sur la photo. Plusieurs ouvriers travaillent en collaboration dans une usine. La relation entre l'usine et les ouvriers est 1:n. C'est-à-direUn processus est constitué d'un ou plusieurs threads, et les threads sont différentes routes d'exécution de code dans un processus;

  • L'espace de l'usine est partagé by Workers , ce qui symbolise que l'espace mémoire d'un processus est partagé, et chaque thread peut utiliser ces mémoires partagées .

  • Plusieurs usines existent indépendamment.

2. Multi-processus et multi-thread

  • Multi-processus : en même temps, si deux processus ou plus sont autorisés dans le même système informatique, plus de deux processus sont en cours d'exécution. Les avantages de plusieurs processus sont évidents. Par exemple, vous pouvez ouvrir un éditeur et saisir du code tout en écoutant de la musique, et les processus de l'éditeur et du logiciel d'écoute de musique n'interféreront pas du tout.

  • Multi-threading : le programme contient plusieurs flux d'exécution, c'est-à-dire que plusieurs threads différents peuvent être exécutés simultanément dans un programme pour effectuer différentes tâches, ce qui signifie qu'un seul programme est autorisé à créer plusieurs threads s'exécutant en parallèle pour accomplir leurs tâches respectives.

Prenons le navigateur Chrome comme exemple. Lorsque vous ouvrez une page à onglets, vous créez en fait un processus. Il peut y avoir plusieurs threads dans un processus (plus de détails ci-dessous). fil de rendu, fil de moteur JS, fil de requête HTTP, etc. Lorsque vous lancez une requête, vous créez en fait un thread. Lorsque la requête se termine, le thread peut être détruit.

2. Noyau du navigateur

En termes simples, le noyau du navigateur obtient le contenu de la page, organise les informations (en appliquant CSS), calcule et combine le résultat final des résultats d'image visuelle, ce qui est généralement également demandé. le moteur de rendu.

Le noyau du navigateur est multithread. Sous le contrôle du noyau, chaque thread coopère les uns avec les autres pour maintenir la synchronisation. Un navigateur se compose généralement des threads résidents suivants :

  • .

    Thème de rendu GUI

  • Thème du moteur JavaScript

  • Thème de déclenchement chronométré

  • Déclencheur d'événement fil

  • Fil de requête http asynchrone

1. Fil de rendu de l'interface graphique

  • Principalement responsable de la page. rendu et analyse HTML, CSS, création d'une arborescence DOM, mise en page et dessin, etc.

  • Ce fil sera exécuté lorsque l'interface doit être redessinée ou lorsqu'une redistribution est provoquée par une opération.

  • Ce thread s'exclut mutuellement avec le thread du moteur JS. Lorsque le thread du moteur JS est exécuté, le rendu de l'interface graphique sera suspendu. Lorsque la file d'attente des tâches est inactive, le moteur JS exécutera l'interface graphique. rendu.

2. Thread du moteur JS

  • Ce thread est bien entendu principalement responsable du traitement des scripts JavaScript et de l'exécution du code.

  • est également principalement responsable de l'exécution des événements qui sont prêts à être exécutés, c'est-à-dire que lorsque le décompte du minuteur se termine ou que la requête asynchrone réussit et revient correctement, elle entrera dans la file d'attente des tâches à son tour et attendez l'exécution du thread du moteur JS.

  • Bien sûr, ce fil s'exclut mutuellement avec le fil de rendu de l'interface graphique. Lorsque le fil du moteur JS exécute des scripts JavaScript pendant trop longtemps, le rendu des pages sera bloqué.

3. Thread de déclenchement du minuteur

  • Un thread responsable de l'exécution de fonctions telles que des minuteries asynchrones, telles que : setTimeout, setInterval.

  • Lorsque le thread principal exécute le code en séquence, lorsqu'il rencontre un minuteur, il remet le minuteur au thread pour traitement. Lorsque le comptage est terminé, le thread déclencheur d'événement. ajoutera les événements comptés. Allez à la fin de la file d'attente des tâches et attendez que le thread du moteur JS s'exécute.

4. Le thread de déclenchement d'événements

  • est principalement responsable de la transmission des événements préparés au thread du moteur JS pour exécution.

Par exemple, lorsque le décompte du minuteur setTimeout se termine, les requêtes asynchrones telles que ajax réussissent et déclenchent la fonction de rappel, ou lorsque l'utilisateur déclenche un événement de clic, le fil ajoutera les événements prêts à envoyer à son tour dans la file d'attente des tâches. A la fin, attendez l'exécution du thread du moteur JS.

5. Fil de requête http asynchrone

  • Un fil chargé d'exécuter des fonctions telles que des requêtes asynchrones, telles que : Promise, axios, ajax, etc.

  • Lorsque le thread principal exécute le code en séquence et rencontre une requête asynchrone, la fonction sera transmise au thread pour traitement lorsque les changements de code d'état sont surveillés, s'il existe une fonction de rappel. , le thread déclencheur d'événement ajoutera la fonction de rappel. Accédez à la fin de la file d'attente des tâches et attendez que le thread du moteur JS s'exécute.

3. Boucle d'événement dans le navigateur

1 Micro-tâche et macro-tâche

Il existe deux types de files d'attente asynchrones dans l'événement. boucle : file d'attente macro (macro-tâche) et file d'attente micro (micro-tâche). Il peut y avoir plusieurs files d'attente de macro-tâches, mais une seule file d'attente de micro-tâches.

  • Macro-tâches courantes telles que : setTimeout, setInterval, setImmediate, script (code global), opérations d'E/S, rendu de l'interface utilisateur, etc.

  • Micro-tâches courantes telles que : process.nextTick, new Promise().then(callback), MutationObserver (nouvelle fonctionnalité html5), etc.

2. Analyse du processus de boucle d'événement

Un processus complet de boucle d'événement peut être résumé dans les étapes suivantes :

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

  • La pile d'exécution est vide au début Nous pouvons considérer la pile d'exécution comme une structure de pile qui stocke les appels de fonction, en suivant le premier entré, le dernier. hors principe . La micro-file d'attente est vide et il n'y a qu'un seul script (le code global) dans la macro-file d'attente.

  • Le contexte global (balise de script) est poussé dans la pile d'exécution pour synchroniser l'exécution du code. Au cours du processus d'exécution, il sera jugé s'il s'agit d'une tâche synchrone ou asynchrone. En appelant certaines interfaces, de nouvelles macro-tâches et micro-tâches peuvent être générées, et elles seront poussées dans leurs files d'attente de tâches respectives. Une fois le code de synchronisation exécuté, le script sera supprimé de la file d'attente des macros. Ce processus est essentiellement le processus d'exécution et de retrait de la macro-tâche dans la file d'attente.

  • Ce que nous avons retiré de la file d'attente à l'étape précédente était une macro-tâche, et dans cette étape, nous avons affaire à une micro-tâche. Mais il convient de noter que lorsque la macro-tâche est retirée de la file d'attente, les tâches sont exécutées une par une tandis que lorsque la micro-tâche est retirée de la file d'attente, les tâches sont exécutées une équipe à la fois . Par conséquent, lorsque nous traiterons la micro-file d'attente, nous exécuterons les tâches de la file d'attente une par une et les retirerons de la file d'attente jusqu'à ce que la file d'attente soit vidée.

  • Effectuer les opérations de rendu et mettre à jour l'interface

  • Vérifier s'il existe une tâche de travail Web, et si oui, effectuez-le Traitement

  • Le processus ci-dessus se répète en boucle jusqu'à ce que les deux files d'attente soient vidées

Résumons, chaque cycle est un processus comme celui-ci :

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

Lorsqu'une macro-tâche est exécutée, elle vérifiera s'il existe une file d'attente de micro-tâches. S'il y en a, toutes les tâches de la file d'attente des microtâches seront exécutées en premier. Sinon, la tâche principale de la file d'attente des macrotâches sera lue lors de l'exécution de la macrotâche, si une microtâche est rencontrée, elle sera ajoutée à la file d'attente des microtâches. à son tour. Une fois la pile vide, relisez les tâches dans la file d'attente des microtâches, et ainsi de suite.

Regardons ensuite un exemple pour présenter le processus ci-dessus :

Promise.resolve().then(()=>{
  console.log('Promise1')
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')
  })
},0)

Le résultat final est Promise1, setTimeout1, Promise2, setTimeout2

  • Une fois la tâche de synchronisation d'exécution de la pile au début (il s'agit d'une tâche macro) terminée, il vérifiera s'il existe une file d'attente de microtâches, qui existe dans la question ci-dessus (il y en a et une seule), et puis exécutez toutes les tâches dans la file d'attente des microtâches et affichez Promise1. En même temps, une macro-tâche setTimeout2

  • sera générée, puis vérifiera la macro-tâche setTimeout1. exécuté avant setTimeout2, et la macro-tâche setTimeout1 sera générée et placée dans la file d'attente des microtâches. toutes les tâches de la file d'attente des microtâches et sortie Promise2

  • Effacer Après toutes les tâches de la file d'attente des microtâches, il ira dans la file d'attente des macrotâches pour en obtenir une Cette fois, setTimeout2

    .
  • est exécuté. 4. Boucle d'événement dans Node

  • 1. Introduction à Node

La boucle d'événement dans Node est complètement différente de celle du navigateur. Node.js utilise V8 comme moteur d'analyse de js et utilise sa propre libuv conçue pour le traitement des E/S. libuv est une couche d'abstraction multiplateforme basée sur les événements qui encapsule certaines fonctionnalités sous-jacentes de différents systèmes d'exploitation et fournit une API unifiée pour. le monde extérieur. , le mécanisme de boucle d'événements y est également implémenté (sera présenté en détail ci-dessous).

Le mécanisme de fonctionnement de Node.js est le suivant : Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

Le moteur V8 analyse les scripts JavaScript .

  • Le code analysé appelle l'API Node.

  • La bibliothèque libuv est responsable de l'exécution de l'API Node. Il alloue différentes tâches à différents threads pour former une boucle d'événements et renvoie les résultats d'exécution des tâches au moteur V8 de manière asynchrone.

  • V8 引擎再将结果返回给用户。

2. 六个阶段

其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

从上图中,大致看出 node 中的事件循环的顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

  • timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调

  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调

  • idle, prepare 阶段:仅 node 内部使用

  • poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里

  • check 阶段:执行 setImmediate() 的回调

  • close callbacks 阶段:执行 socket 的 close 事件回调

注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)

接下去我们详细介绍timerspollcheck这 3 个阶段,因为日常开发中的绝大部分异步任务都是在这 3 个阶段处理的。

(1) timer

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。
同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

(2) poll

poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情

  • 回到 timer 阶段执行回调

  • 执行 I/O 回调

并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

  • 如果 poll 队列为空时,会有两件事发生

    • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

(3) check 阶段

setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。

我们先来看个例子:

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
  • 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3

  • 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差异,下文还会详细介绍)。

3. 注意点

(1) setTimeout 和 setImmediate

二者非常相似,区别主要在于调用时机不同。

  • setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;

  • setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行

setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
  • 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
    进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

  • 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

(2) process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

五、Node 与浏览器的 Event Loop 差异

浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

接下我们通过一个例子来说明两者区别:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

浏览器端运行结果:timer1=>promise1=>timer2=>promise2

浏览器端的处理过程如下:

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

Node 端运行结果:timer1=>timer2=>promise1=>promise2

  • 全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;

  • 首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;

  • 至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2

Node 端的处理过程如下:

Résumé des différences entre les boucles dévénements de navigateur et de nœud (Event Loop)

六、总结

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

  • Node 端,microtask 在事件循环的各个阶段之间执行

  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

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!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer
Article précédent:qu'est-ce que jqueryArticle suivant:qu'est-ce que jquery