Event Loop(事件轮询)
我们大家都知道JS是单线程的,关于线程大家参考阮一峰老师的文章
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
任务队列
我们都知道I/O(例如ajax)操作返回的结果很慢,需要等着返回的结果才能执行下面的代码,这样浪费了很大的性能。
这时JS语言设计者意识到,主线程可以完全不管I/O操作,先运行其他的代码,等主线程空闲后再去执行I/O,所以任务可以分为两种同步任务和异步任务
同步任务
是指在主线程排队的任务,只有前一个执行完毕后,才能执行下一个任务
异步任务
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步任务包含了宏任务和微任务
宏任务:
# | 浏览器 | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务:
# | 浏览器 | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick
异步执行的运行机制如下:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
事件和回调函数
任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
例子:
setTimeout(function(){ console.log('定时器开始啦') }); new Promise(function(resolve){ console.log('马上执行for循环啦'); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); } }).then(function(){ console.log('执行then函数啦') }); console.log('代码执行结束');
执行结果
马上执行for循环啦 代码执行结束 执行then函数啦 定时器开始啦
可能有人会问为什么没有先打印代码执行结束了,promise明显属性微任务
promise只有then后面的回调函数才会被注册到微任务中,个人理解,promise的then之前的是在主线程中,另外ajax是属性宏任务中的
宿主环境提供的叫宏任务,由语言标准提供的叫微任务,这是算比较标准也算比较好记忆的区分宏任务和微任务了。
宿主环境:
简单来说就是能使javascript完美运行的环境,只要能完美运行javascript的载体就是javascript的宿主环境。目前我们常见的两种宿主环境有浏览器和node。
我们都知道window是我们一直使用的全局对象,但其实global 是 javascript 运行时所在宿主环境提供的全局对象,在node出生前这个对象一直都存在于概念里。直到node的出现才使我们真正看到了global。
global 是 javascript 运行时所在宿主环境提供的全局对象,在浏览器中,没有实现global对象,而是通过window对象来指向global对象,代替global成为全局对象。因为浏览器暴露了一系列操作 DOM, Location, History 等 Api 供 javascript 调用,而这些操作对象在global中是不存在的。对于node来说,它不需要DOM这些操作,用到的只是javascript的原声功能。
宿主环境内所有的内建或自定义的变量/函数都是 global/window 这个全局对象的属性/方法,而由宿主环境提供的也叫宏任务。
语言标准:
我们都知道JavaScript是一种编程语言,但其实JavaScript由ECMA制定标准,称之为ECMAScript,所以由语言标准提供的就是微任务,比如ES6提供的promise。
所以回归上面的代码,promise是ES6语言标准提供的,定时器是宿主环境提供的,所以promise会比定时器更早执行。
当然,不同浏览器不同环境对于这两个的概念会有不同,相同的代码在不同浏览器执行会有不同的顺序,在不同浏览器也会有不同的顺序。想要深入研究的可以自行查一查资料。
发表评论
侧栏公告
寄语
譬如朝露博客是一个分享前端知识的网站,联系方式11523518。
热评文章
标签列表
热门文章
友情链接