Rumah hujung hadapan web tutorial js 前端进阶(十二):详解事件循环机制

前端进阶(十二):详解事件循环机制

Apr 04, 2017 pm 05:59 PM

前端进阶(十二):详解事件循环机制

Event Loop

JavaScript的学习零散而庞杂,因此很多时候我们学到了一些东西,但是却没办法感受到自己的进步,甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直试图在寻找一条核心的线索,只要我根据这条线索,我就能够一点一点的进步。

前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。所以,我就马不停蹄的去深入的学习了事件循环机制,并总结出了这篇文章跟大家分享。

事件循环机制从整体上的告诉了我们所写的JavaScript代码的执行顺序。但是在我学习的过程中,找到的许多国内博客文章对于它的讲解浅尝辄止,不得其法,很多文章在图中画个圈就表示循环了,看了之后也没感觉明白了多少。但是他又如此重要,以致于当我们想要面试中高级岗位时,事件循环机制总是绕不开的话题。特别是ES6中正式加入了Promise对象之后,对于新标准中事件循环机制的理解就变得更加重要。这就很尴尬了。

最近有两篇比较火的文章也表达了这个问题的重要性。

这个前端面试在搞事
80% 应聘者都不及格的 JS 面试题

但是很遗憾的是,大神们告诉了大家这个知识点很重要,却并没有告诉大家为什么会这样。所以当我们在面试时遇到这样的问题时,就算你知道了结果,面试官再进一步问一下,我们依然懵逼。

在学习事件循环机制之前,我默认你已经懂得了如下概念,如果仍然有疑问,可以回过头去看看我以前的文章。

  • 执行上下文(Execution context)

  • 函数调用栈(call stack)

  • 队列数据结构(queue)

  • Promise(我会在下一篇文章专门总结Promise的详细使用与自定义封装)

因为chrome浏览器中新标准中的事件循环机制与nodejs几乎一样,因此此处就以整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate

OK,那我就先抛出结论,然后以例子与图示详细给大家演示事件循环机制。

  • 我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。

    当然新标准中的web worker涉及到了多线程,我对它了解也不多,这里就不讨论了。

  • JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。

前端进阶(十二):详解事件循环机制

队列数据结构

  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。

  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。

    // setTimeout中的回调函数才是进入任务队列的任务
    setTimeout(function() {
      console.log('xxxx');
    })
    Salin selepas log masuk
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。

// demo01  出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');
Salin selepas log masuk

首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。

前端进阶(十二):详解事件循环机制

首先script任务开始执行,全局上下文入栈

第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。

setTimeout(function() {
    console.log('timeout1');
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

宏任务timeout1进入setTimeout队列

第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。

因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。

前端进阶(十二):详解事件循环机制

promise1入栈执行,这时promise1被最先输出

前端进阶(十二):详解事件循环机制

resolve在for循环中入栈执行

前端进阶(十二):详解事件循环机制

构造函数执行完毕的过程中,resolve执行完毕出栈,promise2输出,promise1页出栈,then执行时,Promise任务then1进入对应队列

script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。

第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。

前端进阶(十二):详解事件循环机制

执行所有的微任务

第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。

前端进阶(十二):详解事件循环机制

微任务被清空

这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。

前端进阶(十二):详解事件循环机制

timeout1入栈执行

这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。

那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。

这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复制一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。

// demo02
console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
Salin selepas log masuk

这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。

第一步:宏任务script首先执行。全局入栈。glob1输出。

前端进阶(十二):详解事件循环机制

script首先执行

第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

timeout1进入对应队列

第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

进入setImmediate队列

第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。

process.nextTick(function() {
    console.log('glob1_nextTick');
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

nextTick

第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。

new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

先是函数调用栈的变化

前端进阶(十二):详解事件循环机制

然后glob1_then任务进入队列

第六步:执行遇到第二个setTimeout。

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

timeout2进入对应队列

第七步:先后遇到nextTick与Promise

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

glob2_nextTick与Promise任务分别进入各自的队列

第八步:再次遇到setImmediate。

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
Salin selepas log masuk

前端进阶(十二):详解事件循环机制

nextTick

这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。

其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。

当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。

这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。

前端进阶(十二):详解事件循环机制

第二轮循环初始状态

setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。

只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。

当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。

大家需要注意这里的循环结束的时间节点。

当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。

OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。

当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。

// 用数组模拟一个队列
var tasks = [];

// 模拟一个事件分发器
var addFn1 = function(task) {
    tasks.push(task);
}

// 执行所有的任务
var flush = function() {
    tasks.map(function(task) {
        task();
    })
}

// 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中
setTimeout(function() {
    flush();
})

// 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法

var dispatch = function(name) {
    tasks.map(function(item) {
        if(item.name == name) {
            item.handler();
        }
    })
}

// 当然,我们把任务丢进去的时候,多保存一个name即可。
// 这时候,task的格式就如下
demoTask =  {
    name: 'demo',
    handler: function() {}
}

// 于是,一个订阅-通知的设计模式就这样轻松的被实现了
Salin selepas log masuk

这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级

因此,在老的浏览器没有支持Promise的时候,就可以利用setTimeout等方法,来模拟实现Promise,具体如何做到的,下一篇文章我们慢慢分析。


Atas ialah kandungan terperinci 前端进阶(十二):详解事件循环机制. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn

Alat AI Hot

Undresser.AI Undress

Undresser.AI Undress

Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover

AI Clothes Remover

Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool

Undress AI Tool

Gambar buka pakaian secara percuma

Clothoff.io

Clothoff.io

Penyingkiran pakaian AI

AI Hentai Generator

AI Hentai Generator

Menjana ai hentai secara percuma.

Artikel Panas

R.E.P.O. Kristal tenaga dijelaskan dan apa yang mereka lakukan (kristal kuning)
3 minggu yang lalu By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Tetapan grafik terbaik
3 minggu yang lalu By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Cara Memperbaiki Audio Jika anda tidak dapat mendengar sesiapa
3 minggu yang lalu By 尊渡假赌尊渡假赌尊渡假赌

Alat panas

Notepad++7.3.1

Notepad++7.3.1

Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina

SublimeText3 versi Cina

Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1

Hantar Studio 13.0.1

Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6

Dreamweaver CS6

Alat pembangunan web visual

SublimeText3 versi Mac

SublimeText3 versi Mac

Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Ganti aksara rentetan dalam javascript Ganti aksara rentetan dalam javascript Mar 11, 2025 am 12:07 AM

Penjelasan terperinci mengenai kaedah penggantian rentetan javascript dan Soalan Lazim Artikel ini akan meneroka dua cara untuk menggantikan watak rentetan dalam JavaScript: Kod JavaScript dalaman dan HTML dalaman untuk laman web. Ganti rentetan di dalam kod JavaScript Cara yang paling langsung ialah menggunakan kaedah pengganti (): str = str.replace ("cari", "ganti"); Kaedah ini hanya menggantikan perlawanan pertama. Untuk menggantikan semua perlawanan, gunakan ungkapan biasa dan tambahkan bendera global g: str = str.replace (/fi

Bina Aplikasi Web Ajax anda sendiri Bina Aplikasi Web Ajax anda sendiri Mar 09, 2025 am 12:11 AM

Jadi di sini anda, bersedia untuk mempelajari semua perkara ini yang dipanggil Ajax. Tetapi, apa sebenarnya? Istilah Ajax merujuk kepada kumpulan teknologi longgar yang digunakan untuk membuat kandungan web yang dinamik dan interaktif. Istilah Ajax, yang asalnya dicipta oleh Jesse J

Bagaimana saya membuat dan menerbitkan perpustakaan JavaScript saya sendiri? Bagaimana saya membuat dan menerbitkan perpustakaan JavaScript saya sendiri? Mar 18, 2025 pm 03:12 PM

Artikel membincangkan membuat, menerbitkan, dan mengekalkan perpustakaan JavaScript, memberi tumpuan kepada perancangan, pembangunan, ujian, dokumentasi, dan strategi promosi.

Bagaimanakah saya mengoptimumkan kod JavaScript untuk prestasi dalam penyemak imbas? Bagaimanakah saya mengoptimumkan kod JavaScript untuk prestasi dalam penyemak imbas? Mar 18, 2025 pm 03:14 PM

Artikel ini membincangkan strategi untuk mengoptimumkan prestasi JavaScript dalam pelayar, memberi tumpuan kepada mengurangkan masa pelaksanaan dan meminimumkan kesan pada kelajuan beban halaman.

Bagaimanakah saya boleh debug kod javascript dengan berkesan menggunakan alat pemaju pelayar? Bagaimanakah saya boleh debug kod javascript dengan berkesan menggunakan alat pemaju pelayar? Mar 18, 2025 pm 03:16 PM

Artikel ini membincangkan debugging JavaScript yang berkesan menggunakan alat pemaju pelayar, memberi tumpuan kepada menetapkan titik putus, menggunakan konsol, dan menganalisis prestasi.

kesan matriks jQuery kesan matriks jQuery Mar 10, 2025 am 12:52 AM

Bawa kesan filem matriks ke halaman anda! Ini adalah plugin jQuery yang sejuk berdasarkan filem terkenal "The Matrix". Plugin mensimulasikan kesan aksara hijau klasik dalam filem, dan hanya pilih gambar dan plugin akan mengubahnya menjadi gambar gaya matriks yang diisi dengan aksara angka. Datang dan cuba, sangat menarik! Bagaimana ia berfungsi Plugin memuat imej ke kanvas dan membaca nilai piksel dan warna: data = ctx.getimagedata (x, y, settings.grainsize, settings.grainsize) .data Plugin dengan bijak membaca kawasan segi empat tepat gambar dan menggunakan jQuery untuk mengira warna purata setiap kawasan. Kemudian, gunakan

Cara Membina Slider JQuery Mudah Cara Membina Slider JQuery Mudah Mar 11, 2025 am 12:19 AM

Artikel ini akan membimbing anda untuk membuat karusel gambar mudah menggunakan perpustakaan jQuery. Kami akan menggunakan perpustakaan BXSlider, yang dibina di atas jQuery dan menyediakan banyak pilihan konfigurasi untuk menubuhkan karusel. Pada masa kini, Gambar Carousel telah menjadi ciri yang mesti ada di laman web - satu gambar lebih baik daripada seribu perkataan! Selepas membuat keputusan untuk menggunakan karusel gambar, soalan seterusnya adalah bagaimana untuk menciptanya. Pertama, anda perlu mengumpul gambar-gambar resolusi tinggi yang berkualiti tinggi. Seterusnya, anda perlu membuat karusel gambar menggunakan HTML dan beberapa kod JavaScript. Terdapat banyak perpustakaan di web yang dapat membantu anda membuat karusel dengan cara yang berbeza. Kami akan menggunakan Perpustakaan BXSlider Sumber Terbuka. Perpustakaan BXSlider menyokong reka bentuk responsif, jadi karusel yang dibina dengan perpustakaan ini dapat disesuaikan dengan mana -mana

Cara memuat naik dan memuat turun fail CSV dengan sudut Cara memuat naik dan memuat turun fail CSV dengan sudut Mar 10, 2025 am 01:01 AM

Set data sangat penting dalam membina model API dan pelbagai proses perniagaan. Inilah sebabnya mengapa mengimport dan mengeksport CSV adalah fungsi yang sering diperlukan. Dalam tutorial ini, anda akan belajar cara memuat turun dan mengimport fail CSV dalam sudut

See all articles