异步操作
JavaScript的执行环境是“单线程”的,一次只能执行单个任务,如果有多个任务要处理,那么后续任务需要等待前一个任务完成才能执行。
这样的运行模式运行起来简单,但是如果前面的任务假死了,那么后面的任务就长时间无法执行了。所以我们引入了”同步模式“和”异步模式”的执行模式。
异步操作的一个典型例子就是ajax,当发起ajax请求并获取到服务端的数据后,再通过返回的结果做出相应的处理。
对比“同步模式”和“异步模式”,其实简要的概括就是代码顺序和任务执行顺序是否一致
- 同步模式:后面的任务等待前面的任务执行完毕,然后再执行,执行顺序和所有任务的排列顺序一致。
- 异步模式:每个任务都有一个或多个回调函数,前面的任务运行结束后不会马上执行后面的任务,而是先执行前面的那个任务的回调,后面的任务则不等前面的任务结束就执行。这样看起来,所有的任务执行顺序和排列顺序是不一致的。
同步任务在主线程上排队执行的任务。
异步任务不马上进入主线程,进入任务队列。只有等主线程任务执行完毕,任务队列通知主线程某个异步任务可以执行了,该异步任务才会进入到主线程执行。
异步任务通常可以分为两大类:I/O 函数(AJAX、readFile等)和计时函数(setTimeout、setInterval)
其实都是带回调
传统模式下的异步编程,主要是靠回调函数、事件监听来完成,后来引入了Promise
,我们可以避免了传统回调函数的回调地狱,再后来引入了async
和await
,我们就可以避免Promise
带来的多个then
方法。
回调函数
基本的样子:
step1(function(result1){
step2(function(result2){
step3(function(result3){
//...
})
})
})
如果回调多了就看起来很乱。
setTimeout
一个demo:
for (var i=1;i<=2;i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
console.log(i);
结果是先输出一个2,等待1s后输出两个2,原因:
- i作用域在setTimeout回调函数内部,一旦脱离该作用域即失效,所以只保留最后的结果
- 根据前面已知,循环结束后,i等于2,输出也就是2
- JavaScript事件处理器在现场空闲之前不会运行,setTimeout里的回调是异步任务,它加入到任务队列中,当执行完外层的输出(也就是主线程执行完毕),任务队列里面的任务才会执行,所以这才紧接着输出两个2
另一个demo:
var aa;
var test1 = () => {
setTimeout(() => {
aa = 1
console.log(aa)
aa += 1
},0)
// console.log('y') // 它不会加到任务队列
}
var test2 = () => {
if (aa !== undefined)
console.log(aa);
else
console.log('oops')
}
test1();
test2();
上述结果先输出oops
,然后输出1
,也就是先输出了test2
内部的内容,然后在输出test1
的,由于变量aa
是在test1
中赋值的,所以test2
在没有取得aa
的准确值前都是undefined
这段代码的运行流程是这样的:首先test1
的setTimeout内的回调投入到任务队列中,主线程发现还有个test2
任务要处理,那么先处理test2
,test2
没有获取到变量值,输出了oops
,然后主线程没有什么任务要处理了,任务队列就通知它那就把我的事情解决了吧,此时就调用test1
搁置在队列中的内容
var aa;
var test1 = (fn) => {
setTimeout(() => {
aa = 1
console.log(aa)
aa += 1
fn()
},0)
// console.log('y')
}
var test2 = () => {
if (aa !== undefined)
console.log(aa);
else
console.log('oops')
}
test1(test2);
这样,分别输出1
和2
test1
含有setTimeout,那么setTimeout内部的回调任务会加到任务队列中,然后在运行的时候发现除了test1
之外也没有其他占用主线程的任务了(test2
已经作为了test1
的回调了,当然也是投入到任务队列中),任务队列就通知主线程请求执行任务。
事件监听/订阅、发布
同样的,如果是事件监听,其内部的回调也会被加到任务队列里面,所以要等到主线程的其他任务都处理完了才会处理其中的回调。
注意:CSS动画不算在主线程任务里面,就像下面的例子那样,在变色还没结束前点击目标dom还是会执行事件监听内部的回调的
// html部分
<body>
<div id="div1"></div>
<div id="div2"></div>
</body>
// css部分
#div1 {
width: 100px;
height: 100px;
background-color: #ff0000;
}
#div2 {
width: 200px;
height:200px;
background-color: #32442d;
animation: change 5s linear;
}
@keyframes change {
0%{
background-color: #435523;
}
50%{
background-color: #ff0000;
}
100%{
background-color: #000;
}
}
// js部分
document.getElementById('div1').addEventListener('click',() => {alert('div1')});
document.getElementById('div2').className = 'test';
Promise
Promise对象可以传入一个函数作为参数,这个函数又可以传入两个参数,分别是resolve
和reject
,分别代表异步操作执行成功后的回调函数和失败后的回调函数
Promise在实例化之后会马上运行,可以将实例化部分代码封装在一个函数中,比如:
function test() {
return new Promise((resolve, reject) => {
// do something
if(true) { // 事件处理成功
resolve(data);
} else { // 处理失败
reject(data);
}
})
}
then方法
test().then((data) => {
console.log(data); // 可以使用前面异步操作的数据
})
当test
执行完毕后调用then
方法,then
就相当于回调函数
Promise 有三种状态:pending
(进行中)、fulfilled
(成功)和 rejected
(已失败)
其中,pending
是不定态,它可以转变为其他两个状态中的任意一个,fulfilled
和rejected
都是最终的确定态,一旦pending
转变为确定态,那么就不会再发生状态转变。
demo:
function test() {
return new Promise((resolve, reject) => {
let n = Math.ceil(Math.random() * 10);
if (n < 5) {
resolve(n);
} else {
reject('out of range');
}
})
}
test().then((n) => {
console.log(n);
}, (reason) => {
console.log(reason);
})
如果随机数小于5,那么将输出这个随机数;如果大于5,那么输出out of range
catch方法
catch
方法实际上就等同于promise.then(undefined, onRejected)
,也就是用来处理失败回调函数失败后的回调函数,并且,还能即使捕获resolve
回调中的错误,一旦出错,不会阻塞,立马执行catch
中的回调。
比如前面的方法我们可以改写一下:
test().then((n) => {
console.log(n);
}, (reason) => {
console.log(reason);
}).catch((err) => {
console.log(err);
})
then
本身也可以传入失败的回调,那么和用catch
有什么区别?
- 使用
promise.then(onFulfilled, onRejected)
的话,在onFulfilled
中发生异常的话,在onRejected
中是捕获不到这个异常的。 - 在
promise.then(onFulfilled).catch(onRejected)
的情况下then
中产生的异常能在.catch
中捕获 .then
和.catch
在本质上是没有区别的,需要分场合使用。
all方法
它接收一个数组参数,数组每一项返回的都是promise对象,只有数组内所有元素都执行完才会进入then
回调(如果有其中一项出现了失败,那么最终的结果就是失败的),然后每一项返回的数据都会以一个数组的形式传递到then
回调中
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 1000);
})
.then(result => result)
.catch(err => err);
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 1000);
})
.then(result => result)
.catch(err => err);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(err => console.log(err));
上述代码在3s后输出['p1','p2']
一个更加明显的例子:
function timeDelay(delay) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(delay);
}, delay);
});
}
var start = Date.now();
Promise.all([
timeDelay(1000),
timeDelay(2000),
timeDelay(3000),
timeDelay(4000)
]).then((val) => {
console.log(Date.now() - start + 'ms');
console.log(val);
});
最后在4s后输出[1000,2000,3000,4000]
。由此可以看出,传递给 Promise.all
的promise
并不是一个个的顺序执行的,而是同时开始、并行执行的。如果是串行处理,那么等待时间就是总和,即为10s。
race方法
它和all
方法很像,不同之处在于all方法需要每一项都返回了成功结果才会执行then
,而race
则是只要其中任意一项返回成功即执行then
回调
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 1000);
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 3000);
})
.then(result => result);
Promise.race([p1, p2])
.then(result => console.log(result));
// 等待1秒后输出 'p1'
这样看起来,如果只要并行任务中一旦有其中一项运行完成,不等待其他任务结束就直接调用了then
Generator函数
使用generator函数可以避免promise带来的一大堆then方法
基本的格式:
- function关键字和函数名之间有一个
*
- 函数内部用
yield
,定义不同的内部状态
demo:
function* g() {
yield 'a';
yield 'b';
return 'end';
}
var gen = g();
每次调用gen.next()
的时候,分别输出a
、b
、end
;
后面再调用就输出undefined
循环输出20以内斐波拉切数:
function* fib() {
let [prev, curr] = [0, 1];
for(;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fib()) {
if (n > 20) break;
console.log(n);
}
逐行读取文本文件:
function* reader() {
let file = new FileReader("text.txt");
try {
while(!file.eof) {
yield parseInt(file.readline(), 10);
}
} catch(e) {
console.log(e);
} finally {
file.close()
}
}
这部分内容不细说,更详细内容可以看:
阮一峰 Generator 函数的语法
async/await
async/await就是generator函数的语法糖
如何体现?
比如这个demo:
var plugin = require('xxx');
var doSomethings = ((args) => {
return new Promise((resolve, reject) => {
plugin.Func(args, (data, error) => {
if (error)
reject(error);
resolve(data);
});
});
});
var gen = function* (){
var f1 = yield doSomethings('a');
var f2 = yield doSomethings('b');
}
如果是写成async
:
var plugin = require('xxx');
var asyncFunc = async function() {
var f1 = await plugin.Func('a');
var f2 = await plugin.Func('b');
}
对比一下前后的区别,async就是把generator函数的*
替换为async
,将yield
替换为await
它的实现,就是将generator函数和自执行器放在一个函数内
async function func(args){
//...
}
// 等同于
function func(args) {
return fn(function* (){
//...
});
}
这里的fn
函数就是执行器
,其实其内部也是返回了一个Promise
对象
你可以这样测试:
async function test() {
return 'test'
}
var res = test();
console.log(res);
结果将打印一个Promise对象。
如果在async函数中返回一个直接量,那么async会将其通过Promise.resolve()
封装为Promise对象
所以,事实上,async/await
也可以用类似Promise
那样的then
方法:
async function test(arg) {
console.log('result is', arg)
}
test(2).then(() => {
console.log('end')
})
如果async没有返回值,那么将返回Promise.resolve(undefined)
因为其内部就封装了Promise,所以有着和Promise一样的特点--无等待,在没有await
的情况下执行async
,它会被立即执行,返回一个Promise对象,并且不会阻塞后面的代码。
说完了async,再说说await。await在使用时要封装在一个async函数内。前面提到async返回一个promise,await就在等待一个表达式,注意,这个表达式不一定是async返回的promise,也可以是其他值:
function test1() {
return "test1"
}
async function test2() {
return "test2"
}
async function test3(){
const t1 = await test1();
const t2 = await test2();
console.log(t1,t2)
}
test3();
// test1 test2
从上可以看出,假设await等到的不是一个promise对象,那么它的运算结果就是它接收到的返回值;如果它等到的是一个promise对象,那么它就阻塞后面代码,等着promise的resolve方法,然后获取到resolve处理的返回值后再做处理
一个实际点的demo:
var fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test');
}, 2000);
});
};
var test = async () => {
var t = await fn();
console.log(t);
};
test();
console.log('out');
在上述代码中,先是打印出out
,等待2s后输出test
在test
函数中,t
取得了fn
中的值,并且通过await
阻塞了后面代码(console.log(t)
)的运行,直到fn
这个异步函数的执行完毕,才执行了后面的代码
看看它相比较promise做了什么优化:
function takeTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n+200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeTime(n);
}
// Use Promise
function doit(){
console.log('begin');
var time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.log('end');
});
}
doit();
// Use async/await
async function doit2() {
console.log('begin');
var time1 = 300;
var time2 = await step1(time1);
var time3 = await step2(time2);
var result = await step3(time3);
console.log(`result is ${result}`);
console.log('end');
}
doit2();
可以看见,如果用async/await就可以避免promise那一段很长的then,看起来就像是同步的编程方式
优点
- 1)内置执行器
async不信generator函数那样,它自带执行器,可以像普通函数那样使用 - 2)语义化好,比起generator函数的
yield
和*
,ayanc/await编程更像是同步操作 - 3)适用性更广。
yield
后面只能是thunk
函数或者Promise
对象,而async
后面可以跟Promise
对象和原始类型值(数值、字符串和布尔值)
参考:
浏览器事件循环机制(event loop)
Javascript异步编程的4种方法
谈一谈几种处理 JavaScript 异步操作的办法
JavaScript 异步编程学习笔记
js中的同步和异步的个人理解
Promise迷你书
Promise使用手册
Promise的个人理解及实践
ES6 Promise介绍
阮一峰 Generator 函数的语法
[译] 如何在 JavaScript 中使用 Generator?
AlloyTeam ES6 generator介绍
理解 JavaScript 的 async/await
async 函数的含义和用法