继续整理一点对 JS 知识点的理解。或许一篇算一股,集齐八篇就能召唤点什么了(?)

作为一般通过前端,某种程度可以说这种技术本身就是随着性能飞升的红利崛起的,很多时候很少会考虑到靠近底层的东西。所以理解可能还是非常有限。然而,我写这个文章主要还是想到「把知识点讲一遍」是最好的复习方式,于是就用这个办法讲给自己。

一些事件是异步的。比如计时、与服务器交互…,它们可能需要等待一定的时间或者带来延迟。在它们面前,你有这么高速运转的同步事件(指立即执行),它不会默认就按照代码的先后顺序来等异步事件结束。所以问题来了:

  1. 怎么处理异步?
  2. 这些异步事件是怎么延迟的,记住它给出的原理(逃,这下还真是,读代码题已经品鉴的够多了。。。。

怎么处理异步?

看 JavaScript.info,提到第 11 章的时候出现了三种不同的处理异步的方法:callbackpromiseasync / await,但是品鉴这种东西的时候基本上很少见到 callback。

原因其实也很简单,它可以完美的产生回调地狱。

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
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

loadScript('1.js', function(error, script) {

if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...加载完所有脚本后继续 (*)
}
});
}
});
}
});

或者可以拆开嵌套,使用一个个独立的顶层函数来操作。但是会给人一种「支离破碎的思考·发言」的感觉,每一步都要被拆成一个一次性函数,产生了混乱的命名空间(激活死亡 buff,coding 当中最讨厌的事情 —— 起名)。

所以「生产者 - 消费者模式」的 Promise 加入战斗。它的语法相对简单,而且可以链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise((resolve, reject) => {
// 这个时候状态是 pending。
(condition) ? resolve(_result): reject(_reason);
// 状态变成 fulfilled / rejected。
reject(reason); // NO,executor 只能调用一个 resolve 或 reject。
}).then((result, reason) => {
doSomething(result); // 上一步成功, _result
doSomething(reason); // 上一步失败, _reason
}).catch(() => {
console.log(reason); // 只关心失败情况
}).finally(() => {
... // 已经结束嘞,最终的清理
})

resolve 的值会变成下个阶段的 result。当然 then 函数的参数可以为空,就单纯是上一个阶段的 then。

但是,这种来回 resolve 和 reject 的代码还是显得笨重。于是有了语法糖,几乎用起来和同步函数没有太大差别的 asyncawait

1
2
3
4
5
6
async function f1(){
return 1;
}

f1().then(res => console.log(res)); // 1,return 的时候,函数返回一个 promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function f2(){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 1000);
});
}

//2,都这样了,我其实可以直接用 await 代替掉所有的 then。前提是 function 必须 async!

async function afterf2(){
let v = await f2();
console.log(v);
}

/*
f2().then((result) => {
console.log(result);
});

* /
afterf2();
1
2
3
4
5
6
7
8
async function f3() {
await Promise.reject(new Error("Whoops!"));
}

// 3,error 也可以等价
async function f3() {
throw new Error("Whoops!");
}

从回调函数到 async / 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
28
console.log('script start')

const async1 = async () => {
await async2()
console.log('async1 end')
}
const async2 = async () => {
console.log('async2 end')
}

async1()

setTimeout(() => {
console.log('setTimeout')
}, 1000)

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')
1
2
3
4
5
6
7
8
9
10
script start
async2 end
Promise
script end // 同步

async1 end
promise1
promise2 // 异步微任务

setTimeout // 宏任务

毕竟异步事件 then 和 await 会慢一点,它出现在同步输出的后面。但为什么 setTimeout 会在最后?

这就要引入 JS 处理这些事件的一个机制了 —— 事件循环。

在 JS 当中,事件被分为同步异步事件,而异步事件也分为宏任务微任务

· 宏任务:加载 script,setTimeout / setInterval,IO 操作

· 微任务:async await,then …

一般的事件会进入执行栈当中,而异步事件存在两个队列。

这个题目的执行顺序是这样的:

  1. 前四个输出都来源于同步任务。创建 async 函数和 promise 的执行语句并不带来延迟。
  2. await 和 then 是异步微任务。当它等待完毕的时候,会把它执行的代码语句推入微任务队列当中。两个 await 一个 then 后面的 console.log 进入微任务队列。
  3. 1000ms 后(哪怕 0ms 也是这样,微任务总会优先),setTimeout 执行的微任务队列把 console.log 推进宏任务队列。
  4. 微任务后面的内容依次入栈并执行。(并不是微任务全部入栈后执行,栈里每次还是只有一个代码,它们还是会按顺序的)。输出 async1 end、promise1、promise2。
  5. 没有微任务了,轮到宏任务队列。输出 setTimeout。

同时,JS 允许我们自己调用微任务。

1
2
3
queueMicrotask(() => {
console.log('Where am I?');
})

同理,把它放在 async1 和 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
console.log('script start')

const async1 = async () => {
await async2()
console.log('async1 end')
}
const async2 = async () => {
console.log('async2 end')
}

async1()

setTimeout(() => {
console.log('setTimeout')
})

queueMicrotask(() => {
console.log('set Queue')
queueMicrotask(() => {
console.log('set Queue again')
})
})

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')

异步的部分会这样执行。

1
2
3
4
5
async1 end
set Queue
promise1
set Queue again
promise2

宏任务会被拖住。所以如果微任务不断地循环,宏任务根本没有执行的可能,就卡在这里了。