JS异步解决方案

所谓异步,就是在函数执行后不能马上得到结果就是异步函数,常见的比如ajax.
JavaScript针对异步提供了以下六种解决方式

回调函数

回调函数是一个函数作为参数传递给另一个函数的函数,然后在需要调用的时候调用这个函数

1
2
3
4
5
6
7
8
9
10
function add(a, b, callback) {
let sum = a + b;
callback(sum)
}

function print(sum) {
console.log(sum)
}

add(1, 2, print) // 3

优缺点

回调函数的优点是简单、容易理解。缺点不利于代码阅读和维护,高耦合,流程结构混乱,容易造成回调地狱。不能try catch捕获错误,不能直接return

使用场景

  • 异步编程
  • 事件监听
  • 配合setTimeout、setInterval

事件监听

这种思路不取决于代码执行顺序,取决于事件是否触发。比如点击事件等

优缺点

可以绑定多个事件,每个事件可以指定多个回调函数
缺点:程序变成事件驱动型,运行流程不清晰

发布订阅

(与事件监听类似)

Promise/A+ 规范

promise是异步解决方案。ES6将其写进语言标准并提供了promise对象。
我们可以从promise对象内获取异步操作的结果,并使用相应回调函数处理结果

使用 详细语法

1
2
3
4
5
6
7
let f1 = new Promise(function(resolve, reject) {
resolve('resolve')
});
f1.then(res => {
console.log(res)
});
// resolve

通过 .then() 方法处理状态变化的回调函数

特点

  • 状态不受外部影响,也就是说只有内部的结果能决定其状态
  • 状态一经改变就不会在改变。只能从 pending => fulfilled 或 pending => rejected

应用

generator函数

generator函数首先可以理解成一个状态机,封装了多个内部状态。
执行generator函数时会返回一个遍历器对象,可以遍历函数内部的不同状态

使用

1
2
3
4
5
6
7
8
9
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

yield表示暂停标志,next方法可启动,每次返回的是yield后面表达式的结果。
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

async/await

async/await通过generator和promise的实现,是generator函数的语法糖。

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function fn(args) {
await foo()
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
yield foo()
// ...
});
}

function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();

step(function() { return gen.next(undefined); });

function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(val) {
step(function() { return gen.next(val); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}

});
}

实现其实并不难,重要的是理解async函数的行为,返回一个promise函数,要想保持完全同步,await后面要接promise

await都做了什么

首先:

1
2
3
4
5
6
async function async2() { console.log('async2')}
// 等价于
function async2(){
console.log('async2');
return Promise.resolve();
}
1
2
3
4
5
6
7
8
9
10
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
// 等价于
function async1(){
console.log('async1 start')
return RESOLVE(async2()).then(() => { console.log('async1 end') });
}

问题关键就出在这个 RESOLVE 上

  • RESOLVE(p)接近于Promise.resolve(p),不过有微妙而重要的区别:p 如果本身已经是 Promise 实例,Promise.resolve 会直接返回 p 而不是产生一个新 promise;
  • 如果RESOLVE(p)严格按照标准,应该产生一个新的 promise,尽管该 promise 确定会 resolve 为 p,但这个过程本身是异步的,也就是现在进入 job 队列的是新 promise 的 resolve 过程,所以该 promise 的 then 不会被立即调用,而要等到当前 job 队列执行到前述 resolve 过程才会被调用,然后其回调(也就是继续 await 之后的语句)才加入 job 队列,所以时序上就晚了
    await规范不断更新,这一块我还没有十分清楚

所以上述的 async1 函数我们可以进一步转换一下:

1
2
3
4
5
6
7
8
function async1(){
console.log('async1 start')
return new Promise(resolve => {
resolve(async2())
}).then(() => {
console.log('async1 end')
});
}

例子

await后面接普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function foo() {
await test2()
await setTimeout(function() {
console.log('one')
}, 1000);
console.log('three')
}
function test2() {
console.log('two')
}
foo() // two three one

// 其中foo函数等价于
async function foo() {
await test2()
// return new Promise(resolve => {
// resolve(setTimeout(function() {console.log('one')}, 1000))
// }).then(function () {
// console.log('three')
// })
// //上面这种方式和下面的一样
return Promise.resolve(setTimeout(function() {
console.log('one')
}, 1000)).then(function () {
console.log('three')
})
}

await后面接promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async function foo() {
await test()
await test2()
console.log('three')
}
function test() {
console.log('one')
}
function test2() {
return new Promise((resolve) => {
setTimeout(function() {
resolve()
console.log('two')
}, 1000);
});
}
foo() // one two three

// 其中foo函数等价于

async function foo() {
await test()
// await test2()
// console.log('three')
return new Promise(resolve => {
resolve(test2())
// // resolve(test2()) 等价于
/* resolve(new Promise((resolve) => {
setTimeout(function() {
console.log('two')
resolve();
}, 1000);
}))
*/
}).then(() => {
console.log('three')
})
}

// 这是因为async函数返回一个promise对象,
// 通过reslove进行下一步操作,将reslove放在setTimeout里,
// 自然需要等待setTimeout执行完才能输出 'three'

简单面试题-判断执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async function async1({
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2({
console.log('async2');
}
console.log("script start");
setTimeout(function ({
console.log("settimeout");
}, 0);
async1();
new Promise(function (resolve{
console.log("promise1");
resolve();
}).then(function ({
console.log("promise2");
});
console.log('script end');
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// settimeout