categories: [前端,JavaScript,异步编程]
thumbnail: /images/fe/yibu.jpg
toc: true
序言
在event loop中我们已经学习了,js是一门单线程语言,这意味着通常情况下JS运行的代码是同步且阻塞的,但是在实际应用中,尤其是在浏览器端,这种同步阻塞的编程无法实现特定的需求,当遇到一些耗时的计算,请求时就会造成后续线程的等待甚至卡死,给用户造成非常糟糕的体验。
基于这个问题,JS的异步编程解决方案应运而生。时至今日,前端开发人员依然能听到一句话:JS是单线程的,天生异步,适合IO密集型,不适合CPU密集型
接下来就来了解JS异步编程的实现方案
回调函数
一提到回调函数所有人应该都不陌生,从第一天学习JS开始,我们就了解到JS的一个全局函数:setTimeout。
setTimeout(function () {
console.log('Time out');
}, 1000);
在event loop这篇文章中,我们重新认识了setTimeout,它不再是一个简简单单的让代码延迟执行的定时器函数,同时也是JS实现异步编程的一个工具之一
上述例子中,setTimeout内部的这个匿名函数,就叫做setTimeout的回调函数,意义就是在1秒之后,执行回调函数
对于回调函数,大多数人的第一反应应该还是ajax的回调,在请求完成之后执行一段代码。这样看来,回调函数似乎还蛮好用的,能够手动掌握一些代码的执行顺序,但是,考虑一下下面这个情况:
回调地狱
这是微信小程序官方推荐(规定)的登录流程,可以看到,对于前端来说,需要先调用wx.login()接口获取code,然后使用code向自己的后台请求session_key,之后再调用其他业务接口时带上这个session_key,后台再返回数据。
对于上述流程,在使用axaj回调函数时,代码是这样的
wx.login({
success(res) {
//第一层回调,调用wx.login接口
if (res.code) {
ajax.request({
method: 'POST',
url: 'user/register',
data: {
code: res.code,
},
success: result => {
//第二层回调,获取session_key
ajax.request({
method: 'POST',
url: 'user/getinfo',
data: {
session_key: res.session_key,
},
success: result => {
//第三层回调,获取业务数据
...
},
})
},
})
}
}
})
可以看到,整段代码充满了回调嵌套,代码不仅在纵向扩展,横向也在扩展。我相信,对于任何人来说,调试起来都会很困难,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以查看流程,而最终的结果藏在整段代码的中间位置。真实的JavaScript程序代码可能要混乱的多,使得这种追踪难度会成倍增加。这就是我们常说的回调地狱(Callback Hell)。
为什么会出现这种现象?
如果某个业务,依赖于上层业务的数据,上层业务又依赖于更上一层的数据,我们还采用回调的方式来处理异步的话,就会出现回调地狱。
大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码的难度很大,很容易产生Bug。
控制反转
// A
$.ajax({
...
success: function (...) {
// C
}
});
// B
对于上述代码,代码A和代码B是同步代码,由JavaScript的event loop机制监督执行,换句话说,JS扮演了一个绝对可靠的执行者,我们将代码交给它去执行。而代码C,却是由ajax库提供的回调API来执行,这个第三方的身份相对于JS来说就不是那么可靠。
ajax大家都知道是JS提供的异步请求方法与标准,这个问题在这个例子上貌似不会有太严重,但是,请不要被这个小概率迷惑而认为这种控制切换不是什么大问题。实际上,这是回调驱动设计最严重(也是最微妙)的问题。它以这样一个思路为中心:有时候ajax(…),也就是你交付回调函数的第三方不是你编写的代码,也不在你的直接控制之下,它是某个第三方提供的工具。
这种情况称为控制反转,也就是把自己程序一部分的执行控制交给某个第三方,在你的代码和第三方工具直接有一份并没有明确表达的契约。
既然是无法控制的第三方在执行你的回调函数,那么就有可能存在以下问题,当然通常情况下是不会发生的:
- 调用回调过早
- 调用回调过晚
- 调用回调次数太多或者太少
- 未能把所需的参数成功传给你的回调函数
- 吞掉可能出现的错误或异常
- …
这种控制反转会导致信任链的完全断裂,如果你没有采取行动来解决这些控制反转导致的信任问题,那么你的代码已经有了隐藏的Bug,尽管我们大多数人都没有这样做。
Promise
开门见山,Promise解决的是回调函数处理异步的第2个问题:控制反转
在event loop中我们知道了,对于异步代码,event loop会将其放入event queue(任务队列)当中。而任务队列又分为宏队列和微队列,Promise就被分在了微队列当中。
由此可见,使用Promise之后代码的运行将由event loop完全接管,由Javascript运行机制来运行
Promise的含义
所谓Promisem,简单来说就是一个容器,里面保存着某个未来才会结束的事件的结果,说白了就是一个耗时操作。从语法上来说,Promise是一个对象,他可以获取异步操作的消息,Promise提供统一的API,各种异步操作可以用同样的方法进行处理。
Promise对象有两个特点:
- 对象的状态不受外界影响。Promise有三种状态:Pending(进行中), Fulfilled(已成功), Rejected(已失败),只有异步操作的结果可以改变这个状态,其他手段无法改变
- 一旦状态改变之后就是稳定下来,不会发生二次改变。从pending变为Fulfilled或Rejected后,就一直保持这个结果,称为Resolve(已定型)。
Promise可以将异步操作用同步的流程表达出来,避免了层层嵌套
用法
Promise是一个构造函数 ,可以生成实例对象
var promise = new Promise(function(resolve, reject){
//....一些代码
if(//异步操作成功){
resolve(value)
}
else{
reject(err)
}
})
Promise构造函数接收一个函数作为参数,这个函数接收两个参数:resolve和reject。resolve的作用是将状态从pending变为Resolve,并将异步操作的结果带出去,异步操作成功时调用。reject的作用是将状态从pending变为Reject, 异步操作失败时调用,并将错误传递出去。
Promise实例生成后可以指定then方法作为回调,then方法接收两个函数作为参数, 第一个是promise状态变为resolved时调用,第二个是状态变为rejected时调用,其中,第二个参数是可选的
一个简单的例子
let propmise = new Promise(function(resolve, reject){
console.log('a')
resolve()
})
promise.then(function{
console.log('b')
})
console.log('c')
//结果:acb
链式调用
我们把上面那个多层回调嵌套的例子用Promise的方式重构
//获取code
let getKeyPromise = function () {
return new Promsie(function (resolve, reject) {
wx.login({
success(res) {
//第一层回调,调用wx.login接口
if (res.code) {
resolve(res.code)
}
}
})
});
};
//获取session_ket
let getTokenPromise = function (key) {
return new Promsie(function (resolve, reject) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getToken',
data: {
code: key
},
success: function (res) {
resolve(res.session_key);
},
error: function (err) {
reject(err);
}
});
});
};
//获取业务数据
let getDataPromise = function (data) {
return new Promsie(function (resolve, reject) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/getData',
data: {
session_key: data,
},
success: function (res) {