Javascript异步编程
持续更新,文章内容较长,建议收藏食用,如果觉得内容可以,求点赞哦。本文章总结自拉钩教育-大前端高薪训练营(PS:班班让我加的~)
文章目录
概述
众所周知,目前主流的javascript环境都是以单线程模式执行javascript代码,而采用单线程模式的原因,跟它的设计初衷有关,javascript最早是运行在浏览器端的脚本语言,主要用来实现页面上的动态交互,而页面上的交互主要就是实现dom操作,而这就决定了javascript必须采用单线程模式,否则就会出现复杂的线程同步问题,试想一下,如果采用多线程模式,一个线程对dom进行了取值操作,另一个线程对这个dom进行了删除操作,这个在实际场景中就会出问题,因为浏览器不知道应该以哪个线程的结果为准,而为了避免这种问题,所以javascript最开始就设计成了单线程模式,来解决这种问题。单线程模式的优点就是代码从上到下依次执行,安全简单,缺点同样也很明显,就是如果有一个任务非常耗时,那么后续任务必须等待这个任务结束才能继续执行。而为了解决耗时任务导致的问题,javascript将任务的执行模式分成了同步模式和异步模式。
同步模式
同步模式,就是后一个任务必须等待前一个任务执行完才能开始执行,不论前一个任务耗时多久,也就是说,同步任务的执行顺序,跟我们代码编写的顺序是完全一致的。
console.log('start'); // 压入到调用栈,执行完弹出调用栈
function foo() { // 函数声明不会产生调用
console.log('foo')
}
function bar() { // 函数声明不会产生调用
foo();
console.log('bar')
}
bar(); /* bar函数执行,压人调用栈,而bar函数里面又调用了foo函数,
所以继续把foo函数压入调用栈,foo函数执行完输出foo
,然后弹出调用栈,接下来,输出bar,bar函数执行完,然后bar函数弹出调用栈
*/
console.log('end'); //压入调用栈,然后弹出
输出顺序跟我们书写代码执行顺序完全一致
异步模式
掌握异步编程,需要先掌握几个概念:消息队列、Event loop、宏任务、微任务。
异步模式概念
不会等待当前任务完成,直接执行下一个任务,当前任务的后续操作会以回调函数的方式定义,当前任务完成之后,自动执行回调函数。优点就是不会阻塞进程,也就是说不会让页面进入假死状态。缺点就是代码执行顺序混乱,需要多加练习才能掌握。
- 消息队列:暂时存储异步操作的地方。
- Eventloop:监听调用栈和消息队列,当调用栈清空之后,会将消息队列中的第一个任务推到调用栈中执行。
- 宏任务:每次调用栈中的中的执行代码就是宏任务。例如setTimeout
- 微任务:当前任务执行结束之后立即执行的任务就是微任务。例如promise
console.log('start');
setTimeout(function timer1() {
console.log('time1')
}, 1800);
setTimeout(function time2() {
console.log('timer2')
setTimeout(function time3() {
console.log('timer3');
}, 1000)
}, 1000)
console.log('end');
输出结果
可能比较难理解,可以看下面这张图来帮助理解。
回调函数
回调函数是所有异步编程方案的根本。回调函数可以理解为,你想要做的一件事情,你知道这件事情该怎么做,并且,你也知道这个事情应该在哪件事情之后做,但是你不知道的是,上一件事情需要花费多久才能完成,偏偏你又想继续做其他事情,不想傻等着上一件事情结束。所以最好的办法是,你把这件事情的执行步骤记录下来,交给任务的执行者,而任务的执行者是知道上一件事情什么时候完成,等上一件事情完成之后,任务的执行者就会将你想做的这件事情完成。
- 优点:非阻塞
- 缺点:回调地狱
Promise
为了避免回调地狱,CommonJs提出了Promise规范,后来在ES2015中被标准化,成为ECMAScript语言规范。Promise有3中状态,Pending(等待)、Fulfilled(成功)、Rejected(失败)。初始状态为Pending,可以变成成功状态Fulfilled,执行一个成功回调onFufilled;也可以变为失败状态Rejected,然后执行一个失败回调onRejected。需要注意的是,Promise的状态一旦变化,不可二次更改。promise的本质就是定义异步任务结束之后需要执行的任务。
简单示例
const promise = new Promise((resolve, reject) => {
resolve('100'); // 成功回调
// reject(new Error('promise rejected')); // 失败回调
});
promise.then((value) => {
console.log(value); => 100
}, (error) => {
console.log(error);
})
const promise = new Promise((resolve, reject) => {
// resolve('100'); // 成功回调
reject(new Error('promise rejected')); // 失败回调
});
promise.then((value) => {
console.log(value);
}, (error) => {
console.log(error); // => promise rejected
})
const promise = new Promise((resolve, reject) => {
resolve('100'); // 成功回调
reject(new Error('promise rejected')); // 失败回调
});
promise.then((value) => {
console.log(value); // => 100
}, (error) => {
console.log(error);
})
通过new关键字来创建Promise实例,promise.then接收2个函数参数,第一个函数执行成功的回调,第二个参数执行失败的回调。从上面例子也可以看出,当执行resolve的时候,then会执行第一个成功回调;当执行reject的时候,then会执行第二个失败回调;并且当resolve和reject同时执行的时候,then只会执行第一个成功,并没有执行第二个失败的回调,这也可以总结出,Promise的状态一旦变化,不可二次更改。
Promise封装ajax
const ajax = url => {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText));
}
}
xhr.send();
})
}
ajax('/foo.json').then((value) => {
console.log(value);
}, (reason) => {
console.log(reason);
});
Promise链式调用
promise的then可以链式调用,then返回的是一个全新的promise对象,每个then方法都是为上一个then方法返回的promise对象状态确定之后,指定回调函数,这些promise会依次执行,所以这些回调函数也会依次执行,我们也可以在回调函数中手动返回一个promise对象。
const ajax = url => {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText));
}
}
xhr.send();
})
}
const promise1 = ajax('/foo.json')
console.log(promise) // => 返回一个promise对象
const promise2 = promise1.then(function() {
}, function() {
})
console.log(promise2) // => 返回一个promise
console.log(promise1 === promise2); // => 返回false,说明then方法返回的是一个全新的promise对象
ajax('/foo.json').then(res => {
console.log(11111)
return ajax('/user.json')
})
.then(res => {
console.log(22222)
console.log(res)
})
.then(res => {
console.log(33333)
})
相比传统回调函数的写法,promise链式调用在写法和可读性上面有很大的提升。
Promise并行执行
Promise里面提供了一个all和race方法,可以让我们并行执行接口请求。不同的是,all方法是等所有的请求都成功完成之后才执行then方法,并且一旦有一个请求失败,整个Promise执行就是失败的。而race方法是,只要有一个请求完成,就会直接调用回调函数。
Promise执行时序/宏任务VS微任务
console.log('start');
setTimeout(function() {
console.log(2222)
}, 0)
Promise.resolve().then(res => {
console.log(11111)
})
console.log('end')
从输入结果可以看出,promise的回调是作为微任务来执行的。
Promise总结
- Promise对象的then方法防御和一个全新的Promise对象
- 后面的then方法就是在为上一个then返回的Promise注册回调
- 前面then方法中回调函数的返回值会作为后面then方法回调的参数
- 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束
强烈建议,每个then方法都写一下第二个失败的回调函数,这样就算promise内部出现问题,也不会阻塞我们程序的执行。
Generator异步方案
ES2015提供的Generator(生成器函数),通过*来标记生成器函数,生成器函数调用的时候不会立即执行,而是会返回一个生成器对象,必须执行生成器对象的next方法之后才会执行。
function * foo() {
console.log('start');
}
const generator = foo(); // => 不会打印出start
generator.next(); // => start
并且我们可以在生成器函数的内部,使用yield来指定函数的返回值。
function * foo() {
console.log('start');
yield 'aaa'
}
const generator = foo();
const result = generator.next();
console.log(result)
如果next方法里面传入一个参数的话,我们可以在生成器函数里面捕获这个参数。
function * foo() {
console.log('start');
const res = yield 'aaa'
console.log(res)
}
const generator = foo();
generator.next()
generator.next('bbb')
还可以通过try,catch来捕获异常操作
function* foo() {
try {
const res = yield 'aaa';
console.log(res)
} catch (e) {
console.log(e)
}
}
const generator = foo();
generator.next()
generator.throw(new Error('失败了'))
生成器函数,会在yield关键词处暂停执行,如果想让它继续执行,需要继续调用next方法。
Generator实现异步
const ajax = url => {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText));
}
}
xhr.send();
})
}
function * main() {
const res = yield ajax('/foo.json');
console.log(res);
const res2 = yield ajax('/foo2.json');
console.log(res2);
}
const g = main();
const result = g.next();
result.value.then(res => {
const result2 = g.next(res);
if (result.done) return;
console.log(res)
result2.value.then(res2 => {
console.log(res2)
})
})
思考一下上面为什么返回3个结果。
接下来通过递归来改造一下上面的异步操作。
const ajax = url => {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText));
}
}
xhr.send();
})
}
function* main() {
try {
const res = yield ajax('/foo.json');
console.log(res);
const res2 = yield ajax('/foo22.json');
console.log(res2);
} catch (e) {
console.log(e)
}
}
const g = main();
function handleResult(result) {
if (result.done) return;
result.value.then(res => {
handleResult(g.next(res))
}, err => {
g.throw(err)
})
}
handleResult(g.next())
Async/Await语法糖
有了generator之后呢,其实异步操作写起来就已经非常像同步函数了,但是我们每次用的时候,都要自己封装生成器函数,不是很方便。而在ECMAScript2017中新增了一个async的函数,是语言层面的异步编程语法。可以大大简化我们的代码,我们用async来改写一下上面的代码
const ajax = url => {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText));
}
}
xhr.send();
})
}
async function main() {
try {
const res = await ajax('/foo.json');
console.log(res);
const res2 = await ajax('/foo22.json');
console.log(res2);
} catch (e) {
console.log(e)
}
}
main()
本文章总结自拉钩教育-大前端高薪训练营(PS:班班让我加的~)