庖丁解牛之浏览器事件环

浏览器事件环

用实例和知识点描述带您清晰的了解浏览器事件环的每一步;

栈和队列

在计算机内存中存取数据, 基本的数据结构分为栈和队列

  • 栈(Stack)是一种后进先出的数据结构; 栈的特点是 操作只在一端进行, 一般来说, 栈的操作只有两种: 进栈和出栈; 第一个进栈的数据总是最后一个才出来

  • 队列(Queue)和栈类似, 但是它是先进先出的数据结构,它的特点是 操作在队列两端进行, 从一端进入再从另一端出来; 先进入(从A端)的总是先出来(从B端)

名称进出特点端的数量
后进先出进出都在同一端
队列先进先出进出是在不同端
  • 队列好比一条隧道, (车)从隧道的一端(入口)进入, 从隧道的另一端(出口)出来
	// 队列执行时按照放置的顺序依次执行
	setTimeout(function(){
	    console.log(1)
	});

	setTimeout(function(){
	    console.log(2)
	});

	setTimeout(function(){
	    console.log(3)
	});
	
	// => 1 2 3

复制代码
  • 栈好比楼梯, 上楼时第一个踩的楼梯也就是下楼时最后踩的一个楼梯
// 在JavaScript中函数的执行就是一个典型的入栈与出栈的过程

	function a(){
	    console.log('a')
	    function b(){
	        console.log('b');
	        function c(){
	            console.log('c');
	        }
	        c();
	    }
	    b();
	}

	a();
	// => a b c
	// 函数调用顺序是a b c, 而作用域销毁的过程依次是c b a
复制代码

单线程和异步

JavaScript是单线程的, 这里所谓的单线程指的是主线程是单线程;

  • 为什么不是多线程呢? JavaScript最初设计是运行在浏览器中的, 假定是多线程, 有多个线程同时操作DOM, 岂不很混乱! 那会以哪个为准呢?

  • JavaScript为单线程, 在一个线程中代码会一行一行往下走,直到程序执行完毕; 若执行期间遇到较为费时的操作, 那只能等待了;

  • 单线程的设计使得语言的执行效率变差, 为了利用多核CPU的性能,javascript语言支持异步代码; 当有较为费时的操作时, 可将任务写为异步; 主线程在执行过程中遇到异步代码, 会先将该异步任务挂起, 继续执行后面的同步代码, 待同步执行完毕再回过头来, 检查是否有异步任务, 如果有异步任务就执行它;

PS: Java君加班有点累, 他想烧水冲一杯咖啡, 如果采用同步执行方式,那他就傻傻地等待,等水开了再冲咖啡;

PS: Java君加班有点累, 他想烧水冲一杯咖啡, 如果采用异步执行方式,那么他在等待水烧开之前,他可以听听歌,刷刷抖音啥的,等水开了再冲咖啡;

(-很明显异步的方式效率会高一些);

JavaScript是怎么执行的

JavaScript代码是在栈里执行的, 不论是同步还是异步; 代码分为同步代码和异步代码, 异步代码又分为: {宏任务} 和 [微任务]

JavaScript是解释型语言,它的执行过程是这样的:

  1. 从上到下依次解释每一条js语句
  2. 若是同步任务, 则将其压入一个栈(主线程); 如果是异步任务,就放到一个任务队列里面;
  3. 开始执行栈里面的同步任务,直到将栈里的所有任务都走完, 此时栈被清空;
  4. 回头检查异步队列,如果有异步任务完成了,就生成一个事件并注册回调(将异步的回调放到队列里面), 再将回调函数取出压入栈中执行;
  5. 栈中的异步回调执行完成后再去检查,直到异步队列都清空,程序运行结束

从以上步骤可以看出,不论同步还是异步, 都是在栈里执行的, 栈里的任务执行完成后一遍又一遍地回头检查队列,这种方式就是所谓的"事件环"

事件队列

	// 先看个demo吧
	console.log('start');
	
	setTimeout(()=>{
	    console.log('hello');
	}, 1000);
	
	console.log('end');
	// start end hello 上面代码执行后, 输出'start' 'end', 大约1s之后输出'hello'
	// ? 为什么'hello'不在end之前输出呢
复制代码
  • 解析
    1. setTimeout是一个异步函数, 也就是说当我们设置一个延迟函数的时候, setTimeout异步函数并不会阻塞代码执行, 程序还是会往下执行; 与此同时,它会在浏览事件列表中进行标记;
    2. 当延迟时间结束之后(准确说应该是当异步完成后), 事件列表会将标记的异步函数【异步函数的回调函数】添加到事件队列(Task Queue)中
    3. 当主栈中的代码执行完毕, 栈为空时, JS引擎便检查事件队列, 如果不为空的话,事件队列便将第一个任务压入执行栈中运行;
	console.log('start');
	setTimeout(() => {
	    console.log('hello');
	},0);
	
	console.log('end');
	// start end hello
	// 将上例微微调整,发现输出结果还是一样的 
	// 因为setTimeout的回调函数只是会被添加到(事件)队列中,而不会立即执行。 再回头
复制代码
  • 解析:
    1. 因为setTimeout是异步函数, 首先它会被(事件列表)标记(即挂起);
    2. setTimeout的延迟时间0并非真正是0, 在浏览器应该是4ms;
    3. 延迟时间到达(即异步任务完成),setTimeout的回调会被放入事件队列(静静地等待主栈中的同步代码执行);
    4. 当执行栈(即主栈)中的任务(同步任务)执行完毕, 执行栈为空; // 输出了 start end
    5. 执行栈为空后, 回头检查事件队列, (发现队列里面有任务[函数]待执行)将队列中注册的任务(即:异步函数完成后的回调函数)压入执行栈执行; // 输出 hello
	let promise = new Promise(function(resolve, reject) {
	    console.log('Promise');
	    resolve('Sucess');
	});
	  
	promise.then((data)=>{
	    console.log(data);
	});
	  
	console.log('Hello World!');
	// 'Promise' 'Hello World!' 'Sucess'
复制代码
  • 解析:
    1. new Promise()实例时的函数参数(执行器excutor)会立即执行; // 输出 'Promise'
    2. promise.then是异步函数, 它会被先放入事件队列;
    3. 同步任务console.log('Hello World!');执行完毕后主栈被清空 // 输出'Hello World!'
    4. 回头检查事件队列,发现队列里面有任务, 将其压入主栈执行; // 输出'Sucess'

微任务与宏任务

之前说到,异步任务又分为: 宏任务和微任务, 那他们是怎样执行的呢?

  • 在浏览器的执行环境中,总是先执行小的,再执行大的; 也就是说先执行微任务再执行宏任务;
  • 宏任务有: setImmediate(IE) > setTimeout setInterval
  • 微任务有: promise.then > MutationObserver > MessageChannel
  • 任务队列中,在每一次事件循环中,宏任务只会提取一个执行, 而微任务会一直提取,直到微任务队列为空为止;
  • 如果某个微任务被推入到执行栈中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止;
  • 事件循环每次只会入栈一个宏任务,主线程执行完成该任务后又会检查微任务队列,并完成里面所有的任务后再执行宏任务

记忆
  • js代码执行顺序:同步代码会先于异步代码; 异步任务的微任务会比异步任务宏任务先执行
  • js代码默认先执行主栈中的代码,主栈中的任务执行完后, 开始执行清空微任务操作
  • 清空微任务执行完毕,取出第一个宏任务到主栈中执行
  • 第一个宏任务执行完后,如果有微任务会再次去执行清空微任务操作,之后再去取宏任务

上述步骤就形成事件环

	// 查看setTimeout和Promise.then的不同
	console.log(1);
	setTimeout(()=>{
	    console.log(2);
	    Promise.resolve().then(()=>{
	        console.log(6);
	    });
	}, 0);
	  
	Promise.resolve(3).then((data)=>{
	    console.log(data);  	// 3
	    return data + 1;
	}).then((data)=>{
	    console.log(data)		// 4
	    setTimeout(()=>{
	        console.log(data+1)	// 5
	        return data + 1;
	    }, 1000)
	}).then((data)=>{
	    console.log(data);		// 上一个then没有任何返回值, 所以为undefined
	});

	// 1  3  4 undefined 2 6 5
复制代码
  • 解析:
    1. 主栈开始执行, 遇到同步代码console.log(1);,将其执行, 输出 1
    2. 主栈继续往下执行, 遇到异步函数setTimeout(()=>{ console.log(2); }, 0), 将其放入宏任务队列,此时宏任务队列:[s1]
    3. 主栈继续往下执行, 遇到异步函数promise.then将其放入微任务队列, 此时微任务队列[p1(打印3,返回3+1)]
    4. 主栈继续往下执行, 遇到异步函数promise.then将其放入微任务队列, 此时微任务队列[p1, p2(打印4)]
    5. 主栈继续往下执行, 遇到异步函数promise.then将其放入微任务队列, 此时微任务队列[p1, p2, p3]
    6. 主栈的同步代码执行完毕后, 栈里面的任务已空, 回头检查发现有宏任务队列[s1]、微任务队列[p1, p2, p3]
    7. 清空微任务队列(即微任务队列中的任务挨个的执行,直到全部执行完毕为止) 清空微任务流程
    8. 把微任务队列里面的p1拿到主栈执行; // 输出 3, 将data + 1(4)作为下一个then的成功值返回
    9. 把微任务队列里面的p2拿到主栈执行; // 输出 4
    10. 在执行p2时遇到了setTimeout(()=>{ console.log(data+1); return data + 1; }),将其放入宏任务队列(先标记,1s后异步执行完成后再将异步函数的回调放入队列), 此时宏任务队列:[s1,s2]
    11. 主栈继续往下执行, 把微任务队列里面的p3拿到主栈执行, 因为上一个then未显示的返回任何值, 因此data为undefined, 执行完毕后输出 undefined
    12. 主栈继续往下执行, 发现微任务队列已被清空, 此时提取宏任务队列中的第一个s1放到主栈里面执行, 执行后输出 2
    13. s1在输出2之后, 遇到了异步函数promise.then, 将其放入微任务队列, 此时微任务队列[p4]
    14. 第一个宏任务执行完毕后, 发现微任务队列有任务p4, 再去执行清空微任务操作
    15. 把微任务队列里面的p4拿到主栈执行; // 输出 6
    16. 主栈继续往下执行, 发现微任务队列已被清空, 此时提取宏任务队列中的第一个s2放到主栈里面执行, 执行后输出 5

浏览器中的事件环

  1. 所有同步任务都在主线程上执行,形成一个执行栈;
  2. 主线程之外,还存在一个任务队列; 只要异步任务有了运行结果,就在任务队列中放置一个事件(任务);
  3. 一旦执行栈中的所有同步任务执行完毕, 系统就会读取任务队列,将队列中的事件放到执行栈中依次执行;
  4. 主线程从任务队列中读取事件,这个过程是循环不断的 整个这种运行机制又被称为Event Loop(事件循环)

面试题分析

	setTimeout(()=>{
	    console.log(1);
	    Promise.resolve().then(data => {
	        console.log(2);
	    });
	}, 0);
	
	
	Promise.resolve().then(data=>{
	    console.log(3);
	    setTimeout(()=>{
	        console.log(4)
	    }, 0);
	});
	
	console.log('start');

	// start -> 3  1  2  4
	
	// 给方法分类: 宏任务  微任务
	// 宏任务: setTimeout
	// 微任务: then
/*
	// 执行顺序: 微任务会先执行
	// 默认先执行主栈中的代码,执行后完清空微任务;
	// 之后微任务执行完毕,取出第一个宏任务到主栈中执行
	// 第一个宏任务执行完后,如果有微任务会再次去清空微任务,之后再去取宏任务,这样就形成事件环;
*/
复制代码
  • 解析:

    1. 主栈中的代码从上往下执行, 遇到第一个定时器, 先将其挂起(s1) -> 继续往下
    2. 遇到了Promise.then, 它是一个微任务, 将其放在微任务队列 -> 继续往下
    3. 遇到同步代码console.log('start'), 执行后输出: start -> 继续往下
    4. 栈里面的(同步)任务执行完毕后, 查看异步队列, 发现微任务队列有then(p1), 会把这个微任务拿到栈里面执行,执行后输出: 3(微任务要先于宏任务执行)
    5. 接下来往下执行又遇到一个定时器(宏任务), 又将其挂起(s2)
    6. 微任务执行完成后,发现微任务队列已清空,然后执行宏任务; 因为s1先于s2放到异步的回调队列, 将s1拿到栈里面执行, 执行后输出: 1
    7. console.log(1)执行完毕后又遇到一个微任务then, 将其放到微任务队列(p2), 宏任务完成后再次清空微任务队列, 此时发现微任务p2, 将p2拿到主栈执行, 执行后输出: 2
    8. 微任务p2执行完成后,再取宏任务,发现宏任务队列有s2, 将其放到主栈里面执行, 执行后输出: 4
    setTimeout(function () {
        console.log(1);
        Promise.resolve().then(function () {
            console.log(2);
        }); // p2
    }); // s1

    setTimeout(function () {
        console.log(3);
    }); // s2

    Promise.resolve().then(function () {
        console.log(4);
    }); // p1

    console.log(5);  // 5 4 1 2 3
复制代码
  • 解析

    1. 首先输出 5, 因为console.log(5)是同步代码
    2. 接下来将两个setTimeout和最后的Promise放入异步队列(将setTimeout放入宏任务队列[s1, s2],将Promise.then放入微任务队列[p1]);
    3. 执行完同步代码后,发现微任务队列和宏任务队列都有代码, 按浏览器事件环机制, 优先执行微任务
    4. 将微任务队列中的p1拿到栈里执行, 执行完成后输出 4
    5. 微任务p1执行完后发现微任务队列已清空, 接下来执行宏任务
    6. 将宏任务队列中的s1拿到栈里面执行, 执行完成后输出 1
    7. 宏任务s1执行过程中发现promise.then, 将其加入微任务队列[p2]
    8. 宏任务s1执行完成后, 要再次清空微任务队列, 将微任务队列中的p2拿到主栈执行, 执行完成后输出2
    9. 微任务p2执行完成后, 发现微任务队列已清空, 此时宏任务队列有s2
    10. 将宏任务s2拿到栈里面执行, 执行完成后输出 3
	setTimeout(()=>{
	    console.log('A');
	},0);
	var obj={
	    func:function () {
	        setTimeout(function () {
	            console.log('B')
	        },0);
	        return new Promise(function (resolve) {
	            console.log('C');
	            resolve();
	        })
	    }
	};
	obj.func().then(function () {
	    console.log('D')
	});
	console.log('E');
	
	// C E D A B

复制代码
  • 解析:

    1. 首先setTimeout(()=>{ console.log('A'); },0)被加入到宏任务事件队列中,此时宏任务中有[s1(输出A)];
    2. obj.func()执行时,setTimeout(()=>{console.log('B'); },0)被加入到宏任务事件队列中,此时宏任务中有[s1,s2(输出B)];
    3. 接着return一个Promise对象,new Promise实例时,Promise构造函数中的函数参数会立即执行, 执行console.log('C'); 此时打印了 'C'
    4. 接下来遇到then方法,将其回调函数加入到微队列,此时微任务队列中有[p1];
    5. 主栈中的代码继续执行, 遇到同步任务console.log('E'),执行后输出 'E'
    6. 此时所有同步任务执行完毕, 开始检查异步队列,先检查微任务队列, 发现了p1, 执行微任务p1,输出'D'
    7. p1执行完成后,发现微任务队列已清空, 发现宏任务队列依然有任务,取出第一个宏任务s1压到主栈执行, 执行完成后输出'A'
    8. s1执行完毕后,检查发现微任务列表已清空, 而宏任务列表还有一个任务,接着取出下一个宏任务s2
    9. s2执行完毕后输出 'B'

小结

磕磕绊绊终于是理解了这一块的知识点, 以前只是在不断的搬砖, 却从未停下来思考、认真学习, GET到之后感觉解开了不少疑惑;

在写文档时候发现自己的语言描述能力居然如此的不堪, 啰里啰嗦写了很多; 这大抵是成长的必经之路吧;

参考了一些朋友的文章, 从中学习到不少, 有知识点的学习也有大佬对知识点巧妙的描述技巧; 向大佬致敬!

参考文章:

  1. 笔试题——JavaScript事件循环机制(event loop、macrotask、microtask【作者:立志搬砖造福生活】
  2. Javascript事件环该如何理解?
  3. 谈谈Node中的常见概念【作者:凌晨夏沫】(作者是前端大佬一枚,可关注一下)

转载于:https://juejin.im/post/5ca38b5c51882544114cd7ff

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值