JavaScript异步编程

一、使用回调的异步编程

    在最基本的层面上,Javascript 异步编程是通过回调实现的。回调就是函数,可以传给其他函数。而其他函数会在满足某个条件或发生某个(异步)事件时调用(“回调”)这个函数。回调函数被调用,相当于通知你满足了某个条件或发生了某个事件,有时这个调用还会包含函数参数,能够提供更多细节。

1.1 定时器

    一种最简单的异步操作就是在一定时间过后运行某部分代码。例如,可以使用 setTimeout() 函数来实现这种操作。

<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>菜鸟教程(runoob.com)</title> 
</head>
<body>

<p>回调函数等待 3 秒后执行。</p>
<p id="demo"></p>
<script>
function print() {
	document.getElementById("demo").innerHTML="RUNOOB!";
}
setTimeout(print, 3000);
</script>

</body>
</html>

    上面这段代码中,setTimeout() 设置3s之后再执行print函数。除了 setTimeout() 函数外,setInterval() 也可以起到相似的作用,两者的区别是:setTimeout() 只会执行一次回调,而 setInterval() 方法会按照设置的周期不停地进行回调,直到 clearInterval() 被调用或窗口被关闭。

1.2 事件

    客户端 JavaScript 程序几乎都是由事件驱动的:它们通常不等待用户执行某种预定的计算,而是等待用户执行某些操作,然后响应用户的操作。当用户按下键盘上的键,移动鼠标,单击鼠标按钮或触摸触摸屏设备时,Web 浏览器会发生事件。事件驱动的 JavaScript 程序在指定的上下文中为指定类型的事件注册回调函数,并且只要指定事件发生,Web 浏览器就会调用这些函数。这些回调函数称为事件句柄或事件监听器,并且使用 addEventListener() 注册,下面是一个实例:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>

<p>该实例使用 addEventListener() 方法来向按钮添加点击事件。</p>
<button id="myBtn">点我</button>
<p id="demo"></p>
<script>
document.getElementById("myBtn").addEventListener("click", function(){
    document.getElementById("demo").innerHTML = "Hello World";
});
</script>

</body>
</html>
1.3 网络事件

    JavaScript 编程中异步的另一个常见来源是网络事件,即通过异步请求从服务器获取相应数据。Ajax(Asynchronous JavaScript and XML)就是一种典型的异步网络请求,可使网页从服务器请求少量的信息,达到局部刷新的效果。

<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>菜鸟教程(runoob.com)</title> 
</head>
<body>

<p><strong>以下内容是通过异步请求获取的:</strong></p>
<p id="demo"></p>
<script>
var xhr = new XMLHttpRequest();
 
xhr.onload = function () {
    // 输出接收到的文字数据
    document.getElementById("demo").innerHTML=xhr.responseText;
}
 
xhr.onerror = function () {
    document.getElementById("demo").innerHTML="请求出错";
}
 
// 发送异步 GET 请求
xhr.open("GET", "https://www.runoob.com/try/ajax/ajax_info.txt", true);
xhr.send();
</script>

</body>
</html>

    请注意,上面的代码示例未像前面的示例那样调用 addEventListener()。对于大多数 Web API(包括此API),可以通过在生成事件的对象上调用 addEventListener() 并将事件的名称与回调函数一起传递来定义事件处理程序。不过,通常,也可以通过将单个事件侦听器直接分配给对象的属性来注册它。这就是我们在此示例代码中所做的,将函数分配给 onload、onerror 和 ontimeout 属性。按照惯例,此类事件侦听器属性的名称始终以 on 开头。 addEventListener() 是更灵活的技术,因为它允许多个事件处理程序。但是,如果确定没有其他代码需要为相同的对象和事件类型注册一个侦听器,则只需将适当的属性设置为回调会更简单。
    补充一点:如果学会使用 JQuery ,可以更加优雅地使用 Ajax ,示例如下:

$.get("https://www.runoob.com/try/ajax/demo_test.php",function(data,status){
    alert("数据: " + data + "\n状态: " + status);
});

二、Promise的使用

2.1 回调地狱

    我们之前遇到的异步任务都是一次异步,如果需要多次调用异步函数呢?例如,如果我想分三次输出字符串,第一次间隔 1 秒,第二次间隔 4 秒,第三次间隔 3 秒:

setTimeout(function () {
    console.log("First");
    setTimeout(function () {
        console.log("Second");
        setTimeout(function () {
            console.log("Third");
        }, 3000);
    }, 4000);
}, 1000);

    如果我们继续增加回调次数,那么缩进格式将会变得非常冗余。这种情况下便出现了回调地狱。当异步操作越多,这种嵌套的层级也就越复杂,不利于代码维护和异常处理。

2.2 Promise解决回调地狱

    Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。我们可以通过以下方法新建一个 Promise 对象:

new Promise(function (resolve, reject) {
    // 要做的事情...
});

    现在我们使用 Promise 机制来解决上面出现的问题:

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 4000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});

    接下来,我们对上面这段看起来比较”冗长“的代码进行剖析。
    1. Promise 构造函数只有一个参数,是一个函数,这个函数在构造之后会直接被异步运行,所以我们称之为起始函数。起始函数包含两个参数 resolve 和 reject。
    2. 当 Promise 被构造时,起始函数会被异步执行。resolve 和 reject 都是函数,其中调用 resolve 代表一切正常,而 reject 在出现异常时被调用。
    3. Promise 类有 .then() .catch() 和 .finally() 三个方法,这三个方法的参数都是一个函数,.then() 可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch() 则是设定 Promise 的异常处理序列,.finally() 是在 Promise 执行的最后一定会执行的序列。
    4. resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then。
    5. 如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作,reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常。
    6. resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列。resolve 和 reject 并不能够使起始函数停止运行,不要忘记return。
    综上所述,上面的代码将三个延时操作分成了三段,用来进行顺序操作,在第二段中将Promise对象进行返回。resolve()用来向then传递一个空的参数。

2.3 Promise补充示例

    接下来补充几个关于Promise使用的示例,用来增强理解。

    代码1

new Promise(function (resolve, reject) {
    var a = 0;
    var b = 1;
    if (b == 0) reject("Divide zero");
    else resolve(a / b);
}).then(function (value) {
    console.log("a / b = " + value);
}).catch(function (err) {
    console.log(err);
}).finally(function () {
    console.log("End");
});

    这段代码的运行结果如下:

a / b = 0
End

    代码2

new Promise(function (resolve, reject) {
    console.log(1111);
    resolve(2222);
}).then(function (value) {
    console.log(value);
    return 3333;
}).then(function (value) {
    console.log(value);
    throw "An error";
}).catch(function (err) {
    console.log(err);
});

    这段代码的运行结果如下:

1111
2222
3333
An error

2.4 使用Promise函数进行优化

    异步函数(async function)是 ECMAScript 2017 (ECMA-262) 标准的规范,几乎被所有浏览器所支持,除了 Internet Explorer。
    在2.2节中的Promise实现看上去比回调地狱还要长,所以我们可以将它的核心部分写成一个 Promise 函数:

function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

    然后,我们通过以下方式进行调用,实现相同的功能:

print(1000, "First").then(function () {
    return print(4000, "Second");
}).then(function () {
    print(3000, "Third");
});

    这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。

三、async和await

3.1 为什么引入async和await

    ES2017 引入了两个新的关键字(async 和 await)描述异步 JavaScript 编程中的模式转变。这些新关键字极大地简化了 Promise 的使用,使我们能够编写基于 Promise 的异步代码看起来像是等待网络响应或其他异步事件而阻塞的同步代码。尽管了解 Promise 的工作原理仍然很重要,但是当将它们与 async 和 await 一起使用时,它们的大部分复杂性(有时甚至是它们的存在!)就消失了。

3.2 使用async和await优化代码

    async 是 ES7 才有的与异步操作有关的关键字,和 Promise,Generator 有很大关联。await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。它们的用法如下:

async function name([param[, param[, ... param]]]) { statements }
[return_value] = await expression;

    async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。

async function helloAsync(){
    return "helloAsync";
}
  
console.log(helloAsync())  // Promise {<resolved>: "helloAsync"}
 
helloAsync().then(v=>{
   console.log(v);         // helloAsync
})

    async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误。
    正常情况下,await 命令后面是一个 Promise 对象,这时的返回值为Promise对象的处理结果。如果await等待的不是Promise对象,则返回该值本身,如字符串,布尔值,数值以及普通函数。

function testAwait(){
   return new Promise((resolve) => {
       setTimeout(function(){
          console.log("testAwait");
          resolve();
       }, 1000);
   });
}
 
async function helloAsync(){
   await testAwait();
   console.log("helloAsync");
 }
helloAsync();
// testAwait
// helloAsync

    在2.4节中,我们通过引入 Promise 函数对异步操作进行封装,实际上也可以通过使用 async 和 await 机制进行优化,如下所示:

function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

async function asyncFunc() {
    await print(1000, "First");
    await print(4000, "Second");
    await print(3000, "Third");
}
asyncFunc();

    现在看起来是不是So easy,就像同步操作一样!

3.3 async/await补充示例

(1) 正常情况使用resolve

async function asyncFunc() {
    let value = await new Promise(
        function (resolve, reject) {
            resolve("Return value");
        }
    );
    console.log(value);
    // 会输出 Return value
}
asyncFunc();

(2) 异常处理

async function asyncFunc() {
    try {
        await new Promise(function (resolve, reject) {
            throw "Some error"; // 或者 reject("Some error")
        });
    } catch (err) {
        console.log(err);
        // 会输出 Some error
    }
}
asyncFunc();

参考链接:菜鸟教程-Javascript教程

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值