js异步编程和事件循环
参考文章
http://www.cnblogs.com/3body/p/5691744.html JS
的线程、事件循环、任务队列简介
http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
异步编程四种方法
https://juejin.im/post/59e85eebf265da430d571f89 这一次,彻底弄懂 JavaScript 执行机制(推荐)
heap 堆
stack 栈
event loop 事件循环
Task Queue 任务队列
JS 的线程、事件循环、任务队列简介
事件循环 :将任务栈中的事件放到执行栈中
JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为 Tick。
每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。也就是每次 Tick 会查看任务队列中是否有需要执行的任务。
任务队列(事件队列):存储异步操作添加的相关事件回调
异步操作会将相关回调添加到任务队列中。不同的异步操作添加到任务队列的时机也不同。
异步操作是由浏览器内核的 webcore 来执行的,webcore 包含3种 webAPI,分别是 DOM Binding、network、timer模块。
setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
onclick 由浏览器内核的 DOM Binding 模块来处理,当
事件触发
的时候,回调函数会
立即添加
到任务队列中。
ajax 则会由浏览器内核的 network 模块来处理,在
网络请求完成
返回之后,才将回调添加到任务队列中。
主线程:主线程中的执行栈放的同步代码,先执行
js只有一个主线程。主线程将执行栈里的代码执行完毕。这时候事件循环才开始执行的。
所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。
当主线程中执行栈为空,才会进行事件循环来观察要执行的事件回调。当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
- 主线程的执行栈里先放的同步事件,先执行。
- 事件循环观察任务队列,然后取出相关回调放入执行栈中由主线程执行。
- 任务队列里的所有(待处理)事件都由事件循环来执行的。
- 异步操作会将相关回调添加到任务队列中,而且不同的操作添加的时机还不同。
例一:
var
req =
new
XMLHttpRequest();
req.open('GET', url);
req.onload =
function
(){};
// 这两个异步方法就会在 ajax 完成后推入任务队列,再由主线程执行
req.onerror =
function
(){};
req.send();
例二:
setTimeout(
function
(){
// 如果有大量的操作,可能会阻塞 UI 等,则可以使用 setTimeout (让这里的代码等一等)让主线程把更重要的代码先执行,完毕后,再来执行这里的操作。从而提高浏览器的性能。
//这里的代码会给主线程的同步代码让步,但优先于其他异步任务,只要执行栈一为空,它就会立即入栈执行。
},0);
// 设置为 0,也会有个最小间隔值,也会在主线程中的代码运行完成后,由事件循环从任务队列将回调添加到执行栈中才执行
例三:
// 事件循环测试。执行结果是 2-3-4-1,1在最后输出,说明事件循环是所有同步代码执行完后才开始执行的。
'use strict';
//异步操作 ,将相关回调添加到任务队列,等待同步代码执行完毕。执行栈为空后,由事件循环取出回调添加到执行栈中,再由主线程来执行。
setTimeout(
function
() {
console.log(1);
}, 0);
console.log(2);
let end = Date.now() + 1000*5;
while
(Date.now() < end) {}
console.log(3);
end = Date.now() + 1000*5;
while
(Date.now() < end) {}
console.log(4);
《你不知道的 JavaScript》一书中,重新讲解了 ES6 新增的任务队列,和上面的任务队列略有不同,上面的任务队列书中称为事件队列。事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。
Javascript异步编程的4种方法
"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;
"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
一、回调函数
假定有两个函数f1和f2,后者等待前者的执行结果。如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(
callback
){
setTimeout(function () {
// f1的任务代码
callback()
;
}, 1000);
}
执行代码就变成下面这样:
f1(
f2
);
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度
耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
二、事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
首先,为f1绑定一个事件,
当f1发生done事件,就执行f2。
f1.on('done', f2);
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
//触发f1的done操作,从而开始执行f2。
}, 1000);
}
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以
"去耦合"(Decoupling),有利于实现
模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
三、发布/订阅(观察者模式)
上一节的"事件",完全可以理解成"信号"。
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心
"发布"(publish)一个信号,其他任务可以向信号中心
"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做
"发布/订阅模式"(publish-subscribe pattern),又称
"观察者模式"(observer pattern)。
这个模式有多种
实现,下面采用的是Ben Alman的
Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向"信号中心"jQuery
订阅"done"信号。
j
Query.
subscribe
("done",f2);
然后,f1进行如下改写:
function f1(){
setTimeout(function () {
// f1的任务代码...
jQuery.
publish
("done");
//f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
}, 1000);
}
此外,f2完成执行后,也可以取消订阅(unsubscribe)。
jQuery.
unsubscribe
("done", f2);
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
四、Promises对象
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供
统一接口。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
function f1(){
var dfd = $.Deferred();
setTimeout(function () {
// f1的任务代码
dfd.resolve();
}, 500);
return dfd.promise;
}
这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的
配套方法,可以实现许多强大的功能。
比如,指定多个回调函数:
f1().then(f2).then(f3);
再比如,指定发生错误时的回调函数:
f1().then(f2).fail(f3);
而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。
javascript的执行机制
javascript是一门
单线程
语言,所以一切javascript版的"多线程"都是用单线程模拟出来的。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
Ajax
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
- ajax进入Event Table,注册回调函数success。
- 执行console.log('代码执行结束')。
- ajax事件完成,回调函数success进入Event Queue。
- 主线程从Event Queue读取回调函数success并执行。
setTimeout
大家对他的第一印象就是异步可以延时执行
有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?
setTimeout是异步的,应该先执行
console.log这个同步任务,所以结论是:
//执行console//task()
但我们把这段代码在chrome执行一下,却发现控制台执行
task()
需要的时间远远超过3秒,说好的延时三
秒,为啥现在
需要这么长时间啊?
理解setTimeout的定义。我们先说上述代码是怎么执行的:
- task() 进入 Event Table 并注册,计时开始。
- 执行 sleep 函数,很慢,非常慢,计时仍在继续。
- 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep还没执行完,task()只好在任务对列继续等待。
- sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
结论:
setTimeout 这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中。
又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
setTimeout(fn,0)
意思就是不用再等多少秒了,只要主线程执行栈内的
同步任务
全部
执行完成
,栈为空就
马上执行
。
补充:即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。
setInterval
循环的执行:
setInterval
会
每隔指定的时间
将注册的函数
置入Event Queue。
一旦
setInterval
的回调函数
fn
执行时间超过了延迟时间
ms
,那么就完全看不出来有时间间隔了
。
宏任务、微任务
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
1,7,6,8,2,4,3,5,9,11,10,12
- 整体script作为第一个宏任务进入主线程
- new Promise 会立即执行
- promise 的 回调函数 then 是一个微任务
- process.nextTick 是一个微任务
有一个事件循环,但是
任务队列可以有多个。
整个script代码,放在了macrotask queue中,setTimeout也放入macrotask queue。
但是,promise.then放到了另一个任务队列
microtask queue中。
这两个任务队列执行顺序如下,取1个macrotask queue中的task,执行之。
然后把所有
microtask queue顺序执行完,再取macrotask queue中的下一个任务。
牢记概念
(1)js的异步
我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。
(2)事件循环Event Loop
事件循环是js实现异步的一种方法,也是js的执行机制。
(3)javascript的执行和运行
执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。
(4)setImmediate
微任务和宏任务还有很多种类,比如
setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。
(5)最后的最后
- javascript是一门单线程语言
- Event Loop是javascript的执行机制