当我们谈论JS层面的异步时,不得不谈JS单线程模型和事件循环机制。这是JS异步概念的来源。
JS的单线程模型意味着,在执行JS时只有一个主线程,每个任务必须顺序执行。如果当前任务执行时间过长,会导致接下来的所有任务都处于阻塞状态,进而导致浏览器卡死等我们不希望看到的状况。为了解决这一问题,事件循环机制(Event Loop)被发明出来。
事件循环机制中,负责执行JS脚本的单线程我们称为主线程,在内存中表现为一个执行栈,JS只通过主线程执行任务。异步任务被挂起,存储在堆中,当异步任务准备就绪,它对应的事件便进入任务队列。主线程首先执行同步任务,然后查看任务队列是否有就绪的异步任务(或者时间到了的异步任务),调用相应的回调函数执行,直到任务队列为空。至此即完成一个事件循环。如下图所示。
大家都知道做前端开发的时候最让人头痛的就是处理异步请求的情况,在请求到的成功回调函数里继续写函数,长此以往形成了回调地狱。
回调函数(需要得到一个函数内部异步操作的结果)
// 一种数据类型
// 参数
// 返回值
// 函数太灵活了,无所不能
// 一般情况下,把函数作为参数的目的就是为了获取函数内部的异步操作结果
// JavaScript 单线程、事件循环
function add(x, y) {
console.log(1)
setTimeout(function () {
console.log(2)
var ret = x + y
return ret
},1000)
console.log(3)
// 到这里执行就结束了,不会等到前面的定时器,所以直接就返回了默认值 undefined
}
console.log(add(10,20)) // undefine
// 执行结果: 1 3 undefine 2
function add(x, y) {
var ret
console.log(1)
setTimeout(function () {
console.log(2)
ret = x + y
},1000)
console.log(3)
return ret
// 到这里执行就结束了,不会等到前面的定时器,所以直接就返回了默认值 undefined
}
console.log(add(10,20)) // undefine
// 执行结果: 1 3 undefine 2
使用回调函数解决(callback)
function add(x, y,callback) {
// callback 就是回调函数
// var x = 10
// var y = 209
// var callback = function () { console.log(ret) }
console.log(1)
setTimeout(function () {
console.log(2)
var ret = x + y
callback(ret)
},1000)
console.log(3)
// 到这里执行就结束了,不会等到前面的定时器,所以直接就返回了默认值 undefined
}
add(10,20,function (ret) {
console.log(ret)
// 我现在可以拿到这个结果进行任何操作
})
// 注意:凡是需要得到一个函数内部异步操作的结果
// setTimeout
// readFile
// writeFile
// ajax
// 这种情况必须通过:回调函数
问题:
多个异步操作无法保证执行顺序(异步操作之间没啥关系,同时可以进行多个操作)
比如下面的 nodejs 读取文件,无法确定哪个先读取,所以每次打印的结果可能会不一样
所以:
可以通过回调嵌套的方式来保证顺序
嵌套层级少了当然还是可以凑合看的,但是多起来的话,
因此,为了解决以上编码方式带来的问题(回调地狱嵌套),所以在EcmaScript 6 中新增一个api: promise
Promise 是异步编程的一种解决方案,所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息;ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
《前端工程师必知之Promise的实现》、《ES6 Promise 用法讲解》、《promise 中的错误处理》、《阮一峰的es6之Promise》、《promise的.then返回的一个新promise,他的状态和值相关问题?》、《聊一聊看似简单的Promise.prototype.then()方法》
promise 就是为了解决回调地狱问题而出现的,是 承诺、保证的意思,。
Promise有三种状态:
Pending(进行中)
:创建Promise对象时的初始状态,异步任务正在进行Fulfilled(已完成)
:异步任务执行成功时的状态Rejected(已失败)
:异步任务执行失败时的状态Promise 接收一个函数作为参数,这个函数有两个参数
resolve
:将Promise对象的状态从 Pending
变为 Fulfilled
reject
:将Promise对象的状态从 Pending
变为 Rejected
状态只能由 Pending
变为Fulfilled
或由Pending
变为Rejected
,且状态改变之后不会在发生变化,会一直保持这个状态。
一些方法:
resolve(fn)
:也可以创建一个 Promise 对象,返回一个promise对象reject
:也会返回一个新的 Promise 实例,该实例的状态为rejected
。then
:注册状态改变时的回调函数。他返回的是一个新的Promise实例(不是原来那个Promise实例),只要then方法中的程序正常执行完不报错,返回新实例的状态就为 resolved
。链式调用时,每个then里面都要返回一个Promise
对象then 方法中前一个回调函数的返回值可以传递给下一个回调函数。
1、前一个回调函数的返回值是一个非promise实例时(原始值),则直接传递原始值
2、当前一个回调函数的返回值是一个promise实例时,下一个then方法的执行情况要根据这个promise实例的状态来执行(也就是链式调用异步函数)
catch
:结合reject()
进行异常捕获,可以用来指定reject
的回调,而且可以处理前一个resolve
回调函数运行时发生的错误all
:并行执行多个异步操作,并且在所有异步操作执行完后才执行回调。如果全部成功执行,则以数组的方式返回所有执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。race
:all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」。返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败finally
:用于指定不管 Promise 对象最后状态如何,都会执行的操作注意点:
catch
方法,而不使用then
方法的第二个参数。因为这样可以捕获前面then
方法执行中的错误Promise
内部的错误不会影响到 Promise
外部的代码,通俗的说法就是“Promise 会吃掉错误”Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。错误总是会被下一个catch语句捕获。也就是说,如果错误已经捕获了,那么错误不会继续传递下去,如果错误没有被捕获,那么错误会隐式传递下去,直到有错误处理函数来捕获这个错误Promise
对象后面要跟catch
方法,这样可以处理 Promise
内部发生的错误。catch
方法返回的还是一个Promise
对象,因此后面还可以接着调用then
方法。 // then方法是用的最多的了
// 按照then来执行成功和失败的回调函数
function load() {
return new Promise((resovel, reject) => {
$.ajax({
url: 'xxx.com',
data: 'jsonp',
success: function(res) {
resolve(res);
},
error: function(err) {
reject(err);
}
});
});
}
// 用一下
load().then(data => {
console.log(data); // 请求到的数据
console.log('请求数据成功');
}, err => {
console.log('请求失败');
});
注意看了下,第二个then没有返回promise,所以第三个then不会等待就直接执行的。2,3同时执行的情况下,由于2 中有settimeout延时执行,所以延时的输出log。如果需要一个then一个then的跑,每个then都返回一个promise吧。
Axios
:Axios
是一个基于 promise 的 HTTP 库,支持promise所有的API
《axios原理及面试题》、《vue中axios封装和api接口管理、登陆拦截鉴权》
get请求
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
}).then(res => {
resolve(res.data);
}).catch(err =>{
reject(err.data)
})
});
}
post请求
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, qs.stringify(params))
.then(res => {
resolve(res.data);
})
.catch(err =>{
reject(err.data)
})
});
}
比如用户想请求url1接口完后再调url2接口(接口1返回的结果作为接口2的参数)
var promise = new Promise((resolve,reject)=>{
let url1 = '/toutiao/index?type=top&key=秘钥'
this.get(url,{
})
.then((res)=>{
resolve(res);
})
.catch((err)=>{
console.log(err)
})
});
promise.then((res)=>{
let url2 = '/toutiao/index?type=top&key=秘钥'
this.get(ur2,{
})
.then((res)=>{
//只有当url1请求到数据后才会调用url2,否则等待
resolve(res);
})
.catch((err)=>{
console.log(err)
})
})
Promise 的方式虽然解决了 callback hell,但是这种方式充满了 Promise的 then() 方法,如果处理流程复杂的话,整段代码将充满 then。语义化不明显,代码流程不能很好的表示执行流程。
因此 Generator 就因运而生
Generator 方式:
/**
* Generator 方式
*/
function* fetchUserByGenerator() {
const user = yield fetchUser();
return user;
}
const g = fetchUserByGenerator();
const result = g.next().value;
result.then((v) => {
console.log(v);
}, (error) => {
console.log(error);
})
Generator 的方式解决了 Promise 的一些问题,流程更加直观、语义化。但是 Generator 的问题在于,函数的执行需要依靠执行器,异步操作需要暂停的地方,都用 yield 语句注明。每次都需要通过 g.next() 的方式去执行。(next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值)
因此 async+await 就出现了
理解 async/await
async 方式:
/**
* async 方式
*/
async function getUserByAsync(){
let user = await fetchUser();
return user;
}
getUserByAsync()
.then(v => console.log(v));
async 函数完美的解决了上面两种方式的问题。流程清晰,直观、语义明显。操作异步流程就如同操作同步流程。同时 async 函数自带执行器,执行的时候无需手动加载。让代码执行起来像同步一样
async 函数是 Generator 函数的语法糖。使用 关键字 async 来表示,在函数内部使用 await 来表示异步。async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。
async 函数的实现原理,就是将 Generator 函数和自动执行器co
模块,包装在一个函数里。co就是用于 Generator 函数的自动执行,内部原理是基于Promise对象
想较于 Generator,Async 函数的改进在于下面四点:
co 模块
约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)Async/await 和 Promises 区别
与Promise血脉相连的async/await
在我们处理异步的时候,比起回调函数,Promise的then方法会显得较为简洁和清晰,但是在处理多个彼此之间相互依赖的请求的时候,就会显的有些累赘。这时候,用async和await更加优雅,
reject()
+ .catch()
,但是在 Async/await 中可以像处理同步代码处理错误,使用try、catch()
《try catch 捕获不到异步错误》
ps: `try-catch----只能处理同步异常,因为try和catch是在当前调用栈里,遇到setTimeout的时候,把里面的回调函数放在了任务队列里了,try结束未发现异常也就不执行catch了,当调用栈执行结束,开始任务队列里的代码,这个时候抛出了错误,但已经没有接受此错误的地方了,因此报错
async
/await
的具体使用规则:
《神三元博客》
问题:对于异步代码,forEach 并不能保证按顺序执行。
forEach
底层实现:
// 核心逻辑
for (var i = 0; i < length; i++) {
if (i in array) {
var element = array[i];
callback(element, i, array);
}
}
可以看到,forEach 拿过来直接执行了,这就导致它无法保证异步任务的执行顺序。比如后面的任务用时短,那么就又可能抢在前面的任务之前执行。
利用for...of
就能轻松解决
解决原理:其实for...of
并不像forEach
那么简单粗暴的方式去遍历执行,而是采用一种特别的手段——迭代器去遍历。通过.next()
来保证执行顺序
假设有这样一个需求:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?三个亮灯函数已经存在:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
这道题复杂的地方在于需要“交替重复”亮灯,而不是亮完一遍就结束的一锤子买卖,我们可以通过递归来实现:
// 用 promise 实现
let task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
if (light === 'green') {
green()
}
if (light === 'yellow') {
yellow()
}
resolve()
}, timer);
})
}
let step = () => {
task(3000, 'red')
.then(() => task(1000, 'green'))
.then(() => task(2000, 'yellow'))
.then(step)
}
step()
// async/await 实现
let step = async () => {
await task(3000, 'red')
await task(1000, 'green')
await task(2000, 'yellow')
step()
}
step()