JavaScript 关于事件循环机制的刨析

目录
  • 前言:
  • 一、事件循环和任务队列产生的原因:
  • 二、事件循环机制:
  • 三、任务队列:
    • 3.1 任务队列的类型:
    • 3.2 两者区别:
    • 3.3 更细致的事件循环过程
  • 四、强大的异步专家 process.nextTick()
    • 4.1 process.nextTick()在何时调用?

前言:

这次主要整理一下自己对 Js事件循环机制,同步,异步任务,宏任务,微任务的理解,大概率暂时还有些偏差或者错误。如果有,十分欢迎各位纠正我的错误!

一、事件循环和任务队列产生的原因:

首先,JS是单线程,这样设计也是具有合理性的,试想如果一边进行dom的删除,另一边又进行dom的添加,浏览器该如何处理?

引用:

单线程即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给相应的异步模块去处理,主线程的效率大大提升,可以并行的去处理其他的操作。当异步处理完成,主线程空闲时,主线程读取相应的callback,进行后续的操作,最大程度的利用CPU。此时出现了同步执行和异步执行的概念,同步执行是主线程按照顺序,串行执行任务;异步执行就是cpu跳过等待,先处理后续的任务(CPU与网络模块、timer等并行进行任务)。由此产生了任务队列与事件循环,来协调主线程与异步模块之间的工作。“”

二、事件循环机制:

图解:

首先把JS执行代码操作 分为主线程任务队列,任何一段js代码的执行都可以分为以下几个步骤:

步骤一: 主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;
步骤二: 当主线程遇到异步操作的时候,将异步操作交给对应的API进行处理;
步骤三: 当异步操作处理完成,推入任务队列中
步骤四: 主线程执行完毕后,查询任务队列,取出一个任务,并推入主线程进行处理
步骤五: 重复步骤二、三、四

其中常见的异步操作有:ajax请求,setTimeout,还有类似onclik事件等

三、任务队列:

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列

首先,顾名思义,既然是一个队列,那么就遵循FIFO原则

如上示意图,任务队列存在多个,它们的执行顺序:

同一任务队列内,按队列顺序被主线程取走;
不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O)

3.1 任务队列的类型:

任务队列分为 宏任务(macrotask queue)微任务(microtask queue)

宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

3.2 两者区别:

微任务microtask queue:

(1) 唯一,整个事件循环当中,仅存在一个;
(2) 执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;

PS:所以利用microtask queue可以形成一个同步执行的环境

宏任务macrotask queue:

(1) 不唯一,存在一定的优先级(用户I/O部分优先级更高)
(2) 异步执行,同一事件循环中,只执行一个

3.3 更细致的事件循环过程

  • 一、二、三、步同上
  • 主线程查询任务队列,执行microtask queue,将其按序执行,全部执行完毕;
  • 主线程查询任务队列,执行macrotask queue,取队首任务执行,执行完毕;
  • 重复四、五步骤;

先用一个简单的例子加深一下理解:

console.log('1, time = ' + new Date().toString()) // 1.进入主线程,执行同步任务,输出1
setTimeout(macroCallback, 0)// 2. 加入宏任务队列 // 7.开始执行此定时器宏任务,调用macroCallback,输出4
new Promise(function (resolve, reject) {//3.加入微任务队列
  console.log('2, time = ' + new Date().toString())//4.执行此微任务中的同步代码,输出2
  resolve()
  console.log('3, time = ' + new Date().toString())//5.输出3
}).then(microCallback)// 6.执行then微任务,调用microCallback,输出5

//函数定义
function macroCallback() {
  console.log('4, time = ' + new Date().toString())
}

function microCallback() {
  console.log('5, time = ' + new Date().toString())
}

运行结果:

四、强大的异步专家 process.nextTick()

第一次看见这东西,有点眼熟啊,想了一下好像之前vue项目中 用过 this.$nextTick(callback) 当时说的是 当页面上元素被重新渲染之后 才会执行回调函数中的代码
,不是很理解,暂时记住吧

4.1 process.nextTick()在何时调用?

任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析

在事件循环中,每进行一次循环操作称为tick,知道了这个之后,对理解这个方法什么时候调用瞬间明白了一些!

再借用别人的例子,加深一下对事件循环的理解吧:

var flag = false // 1. 变量声明

Promise.resolve().then(() => {
  // 2. 将 then 任务分发到本轮循环微任务队列中去
  console.log('then1') // 8. 执行 then 微任务, 打印 then1,flag 此时是 true 了
  flag = true
})
new Promise(resolve => {
  // 3. 执行 Promise 里 同步代码
  console.log('promise')
  resolve()
  setTimeout(() => { // 4. 将定时器里的任务放到宏任务队列中
    console.log('timeout2') // 11. 执行定时器宏任务 这边指定了 10 的等待时长, 因此在另一个定时器任务之后执行了
  }, 10)
}).then(function () {
  // 5. 将 then 任务分发到本轮循环微任务队列中去
  console.log('then2') // 9. 执行 then 微任务, 打印 then2,至此本轮 tick 结束
})
function f1(f) {
  // 1. 函数声明
  f()
}
function f2(f) {
  // 1. 函数声明
  setTimeout(f) //  7. 把`setTimeout`中的`f`放到宏任务队列中,等本轮`tick`执行完,下一次事件循环再执行
}
f1(() => console.log('f为:', flag ? '异步' : '同步')) // 6. 打印 `f为:同步`
f2(() => {
  console.log('timeout1,', 'f为:', flag ? '异步' : '同步') // 10. 执行定时器宏任务
})

console.log('本轮宏任务执行完') // 7. 打印

运行结果:

process.nextTick 中的回调是在当前tick执行完之后,下一个宏任务执行之前调用的。

官方的例子:

let bar;

// 这个方法用的是一个异步签名,但其实它是同步方式调用回调的
function someAsyncApiCall(callback) { callback(); }

// 回调函数在`someAsyncApiCall`完成之前被调用
someAsyncApiCall(() => {
  // 由于`someAsyncApiCall`已经完成,bar没有被分配任何值
  console.log('bar', bar); // undefined
});

bar = 1;

使用 process.nextTick:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

再看一个含有 process.nextTick的例子:

console.log('1'); // 1.压入主线程执行栈,输出1

setTimeout(function () { //2.它的回调函数被加入 宏任务队列中
	//7.目前微任务队列为空,所以取出 宏任务队列首项,执行此任务
    console.log('2'); // 输出2
    process.nextTick(function () { // 16.上一次循环结束,在下一次宏任务开始之前调用,输出3
        console.log('3');
    })
    new Promise(function (resolve) {
    	//8.执行 此promise的同步任务,输出4,状态变为resolve
        console.log('4');
        resolve();
    }).then(function () {//9.检测到异步方法then,将其回调函数加入 微任务队列中
        console.log('5'); // 10. 取出微任务队列首项,也就是这个then的回调,执行,输出5
    })
})

process.nextTick(function () { // 11.一次事件循环结束,执行nextTick()的回调,输出6
    console.log('6');
})
new Promise(function (resolve) {
	//3.执行promise中的同步任务 输出7,状态变为resolve
    console.log('7');
    resolve();
}).then(function () { //4.检测到异步方法then,将其回调函数加入 微任务队列中
    console.log('8'); //6. 主线程执行完毕,取出微任务队列中首项,将其回调函数压入执行栈,输出8
})

setTimeout(function () { //5.它的回调函数 加入 宏任务队列中
	//12.此刻,微任务队列为空,开始执行此宏任务
    console.log('9'); // 输出9
    process.nextTick(function () { // 17.此刻 微任务和宏任务队列都为空了,此次循环自动结束,执行此回调,输出10
        console.log('10');
    })
    new Promise(function (resolve) {
    	//13. 执行此promise的同步任务,输出11,状态改变
        console.log('11');
        resolve();
    }).then(function () {//14.检测到then异步方法,加入微任务队列
        console.log('12');//15.取出微任务队列首项,执行此then微任务,输出12
    })

})

运行结果:

此过程步骤详解:

  • 首先进入主线程,检测到log只是普通函数,压入执行栈,输出1;
  • 检测到setTimeout为特殊的异步方法(macrotask),将其交由其他内核模块处理,setTimeout的回调函数被放入宏任务(macrotask)队列中;
  • 检测到promise对象以及其中的resolve是一般的方法,将其同步任务压入执行栈,输出7,并且状态改变为ressolve;
  • 检测到刚才的promise对象的then方法是异步方法,将其交由其他内核模块处理,回调函数被放入微任务(microtask)队列中;
  • 又检测到一个setTimeout为特殊的异步方法,其回调函数被放入宏任务(macrotask)队列中;
  • 此时,主线程空了,开始从任务队列中取,取出 微任务队列首项,也就是第一个promise的then方法的回调,执行,输出8;
  • 检查此时微任务队列为空,取出宏任务队列首项,也就是第一个setTimeOut,执行其回调函数,输出2;
  • 在它的回调中碰到一个promise,执行其同步任务,输出4,状态改变;
  • 然后检测到then,同上,加入到微任务队列;
  • 取出微任务队列首项到主线程执行,也就是刚才的then,输出5;
  • 此次循环结束,在下一个宏任务开始之前,调用第一个process.nextTick()的回调,输出6;
  • 开始下一个宏任务,取出宏任务队列首项,也就是第二个setTimeout的回调,将其压入执行栈,输出9;
  • 然后将里面的promise对象的同步任务压入执行栈,输出11,状态改为resolve;
  • 这时又检测到异步then方法,同上,将其回调加入 微任务队列;
  • 取出微任务队列首项,也就是刚才的then回调,输出12;
  • 此次循环结束,在下一次宏任务开始之前执行,process.nextTick()的回调,输出3;
  • 此时发现 任务队列和主线程都空了,此次事件循环自动结束,执行最后一个process.nextTick()的回调,输出10;

结束!趁着灵光乍现的时候,噼里啪啦赶紧记录下来,后面再检查检查是否有问题,也欢迎各位指出我的错误。

再来分析一个简单的例子:

console.log('0');
setTimeout(() => {
    console.log('1');
    new Promise(function(resolve) {
        console.log('2');
        resolve();
    }).then(()=>{
        console.log('3');
    })
    new Promise(resolve => {
        console.log('4');
        for(let i=0;i<9;i++){
            i == 7 && resolve();
        }
        console.log('5');
    }).then(() => {
        console.log('6');
    })
})
  • 进入主线程,检测到log为普通函数,压入执行栈,输出0;
  • 检测到setTimeOut是特殊的异步方法,交给其他模块处理,其回调函数加入 宏任务(macrotask)队列;
  • 此时主线程中已经没有任务,开始从任务队列中取;
  • 发现为任务队列为空,则取出宏任务队列首项,也就是刚才的定时器的回调函数;
  • 执行其中的同步任务,输出1;
  • 检测到promise及其resolve方法是一般的方法,压入执行栈,输出2,状态改变为resolve;
  • 检测到这个promise的then方法是异步方法,将其回调函数加入 微任务队列;
  • 紧接着又检测到一个promise,执行其中的同步任务,输出4,5,状态改变为resolve;
  • 然后将它的then异步方法加入微任务队列;
  • 执行微任务队列首项,也就是第一个promise的then,输出3;
  • 再取出为任务队列首项,也就是第二个promise的then,输出6;
  • 此时主线程和任务队列都为空,执行完毕;

代码运行结果:

到此这篇关于JavaScript 关于事件循环机制的刨析的文章就介绍到这了,更多相关JavaScript 事件循环机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解JS浏览器事件循环机制

    先来明白些概念性内容. 进程.线程 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的. 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的. 浏览器内核 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种. 浏览器内核有多种线程在工作. GUI 渲染线程: 负责渲染页面,解析 HTML,C

  • 关于js的事件循环机制剖析

    前言 众所周知, JavaScript是单线程这一核心,可是浏览器又能很好的处理异步请求,那么到底是为什么呢?其中的原理与事件循环机制大有关系. 在探索事件循环之前,我们得先了解浏览器执行线程~~ 浏览器的渲染进程是多线程的,浏览器每一个tab标签都代表一个独立的进程,其中浏览器内核属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等.其包含的线程有以下几种 GUI 渲染线程:负责渲染页面,解析 HTML,CSS 构成 DOM 树: JS 引擎线程:解释执行代码.用户输入和网络请求:

  • JS事件循环机制event loop宏任务微任务原理解析

    首先看一段代码 async function (){ await f2() console.log('f1') } async function f2(){ console.log('f2') } console.log('正常1') f1() setTimeout(()=>{ console.log('定时器') }) console.log('正常2') 正确的打印顺序应该是:正常1,f2 ,正常2,f1,定时器 为什么会出现这样打印顺序呢 首先javascript是一门单线程语言,在最新的

  • 实例分析js事件循环机制

    本文通过实例给大家详细分析了JS中事件循环机制的原理和用法,以下是全部内容: var start = new Date() setTimeout(function () { var end = new Date console.log('Time elapsed:', end - start, 'ms') }, 500) while (new Date() - start < 1000) { } 有其他语言能完成预期的功能吗?Java, 在Java.util.Timer中,对于定时任务的解决方案

  • 详解JavaScript事件循环机制

    众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心.可看HTML规范中的这段话: To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There a

  • JavaScript 关于事件循环机制的刨析

    目录 前言: 一.事件循环和任务队列产生的原因: 二.事件循环机制: 三.任务队列: 3.1 任务队列的类型: 3.2 两者区别: 3.3 更细致的事件循环过程 四.强大的异步专家 process.nextTick() 4.1 process.nextTick()在何时调用? 前言: 这次主要整理一下自己对 Js事件循环机制,同步,异步任务,宏任务,微任务的理解,大概率暂时还有些偏差或者错误.如果有,十分欢迎各位纠正我的错误! 一.事件循环和任务队列产生的原因: 首先,JS是单线程,这样设计也是

  • 深入了解Javascript的事件循环机制

    目录 单线程的Javascript 同步 vs 异步 宏任务 vs 微任务 定时器 To Be Continued 单线程的Javascript JavaScript是一种单线程语言,它主要用来与用户互动,以及操作DOM.多线程需要共享资源.且有可能修改彼此的运行结果,且存在上下文切换. 在 JS 运行的时候可能会阻止 UI 渲染,这说明两个线程是互斥的.这是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI. JS 是单线程运行的,可以达

  • 简单聊聊JavaScript的事件循环机制

    目录 前言 概念 举个栗子 TIP 再次举个栗子 总结 前言 JavaScript是一门单线程的弱类型语言,但是我们在开发中,经常会遇到一些需要异步或者等待的处理操作. 类似ajax,亦或者ES6中新增的promise操作用于处理一些回调函数等. 概念 在JavaScript代码执行过程中,可以分为同步队列和异步队列. 同步任务类似我们常说的立即执行函数,不需要等待可以直接进行,可以直接进入到主线程中去执行,类似正常的函数调用等. 异步队列则是异步执行函数,类似ajax请求,我们在发起的过程中,

  • JavaScript中事件冒泡机制示例详析

    什么是冒泡? DOM事件流(event  flow )存在三个阶段:事件捕获阶段. 处于目标阶段. 事件冒泡阶段. 事件捕获(event  capturing):通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件. 事件冒泡(dubbed  bubbling):与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点. dom标准事件流的触发的先后顺序为:先捕

  • JavaScript 的setTimeout与事件循环机制event-loop

    目录 1.先说说我们都知道的setTimeout 2.再讲讲我们可能不知道的setTimeout event-loop 3. node中的时间循环执行顺序 4. 关于事件循环中的promise 1.先说说我们都知道的setTimeout setTimeout在我们写代码中会经常用到,不管是前端还是服务端,目的是延迟执行. setTimeout(() => { console.log('延迟执行'); },1000); 貌似没什么可讲的. 2.再讲讲我们可能不知道的setTimeout 我们可能遇

  • 深入理解JavaScript的事件执行机制

    目录 前言 浏览器 JS 异步执行的原理 浏览器中的事件循环 执行栈与任务队列 宏任务和微任务 Async/await的运行顺序 特点 示例 个人分析 前言 熟悉事件循环,了解浏览器运行机制将对我们理解 JavaScript 的执行过程和排查运行问题有很大帮助.以下是总结的一些浏览器事件循环的一些原理和示例. 浏览器 JS 异步执行的原理 JS 是单线程的,也就是同一个时刻只能做一件事情,那么,为什么浏览器可以同时执行异步任务呢? 因为浏览器是多线程的,当 JS 需要执行异步任务时,浏览器会另外

随机推荐