回到函数

在执行异步任务时,必须指明它的回调函数。回调函数就是那些会被 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!