回到函数
在执行异步任务时,必须指明它的回调函数。回调函数就是那些会被 JavaScript 主线程挂起且暂不执行的代码。
当非 JavaScript 线程执行完任务后,会将结果以事件通知的形式存到 JavaScript 任务队列中,任务队列中的事件将指明具体调用哪个回调函数。当 JavaScript 主线程的同步代码均执行完毕后,就会获取任务队列中的事件,调用与之相关的回调函数。
常规异步任务
前面介绍了请求百度首页的例子,这就是一种常规的异步任务,代码如下。
import * as request from 'request'
request('http://www.baidu.com', function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body) //显示百度首页的HTML源码
}
})
let a = "hello";
console.log(a);
本例中使用了 Request 框架,只能在 Node.js 中执行,需要先执行以下命令安装 Request 框架。 |
$ npm install -g request
由于示例代码中以非相对模块形式导入了 Request 框架,因此建议将其按 CommonJS 规范编译成 JavaScript 代码来执行。
在本例中,request() 函数需要传入两个参数:第一个参数为需要请求的 URL,Request 框架将以异步形式访问该 URL 并获取结果;第二个参数是网络结果返回后要执行的回调函数。
基于异步任务运行机制,可知代码的执行顺序为 1→2→6→7→8→3→4→5
,其运行结果如下。
> hello
> <!DOCTYPE html><!--STATUS OK-->
<html>
<head>
<title>百度一下,你就知道</title>
…//省略后续HTML源码
除用匿名函数之外,回调函数还可以使用常规声明的函数,示例代码如下。
import * as request from 'request'
function receiveResponse(error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body) //显示百度首页的HTML源码
}
}
request('http://www.baidu.com', receiveResponse);
虽然使用回调函数执行简单的异步任务并无任何问题,但是在实际项目中经常遇到这类场景:某个业务需要连续调用多个 API,每个 API 都从上一个 API 中获取数据,如果还采用回调函数的方式来处理,就会出现回调灾难。
例如,需要先后获取产品主数据、产品评论、产品促销和产品推荐的信息,以回调函数的形式编写,代码如下。
import * as request from 'request'
request('http://xxx.api/product', function (error, response, body) {
if (!error && response.statusCode == 200) {
let product:any = {};
product.mainInfo = JSON.parse(body);
request('http://xxx.api/product/reviews', function (error, response, body) {
if (!error && response.statusCode == 200) {
product.reviews = JSON.parse(body);
request('http://xxx.api/product/promotinon', function (error,
response, body) {
if (!error && response.statusCode == 200) {
product.promotinon = JSON.parse(body);
request('http://xxx.api/recommendation', function (error,
response, body) {
if (!error && response.statusCode == 200) {
product.recommendation = JSON.parse(body);
//其他操作,例如将Product渲染到UI...
}
})
}
})
}
})
}
})
这就是典型的回调灾难场景:代码充满了嵌套结构,不仅在纵向增长,还在横向增长。这样的代码调试起来相当困难,必须从一个函数跳到下一个,再跳到下一个,需要在整个代码中跳来跳去查看运行情况,而最终的结果却藏在整段代码的中间位置。回调函数表达异步流程的方式是非线性的、非顺序的,不符合我们的思维方式。这不仅使得正确推导代码的难度很大,还使得代码难以理解和维护。
为了解决回调灾难的问题,ECMAScript 最新标准中先后引入了 Promise 对象和 async/await 语法,后面将详细介绍。
计时器
除常规异步任务之外,在 TypeScript 中还有两种内置的特殊异步任务,它们是 setTimeout 计时器和 setInterval 计时器,用于在指定的时间间隔后执行回调函数。
setTimeout()
setTimeout() 函数用于在指定的时间间隔后执行一段特定的代码。对于它,需要传入以下参数。
-
回调函数。
-
执行回调函数前需要等待的时间间隔(单位为毫秒),当时间间隔为 0 时,将不等待,尽快执行回调函数(由于这是异步任务,将以事件的形式存放到任务队列中,队列中的任务要在主线程任务完成后才执行)。
-
传给回调函数的参数。
例如,以下代码使用了 setTimeout() 函数,回调函数将在两秒后执行并输出 “Hello world!”。
let myGreeting = setTimeout(function() {
console.log('Hello world!');
}, 2000)
console.log('Main thread excuted!');
输出结果如下。
> Main thread excuted!
两秒后,又输出以下内容。
> Hello world!
除用匿名函数之外,回调函数还可以使用常规声明的函数,示例代码如下。
function sayHello() {
console.log("Hello world!");
}
let myGreeting = setTimeout(sayHello, 2000);
console.log('Main thread excuted!');
回调函数可以声明参数,在调用 timeout() 函数的末尾参数时可以指定参数值,示例代码如下。
function sayHello(somebody) {
console.log('Hello ' + somebody + '!');
}
let myGreeting = setTimeout(sayHello, 2000, 'world');
console.log('Main thread excuted!');
要使用 clearTimeout() 函数清除已经定义的计时器,只需在函数中传入计时器变量即可。例如,在以下代码中,将计时器变量 myGreeting 传入了 clearTimeout() 函数中,计时器将被销毁,回调函数不会执行。
function sayHello(somebody) {
console.log('Hello ' + somebody + '!');
}
let myGreeting = setTimeout(sayHello, 2000, 'world');
console.log('Main thread excuted!');
clearTimeout(myGreeting);
输出结果如下。
> Main thread excuted!
setInterval()
setInterval() 函数用于在一段时间间隔后重复执行一段特定的代码。它的参数和调用方式与 setTimeout() 函数完全一致,唯一的区别在于它并不像 setTimeout() 函数那样只执行一次回调函数,而每隔一段时间都会执行回调函数,直到计时器销毁为止。
例如,以下代码使用了 setInterval() 函数,回调函数将每隔两秒就执行一次并输出 “Hello world!”。
function sayHello(somebody) {
console.log('Hello ' + somebody + '!');
}
let myGreeting = setInterval(sayHello, 2000, 'world');
console.log('Main thread excuted!');
输出结果如下。
> Main thread excuted!
两秒后,又输出以下内容。
> Hello world!
两秒后,又输出以下内容。
> Hello world!
两秒后,又输出以下内容。
> Hello world!
要使用 clearInterval() 函数清除已经定义的计时器,只需在函数中传入计时器变量即可。例如,在以下代码中,将计时器变量 myGreeting 传入了 clearInterval() 函数,计时器将被销毁,回调函数不会执行。
function sayHello(somebody) {
console.log('Hello ' + somebody + '!');
}
let myGreeting = setInterval(sayHello, 2000, 'world');
console.log('Main thread excuted!');
clearInterval(myGreeting);
输出结果如下。
> Main thread excuted!