单线程的JavaScript:
众所周知JavaScript这门语言是单线程,但是为什么要设计成单线程呢?明明多线程更加有效率。
这里我们就要从JavaScript的用途来考虑,JavaScript是一门浏览器脚本语言,也就是说它需要操作DOM来更改页面展示,提供用户良好的上网体验。这时候单线程的好处就体现出来,不妨想象一下:
如果JavaScript是多线程的语言,当一个线程正在进行一个DOM的删除操作,这是另一个线程出来搞事情,进行这个DOM的修改操作。这种情况要怎么处理呢?这样的场景就会出现一些问题。
所以说JavaScript单线程的设计是从它的用途出发的,并且在以后也会一直坚持单线程的设计。
同步、异步:
单线程就像是大家在食堂排队打饭,如果想打到饭,那就必须等食堂大妈一个一个把排在前面的人的饭打完才能轮到你。但是,如果JavaScript真的是这样那就糟糕了。不妨想一下,如果我访问的这个网站有一个超清图片,加载很慢,难道是要用户等到图片完全加载出来之后才能进行其他的操作嘛,显然现在我们浏览网站并不会出现这样的情况,那这又是怎么一回事呢?
那是因为JavaScript的任务分为同步任务和异步任务两种:
- 同步任务
同步任务就是进入主线程的任务,必须排队一个一个按顺序的执行。
- 异步任务
开发者们认识到,像费时的IO操作,接口请求完全可以不理他们,将他们暂时挂起,放入事件表(Event Table)中当,主线程中的任务执行完,再来“宠幸”它们。这种暂时挂起的任务就是异步任务。
回调函数:
每一个异步任务都需要指定一个事件,例如当耗时的IO操作执行完之后,就会将它指定的这个事件添加到任务队列中,这个事件就是回调函数。
所以说我们经常说的主线程开始执行异步任务了,其实主线程执行的是异步任务的回调函数。
Event Loop:
现在来分析一下上图中的事件执行顺序:
- 首先任务进入执行栈,会先来判断这个任务是同步任务还是异步任务。
- 如果是同步任务则进入主线程来执行,如果是异步任务则进入到事件表中注册函数
- 当事件表中的异步事件执行完后会在事件队列中注册自身的回调函数
- 当主线程的任务执行完后会去事件队列中检查是否有需要执行的事件,如果事件队列中有任务,则进入主线程执行。
上述的过程会不断的重复执行,这个重复的过程就是我们通常所说的事件循环机制(Event Loop),看下面代码:
console.log(1);
document.body.onclick = function () { console.log('2'); }
console.log(3);复制代码
JavaScript中的给DOM注册一个点击事件,这个点击事件其实就是一种异步任务,因为我们不知道用户什么时候才会点击。
现在我们根据上图来分析一下这段代码的执行:
1.首先主线程会将同步的console.log操作放在主线程中执行,
2.首先打印出1,3
3.同时将点击事件放入到事件表中,当用户点击body后,JS会在事件队列中注册点击的回调函数。
4.这时主线程任务执行完毕,去任务队列中检查是否有需要执行的任务,这是发现了点击body的回调函数,JavaScript就会将这个回调函数放在主线程中执行。
5.所有就打印出了1,3,2的结果。
宏任务和微任务
异步任务其实还可以细分为宏任务和微任务,他们的区别就是执行顺序的不同,下面我们就讨论一下他们具体的执行顺序,在Event Loop中到底有什么不同。
其实异步任务的执行是有两个执行队列的,一个是宏任务队列,一个是微任务队列,每次执行的时候,首先是去微任务队列中查看是否有需要执行的任务,然后再去宏任务执行队列中查看是否有需要执行的事件。
宏任务:整体的script代码,setTimeout,setInterval、setImmediate
微任务:promise,process.nextTick
我们现在根据这个流程图来分析一下具体的执行顺序:
- 首先执行主线程中的script代码,也就是执行宏任务
- 当宏任务执行完毕后,查看微任务队列是否有需要执行的事件,如果没有则继续查看宏任务,如果有则将微任务队列中的事件全部执行完毕。
- 当微任务清空后,在继续检查宏任务队列是否有可执行的事件,这个循环的过程就是宏任务和微任务的循环过程。
光说不练假把式:
现在我们来分析一段代码,看看输出的顺序是否符合上面的流程图:
- 按照上面的流程图,先检查同步代码也就是宏任务代码进行执行,输出1,6 ,这里需要注意(Promise声明中的代码是会立即执行的也就是同步代码)
- 检查微任务事件队列,这里面的微任务就是promise.then里面的代码,这时输出7。
- 微任务完成之后继续检查宏任务,这段代码里面的宏任务就是setTimeOut,所以接下来执行宏任务中的代码,也就是输出2,4,9,10,
- 然后再去执行微任务队列中的事件,输出5,11
- 在输出7的时候then方法里面注册了setTimeout宏任务事件,在输出2的时候也同样注册了宏任务setTimeout,所以再次检查宏任务的时候,按顺序输出8,3
所以上述代码最终的输出应该是:1,6,7,2,4,9,10,5,11,8,3
结尾
希望大家看完这篇文章能够有收获,哪里写的不对也希望各位大佬多加指点,本文章仅为记录前端小白的学习过程,谢谢大家!