概述
单线程模型
JavaScript是单线程模式,同时只能执行一个任务。
js引擎有多线程,每个js脚本只能在一个线程上运行(主线程),其他线程在后台配合。
当IO操作耗时很长时, CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
同步任务和异步任务
程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。
- 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
- 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务,异步任务不具有“堵塞”效应。
任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供多个任务队列(task queue),里面是各种需要当前程序处理的异步任务。
引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)
event Loop
宏任务按照宏任务队列执行,浏览器可以在不同宏任务执行之间进行渲染
微任务按照微任务队列执行,执行的时机:
如果没有js在执行列,将会在每个callback后执行
或者在每个宏任务的结尾执行。
Event Loop的执行顺序:
- 首先执行同步代码,这属于宏任务(macrotask)
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行(微任务)
- 执行所有的微任务(microtask),在这期间加入的微任务会加入到微任务队列尾部,并在之后执行。
- 执行完所有微任务后,如有必要会渲染页面
- 开始下一轮event loop,执行宏任务重的异步代码(setTimeOut)
微任务包括:
- process.nextTick (Node独有)
- promise
- MutationObserver
宏任务:
- script
- setTimeOut
- setInterval
- setImmediate
- I/O
- UI rendering
Node 中的 event loop
node中的 event loop 和浏览器中的弯曲是不同的东西
Node中的 event loop 分为 6 个阶段,会按照顺序反复运行。当进入相应的阶段之后,会从对应的回调队列中取出函数去执行。当该阶段的回调函数队列为空或者数量到达系统设定的阈值之后,进行状态切换,进入下一阶段。
异步操作的模式
回调函数
回调函数的优点是简单、容易理解和实现
缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling)
事件监听
事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。
缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布、订阅
异步操作的流程控制
串行执行
可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function series(item) {
if(item) {
async( item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results[results.length - 1]);
}
}
series(items.shift());
并行执行
流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length === items.length) {
final(results[results.length - 1]);
}
})
});
如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。
并行与串行的结合
所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final(results);
}
});
running++;
}
}
launcher();
定时器
JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成
setTimeOut()
- 如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象。
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y, 1000) // 1
可以将obj.y放入一个函数来解决这个问题
setTimeInterval()
setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。
- 一个常见用途是实现轮询
var hash = window.location.hash;
var hashWatcher = setInterval(function() {
if (window.location.hash != hash) {
updatePage();
}
}, 1000);
clearTimeout(),clearInterval()
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
setTimeout和setInterval返回的整数值是连续的,也就是说,第二个setTimeout方法返回的整数值,将比第一个的整数值大1。利用这一点,可以写一个函数,取消当前所有的setTimeout定时器。
(function() {
// 每轮事件循环检查一次
var gid = setInterval(clearAllTimeouts, 0);
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
debounce(去抖)函数
$('textarea').on('keydown', debounce(ajaxAction, 2500));
function debounce(fn, delay){
var timer = null; // 声明计时器???
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
运行机制
setTimeout和setInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
如果本轮事件循环过长,没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。
setTimeout(f, 0)
setTimeout(f, 0)中的 f 会在下一轮事件循环一开始就执行。
应用
- 一大应用是,可以调整事件的发生顺序。
- 另一个应用是,用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,keypress事件会在浏览器接收文本之前触发。只有用setTimeout改写,上面的代码才能发挥作用
//以下代码将会达不到目的
document.getElementById('input-box').onkeypress = function (event) {
this.value = this.value.toUpperCase();
}
//用setTimeout改写
document.getElementById('input-box').onkeypress = function() {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
}
Promise对象
Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
Promise 是一个对象,也是一个构造函数。
Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。
// 传统写法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promise 的写法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
Promise 对象的状态
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
- 异步操作未完成(pending)
- 异步操作成功(fulfilled)
- 异步操作失败(rejected)
上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。
这三种的状态的变化途径只有两种。
- 从“未完成”到“成功”
- 从“未完成”到“失败”
一旦状态发生变化,就凝固了,不会再有新的状态变化,这也意味着,Promise 实例的状态变化只可能发生一次。
因此,Promise 的最终结果只有两种。
- 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
- 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。
Promise 构造函数
JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
Promise.prototype.then()
then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
then方法可以链式使用。
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
最后一个then方法,回调函数是console.log和console.error,用法上有一点重要的区别。
- console.log只显示step3的返回值
- console.error可以显示p1、step1、step2、step3之中任意一个发生的错误。
举例来说,如果step1的状态变为rejected,那么step2和step3都不会执行了(因为它们是resolved的回调函数)。Promise 开始寻找,接下来第一个为rejected的回调函数,在上面代码中是console.error。这就是说,Promise 对象的报错具有传递性。
then() 用法辨析
// 写法一 f3回调函数的参数,是f2函数的运行结果
f1().then(function () {
return f2();
}).then(f3);
// 写法二 f3回调函数的参数是undefined
f1().then(function () {
f2();
}).then(f3);
// 写法三 f3回调函数的参数,是f2函数返回的函数的运行结果
f1().then(f2())
.then(f3);
// 写法四 写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果。
f1().then(f2)
.then(f3);
实例:图片加载
var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
小结
Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。
Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态(传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。)
Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑
微任务
Promise 的回调函数属于异步任务,会在同步任务之后执行,但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1