JS之异步1

什么是异步

异步:一种通讯方式, 一个可以无需等待被调用函数的返回值就让操作继续进行的方法.

在操作系统中对设备的处理,很多就是利用异步来处理的;比如中断,计算机的网卡接受从网络中发过来的数据,如果让cpu一直轮询等待数据的传输完成,这个很费时间, 浪费cpu的效率,
在网卡接受网络数据的时候,可以让cpu自己先忙其他的,等网络数据接受完毕之后才让网卡发起一个中断请求让cpu介入进行处理;这就是异步。

JS中的异步

谈起JS的异步,就不得不说JS引擎是基于单线程事件循环的概念构建的。跟上面举的例子一样,为了让JS更有效率,响应更及时,JS中加入了异步。

JS引擎同一时刻只能执行一个代码块,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列中,每当一段代码准备执行是,都会被添加到任务队列中。每当JS引擎中的一段代码执行结束时,
事件循环会执行队列中的下一个任务。如下图可示:

JS中有哪些异步呢?

setTimeout(), setInterval(), ajax请求,dom事件这些都是JS中比较常见的异步.

事件模型

用户点击按钮或按下键盘上的按钮会触发类似onclick这样的事件,它会向任务队列添加一个新任务来响应用户的操作。如:

1
2
3
4
let button = document.getElementById("my-btn");
button.onclick = function(event) {
console.log("clicked");
};

这段代码中当按钮被点击的时候,会触发onclick事件,执行函数(clickFn)会被添加到任务队列中,当任务队列执行到clickFn的时候打印出clicked;

如下图:

事件模型适用于响应用户交互和完成类似的低频功能,但其对于更复杂的需求来说却不是很灵活。

setTimeout与setInterval

这两个区别不大,我们看一段代码:

1
2
3
4
5
6
7
8
9
setTimeout(function() {
console.log('settimeout');
}, 1000)
var sTime = new Date().getTime();
console.log(new Date());
console.log('test1');
while(new Date().getTime() - sTime < 2000) {
}
console.log(new Date())

执行结果显示打印出:

1
2
3
4
// Sun Oct 29 2017 20:24:29 GMT+0800 (CST)
// test1
// Sun Oct 29 2017 20:24:31 GMT+0800 (CST)
// settimeout

从中可以看出:

  1. 先打印test1,setTimeout的确是异步过程,会在1秒后加入任务队列;
  2. 只有在任务队列中的前面任务执行完,才会执行加入的任务(这段代码是过了两秒);

setTimeout也是一种异步的方式,但是不能很好的把控。

回调模式

回调模式与事件模式类似,只不过回调模式中被调用的函数是作为参数传入的,如下所示:

1
2
3
4
5
6
fs.readFile('example.txt', function(err, contents) {
if(err) {
throw err;
}
console.log(contents)
})

文件example.txt读取完毕后会把回调加入任务队列中,如果此时任务队列为空,直接执行回调,如果不为空,等前面的任务执行完再执行回调;

但是这有一个问题,当我们的需求是,先读取文件1,然后需要把读出的文件1作为内容进行网络转发;那就会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fs.readFile('example.txt', function(err, contents) {
if(err) {
throw err;
};
console.log(contents);
ajax({
url: 'xxx.json'
data: contents,
success: function() {
doSomething()
}
});
})

这样看起来还是,当时如果这样的需求是叠加的,那就可能会形成传说中的回调地狱了。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn1(function(err, result) {
if(err) {throw err;}
fn2(function(err, result) {
if(err) {throw err;}
fn3(function(err, result) {
if(err) {throw err;}
fn4(function(err, result) {
if(err) {throw err;}
})
...
})
})
})

像上面这样嵌套多个方法调用,会创建出一堆难于理解和调试的代码。

异步之生成器

生成器简单介绍

有时间会深入讲述一下生成器,这里只是简单说一下生成器的概念。下面就是一个生成器函数的栗子:

1
2
3
4
5
function *test() { // 生成器函数
yield 1;
yield 2;
}
var iter = test() // 迭代器

上面就是一个简单的生成器函数,当执行会生成一个迭代器,迭代器可参考之前写过的一片文章谈谈遍历与迭代协议.

迭代器每次调用next方法会执行生成器函数,并且每次执行完yield关键词就停止,只在当下一次调用next的时候才回到之前执行停止的地方继续执行;

如若需要回调或者系列化一序列的异步操作,生成器和yield语句就派上用场了。

生成器处理异步操作

任务处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function run(taskDef) {
let task = taskDef(); // 1.外层生成器函数执行 创建一个无使用限制的迭代器
let result = task.next(); // 2. 开始任务,执行到第一个yield
// 循环调用next
function step() {
if(!result.done) { // 3.判断是否迭代完毕(没有yield或者return)
if(typeof result.value === 'function') { // 4.下面包裹函数return的函数
result.value(function(err, data) { // 5.调用return的函数, 传入回调函数到callback
if(err) {
result = task.throw(err);
return;
}
result = task.next(data); // 6.异步回调完,继续执行到下一个yield
step(); // 7. 循环执行
})
}
} else {
result = task.next(result.value);
step(); // 循环执行
}
}
step(); // 启动
}

然后把异步操作包裹起来,例如:

1
2
3
4
5
6
7
let fs = require('sf');
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback)
}
}

下面是最终的调用方式:

1
2
3
4
5
6
run(function *() {
let contents = yield readFile('config.json');
doSomethingWith(contents);
yield readMoreFile('xxx.json');
console.log('Done');
});

这样就可以很好的解决回调地狱的问题了,并且把异步操作通过同步的形式来调用。

小结

上面谈了JS的异步,并分别讲了事件模型/setTimeout/回调模型,最后讲述了怎么用生成器来处理回调地狱,并可以用同步的操作方式.

上面这种处理形式还是比较麻烦,需要把异步函数包裹起来,并且无法区分用作任务执行器回调函数的返回值和一个不是回调函数的返回值。

这种方式在场景使用中还是有局限性,如果需求需要并行执行两个异步操作或者同时进行两个异步操作并且只取优先完成的操作;这种就不太好处理了,会在下一篇中讲述解决的办法.