【JavaScript】异步编程

在传统的单线程编程中,任务也就是函数,按照被调用的顺序执行。一次只能完成只能完成一件任务,后一件任务必须等待前一件任务的完成才能执行。

但这种单线程的模式会带来隐患,假如有一个耗时长的任务,例如网络请求,那么后面的任务都要等待这个任务完成,这拖慢了整个程序的执行。或者有一个死循环任务,那么其他任务也无法执行,程序无响应。

为了解决这个问题,JavaScript 将任务的执行分为同步(Synchronous)和异步(Asynchronous)两种模式。

同步也就是上面所提到的模式,后一个任务必须等待前一个任务的完成,任务的执行顺序与被调用的顺序是一致的,同步的。

异步是任务被调用后会新开一个子线程,任务在这个子线程中执行,后一个任务不需要等待前一个任务完成就可以执行。

异步带来了一个问题:由于任务是在子线程中执行的,与主线程失去了同步,主线程无法确定它的结束,如果需要处理任务结果,是无法合并到主线程中的。回调函数就是用来解决这个问题的。

回调函数

回调函数(callback)是作为参数传给另一个函数的函数。一个异步任务在结束后会调用回调函数,从而合并到主线程。

setTimeout(myCallback, 1000);

function myCallback() {
  //some code
}

在这个例子中,myCallback 被用作回调。函数(函数名)作为参数传递给 setTimeout()。setTimeout 开了一个子线程,等待一秒后执行回调函数。

:当您将函数作为参数传递时,请记住不要使用括号。

我们再来看一个定时器的例子体会一下回调函数:

setInterval(myFunction, 1000);

function myFunction() {
  let d = new Date();
  document.getElementById("demo").innerHTML=
  d.getHours() + ":" +
  d.getMinutes() + ":" +
  d.getSeconds();
}

像上面两个例子这种一次异步的还好,但多次异步呢?例如,如果我想分三次输出字符串,第一次间隔 1 秒,第二次间隔 4 秒,第三次间隔 3 秒:

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

这段程序实现了这个功能,但是它是用 "函数瀑布" 来实现的。可想而知,在一个复杂的程序当中,用 "函数瀑布" 实现的程序无论是维护还是异常处理都是一件特别繁琐的事情,而且会让缩进格式变得非常冗赘。

什么是回调地狱?回调地狱有两个主要的问题:

  1. 多层嵌套的问题;
  2. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

怎么解决回调地狱呢?ES6 标准提供了Promise。

Promise 对象

其实在 ES6 标准出现之前,社区就最早提出了 Promise 的方案,后随着 ES6 将其加入进去,才统一了其用法,并提供了原生的 Promise 对象。

Promise 的诞生就是为了解决回调地狱。Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡。

Promise 的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。先来体验下 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);
});

回调函数变成了链式写法,程序的流程可以看得很清楚。

可能你会觉得怎么这比函数瀑布写法还要长,我们来封装一下就好了:

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

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

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

用法

首先认识下 Promise对象 的三种状态:

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

Promise 对象在被创建出来时状态是 pending,

当异步操作成功时状态变化:pending——>fulfilled;

当异步操作失败时状态变化:pending——>rejected。

Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。

创建 Promise 实例

创建一个 promise 对象、可以使用 new 来调用 Promise 的构造器来进行实例化:

let promise = new Promise(function(resolve, reject) {
    /*
   	发起异步请求的代码
    */
    
    if (/*异步操作成功*/) {
        resolve(data)  //data 为异步操作的结果
    } else {
        reject(error)    // error 为异步操作的错误信息 
    }
})
  • Promise构造函数接受一个函数作为参数,并且该函数在 new 了之后会立即执行。该函数接受两个参数,分别是resolve和reject,它们是两个函数,是由 javascript 引擎提供,不用自己部署。(resolve、reject这两个方法名是约定俗成的)
  • resolve 作用是将 Promise对象状态由 pending ——> fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。
  • rejected函数则是将Promise对象状态由 pending ——> rejected,在异步操作失败时调用,并将异步操作的错误信息作为参数传递出去。

then方法

回调函数不是直接声明的,而是通过 then 方法传入的,即延迟传入,这就是回调函数延迟绑定。

then方法是定义在原型对象 Promise.prototype 上的,它的作用是为 Promise实例添加状态改变时的回调函数。

Promise实例生成后,可用then方法分别指定两种状态回调函数。then 方法可以接受两个回调函数作为参数:

  1. 第一个参数是Promise对象状态改为 fulfilled 时的回调函数
  2. 第二个参数Promise对象状态改为 rejected 时的回调函数

两者都是可选的,因此您可以为成功或失败添加回调,但两个函数只会有一个会被调用,要么成功要么失败。

promise.then(onFulfilled, onRejected)

我们使用 setTimeout() 来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法:

let promise = new Promise((resolve, reject) => {
    let status = 200;
    setTimeout(() => {
        if (status === 200) {
            resolve("success");
        } else {
            reject("fail");
        }
    }, 3000);
});
promise.then(
    data => {     //data 值是上面 resolve 传入的值
        console.log(data);
    },
    err => {   // err 值是上面 reject 传入的值
        console.log(err);
    }
)
//控制台打印 success

then 方法的特点:在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。

let promise = new Promise(function(resolve, reject){
    console.log(1);
    resolve()
});
promise.then(() => console.log(2));
console.log(3)
/*
1  
3  
2
*/

执行后,我们发现输出顺序总是 1-> 3 -> 2。表明,在 Promise 新建后会立即执行,所以首先输出 1。然后,then方法指定的回调函数将在当前脚本所有同步任务执行完后才会执行,所以 2 最后输出。

再来看下面这个例子:

let promise = new Promise(function(resolve, reject){
    console.log("1");
    resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");

/*
1
4
3
2
*/

1 和 4 的顺序不必多说。为什么是先输出 3 呢?原因是 Promise 属于 JavaScript 引擎内部任务,而 setTimeout 则是浏览器API,而引擎内部任务优先级高于浏览器API任务。

链式操作

根据 then 中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise 穿透到外层,以供后续的调用,这便是返回值穿透的效果。

Promise.prototype.then 方法返回的是一个新的 fulfilled 或 rejected 状态的 Promise 对象,因此可以采用链式写法:

let promise = new Promise((resolve, reject) => {
    resolve(1);
})
promise.then(data => {//回调函数返回2,于是then返回了一个fulfilled状态的Promise对象,其结果值是2
    console.log(data);
    return 2;    
}).then(data => { //回调函数没有return,then默认返回一个fulfilled状态的Promise对象,结果值是undefined
    console.log(data); 
}).then(data => { //回调函数抛出错误,then返回一个rejected状态的Promise对象,结果值是"fail"
    console.log(data);
    throw new Error("fail") 
}).catch(err => { //then返回一个fulfilled状态的Promise对象,结果值是undefined
    console.log(err)
});
/*
1
2
undefined
*/

这种设计使得嵌套的异步操作,可以被很容易得改写,从回调函数的"横向发展"改为"向下发展"。

这种更符合人的线性思维模式,开发体验也更好,解决了多层嵌套的问题。

另外需要注意的是如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

let p1 = new Promise((resolve, reject) => {
    resolve("1")
})
//模拟要运行很久的异步操作
let p2 = new Promise((resolve, reject) => {
    setTimeout(()=>{
        resolve(2)
    }, 10000)
})
p1.then(data => {
    console.log(data)
    return p2;
}).then(data => {
    console.log(data)
})

catch方法

catch方法是 then(null, onRejected) 的别名,用于指定发生错误时的回调函数。

下面的代码中,promise抛出一个错误,就被catch方法指定的回调函数捕获:

let promise = new Promise((resolve, reject) => {
  throw new Error('test');
});
promise.catch(err =>
  console.log(err);
});
// Error: test

如果Promise状态已经变成resolved,再抛出错误是无效的,因为Promise的状态一旦改变,就永久的保持该状态,不会再发生变化:

const promise = new Promise((resolve, reject) => {
  resolve('success');
  throw new Error('test');
});
promise.then(data => {
    console.log(value)
}).catch(err => {
    console.log(err)
});
// success

每次任务执行结束后分别处理成功和失败的情况怎么解决的呢?Promise 采用了错误冒泡的方式,这样前面产生的错误会一直向后传递,直到被 catch 接收到,就不用频繁地检查错误了。

promise.then(data => {
    //process data
}).then(data => {
    //process data
}).then(data => {
    //process data
}).catch(err => {
    //handle err
});

上面的例子中,promise 和 三个 then 的回调函数中任何一个抛出错误,都会被 catch 捕获到。

实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题。

:如果设置 then 中的第二个回调函数,那么错误将进入这个回调函数中处理,而不会被 catch 捕获。

有些错误(比如断网等)只会进入catch而不进入第二个回调。

一般来说,不要在 then 方法里面定义 rejected 状态的回调函数(即 then 的第二个参数)而是使用 catch 方法。

finally方法

finally() 是不管 Promise 实例的状态是fulfilled还是rejected,它都一定会执行:

let promise = new Promise((resolve, reject) => {
    resolve("success")
})
promise.then(data => {
    console.log(data)
}).catch(err => {
    console.log(err)
}).finally(() => {
    console.log("END")
})
/*
success
END
*/

Promise 静态方法

Promise.resolve和Promise.reject

当需要将现有对象转为Promise对象,这两个方法就起到这个作用。

如果 Promise.resolve 方法的参数不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为fulfilled。Promise.resolve方法的参数就是回调函数的参数:

let p = Promise.resolve("success")
p.then(function(res){
  console.log(res)
}) 
//success

如果Promise.resolve方法的参数是一个Promise对象的实例,则会被原封不动地返回。

let p1 = new Promise((resolve,reject)=>{
    reject("success")
})
const p2 = Promise.resolve(p1)
p.then(function(res){
  console.log(res)
}).catch(err=> console.log(err))
//success

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

let p = Promise.reject('fail');
 
p.catch(err => {
    console.log(s)
});
// fail

Promise.all(iterable)

该方法参数是一个可迭代对象,如 Array。

在 ES6 中可以通过 Promise.all 将多个异步请求并行操作,返回结果一般有下面两种情况。

  1. 当所有结果成功返回时按照请求顺序返回成功。
  2. 当其中有一个失败方法时,则进入失败方法。
let p1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let p2 = new Promise((resolve, reject) => {
  resolve('success')
})

let p3 = Promise.reject('失败')

Promise.all([p1, p2]).then((result) => {
  console.log(result)               //['成功了', 'success']
}).catch((error) => {
  console.log(error)
})

Promise.all([p1,p3,p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)      //  '失败' 有一个失败就返回reject传回的信息
})

对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all 来实现可能效果会更好:

//获取轮播列表
function getBannerList() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {resolve("轮播数据")}, 1000)
    })
}

//获取店铺列表
function getStoreList() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {resolve("店铺数据")}, 2000)
    })
}

//获取分类列表
function getCategoryList() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {resolve("分类数据")}, 3000)
    })
}

function initLoad() {
    Promise.all([getBannerList(), getStoreList(), getCategoryList()])
    .then(data => {
        console.log(data);
    }).catch(err => {
        console.log(err);
    })
}
initLoad() //['轮播数据', '店铺数据', '分类数据']

从上面代码中可以看出,在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 Promise.all 来实现,看起来更清晰、一目了然。


Promise.allSettled(iterable)

Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,但返回的是一个新的 Promise,而且执行完之后不会失败,我们可以拿到每个 Promise 的状态,不管其是否处理成功。

let p1 = Promise.resolve("success");
let p2 = Promise.reject("fail");
let allSettledPromise = Promise.allSettled([p1, p2]);
allSettledPromise.then(res => {
    console.log(res);
});
// [{status: 'fulfilled', value: 'success'}, {status: 'rejected', reason: 'fail'}]


Promise.any(iterable)

any 方法返回一个 Promise,只要参数中的 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态。

let p1 = Promise.resolve("fail");
let p2 = Promise.reject("success 2");
let p3 = Promise.reject("success 3");
let anyPromise = Promise.any([p1, p2, p3]);
anyPromise.then(res => console.log(res)) //success 2

根据参数中Promise实例的顺序,返回值是遇到的第一个fulfilled状态的Promise的返回值。


Promise.race(iterable)
race 方法返回一个 Promise,只要参数的 Promise 实例之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功')
  },1000)
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('失败')
  }, 500)
})

Promise.race([p1, p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)  //  '失败'
})

我们来看一下业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断:

//请求图片资源
function requestImg() {
    return new Promise((resolve, reject) => {
        var img = new Image();
        img.onload = () => resolve(img);
        img.src = "https://th.wallhaven.cc/small/72/72rxqo.jpg"
    })
}

function timeout() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("图片请求超时")
        }, 5000)
    })
}

Promise.race([requestImg(), timeout()])
.then(data => {
    console.log(data)
}).catch(err => {
    console.log(err)
})

如果 Promise.all 方法和 Promise.race 方法的参数不是Promise实例,就会先调用 Promise.resolve 方法,将参数转为Promise实例,再进一步处理。

优缺点

优点:

  • Promise 对象可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
  • Promise 对象提供统一的接口,使得控制异步操作更加容易。

缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
  • 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

工作原理

下面将简单描述下 Promise 的工作原理,想具体了解可去研究源码。

function Promise(fn) {
    var PENDING = 0;
    var FULFILLED = 1;
    var REJECTED = 2;
    // Promise的状态:PENDING, FULFILLED or REJECTED
    var state = PENDING;

    // 状态FULFILLED 或 REJECTED 时的值
    var value = null;

    // 保存then方法传入的回调函数
    var handlers = [];
    
    //执行fn
    fn(resolve,reject);
    
    /*
    other code
    */
}

实例化Promise时 fn 会立即执行,函数中的异步操作执行完成后,成功则调用 resolve 函数,失败则调用 reject 函数。

resolve(data) 函数会将 state 改为 FULFILLED,value 改为 data。

reject(err) 函数会将 state 改为 REJECTED,value 改为 err。

然后 Promise 对象调用 then 方法,可以传入两个回调函数,回调函数会保存在 handlers 中。

由于 fn 函数中的任务是异步的,我们并不知道 resolve/reject 函数是否被调用,什么时候被调用。

但是没有关系,promise.then() 和 resolve() 谁先执行都可以。

如果 resolve() 先执行,将状态改为 fulfilled,then 执行的时候异步调用回调函数。

如果 resolve()后执行,then 会将回调函数保存在handlers,resolve()执行时会异步调用回调函数。

回调函数的传入参数为 value值。

Async

异步函数(async function)是 ECMAScript 2017 (ECMA-262) 标准的规范,几乎被所有浏览器所支持,除了 Internet Explorer。

async函数是对 Generator 函数的改进

async 关键字

语法:

async function name([param[, param[, ... param]]]) { statements }
  • name: 函数名称。
  • param: 要传递给函数的参数的名称。
  • statements: 函数体语句。

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

async function helloAsync(){
    return "helloAsync";
}

console.log(helloAsync())  // Promise {<fulfilled>: "helloAsync"}
 
helloAsync().then(data => {
    console.log(v);      // helloAsync
})

如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象;
如果没有 return,那么返回 Promise {<fulfilled>: undefined}

async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。

await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误。

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

await 操作符

语法:

[return_value] = await expression;

expression可以是 一个 Promise 对象或者任何要等待的值。

  • Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。
  • 非 Promise 对象:直接返回对应的值。

另外需要注意的是执行顺序:

async function a(){  
  console.log("1")  
   console.log("2")
}
a()
console.log("3")
//打印: 1 2 3 

使用 await

async function a(){  
  await 1  
  console.log("1")  
  console.log("2")
}
a()
console.log("3")
//打印: 3 1 2

使用 await 后会等当前脚本所有同步任务执行完后才会执行之后的任务。


异步函数是用来干嘛的呢?

进一步简化Promise。

前面所提到的Promise实现分三次输出字符串不知还是否记得,我们可以更进一步简化:

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

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

异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于程序员阅读。

处理异常的机制将用 try-catch 块实现:

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

参考文章

posted @   hzyuan  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)

喜欢请打赏

扫描二维码打赏

支付宝打赏

点击右上角即可分享
微信分享提示