Node-JS专精12_01Eventloop

参考链接

Eventloop 之前的前置内容

操作系统相关

1
2
3
4
5
6
7
8
网页上有个输入框,当你按下键盘的 “J” 之后发生了什么

实际 “J” 下面有一个非常复杂的电路
当你按下 之后,会触发一个电流
这个电流会触发一个你按键的信息 如 101
然后把这个 信息 101 传递给 操作系统
操作系统知道这个 按键信息之后,它就会通知给你的浏览器
浏览器得到 “J” 按键信息之后,就会把这个 “J” 显示在你的输入框上

为什么讲这个:浏览器它会接受到系统给它的一个事件用户不停的触发事件,浏览器就不停的把用户输入渲染到输入框里

这个东西呢就叫做 “事件”

事件

接受各种外部设备发出的信号

  • 蓝牙事件
  • 鼠标事件
  • 键盘事件
  • USB事件

疑问?为什么你一插 USB操作系统马上就知道呢?有没有延迟呢?它是非常的快呢?还是一段时间一段时间的查呢?

  • 非常遗憾的是,操作系统并没有你想想的那么聪明
  • 它是如何知道 用户按下了 “J” 呢?

它实际上是 不停的等键盘,可能每隔5毫秒查看一下有没有按下这个键 这个过程 叫做 “轮询”

接下来我们看一下 浏览器的内容

  • 浏览器除了运行 JS 之外,还有些网络请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 假设你做了这样的事情
    console.log(1)
    console.log(2)
    console.log(3)
    ...
    console.log(10)
    ajax();
    console.log(11)
    console.log(12)
    ...
    console.log(20)


    当我们发送了请求之后,请求大概需要0.2秒得到结果,
    首先JS是单线程,那么在这请求 0.2 秒的时间内, ajax() 后面的代码有么有执行?

    有!!! 执行了

    那么此时 ajax 请求的 0.2秒 是谁在等?

或者说 等待请求结果是谁在处理 / 谁在轮询

  • 答案是:“C++”, 浏览器有一个轮询机制,每隔一段时间假设5毫秒就问 结果来了没?
  • 反正始终有一个东西在轮询 ,我们不管到底是 浏览器还是 操作系统这些细节。 反正不是JS在等就好了

所有的事件 应该如何通知给 JS

这个轮询的规则是什么?

搞清楚这些规则,你就明白什么是 Eventloop

  • 刚刚的内容讲的是浏览器,我们必须要回到 Node.js 和浏览器是差不多的东西
  • Node.js 可以执行js代码,浏览器也可以执行 js代码,Eventloop 是 Node.js的概念而不是 浏览器。 所以我们来看 Node.js

回到 Node.js

1
2
3
4
5
6
同样执行10行代码
然后发起一个异步任务 耗时2秒 setTimeout(fn,2000)
下面又执行20行代码


那么这个 setTimeout 到底是谁在做呢?

引入 Eventloop

  • 首先这个 Eventloop 会去监听这个 setTimeout,它会保证 一段时间后执行这个 fn

Eventloop 之前的铺垫

  • Eventloop 到底是真实存在的对象,还是一个虚拟的概念?

来个例子

1
2
3
4
5
6
7
8
9
假设人是有轮回的? 我们叫它 人生循环
那么人生循环有那些阶段呢?

生老病死 ==》 投胎 ==》 生老病死 ==》 投胎 。。。
什么时候是尽头,除非宇宙毁灭,什么时候开始,也不要太关心。

请问人生循环 是什么?

人在一生中处于不同阶段的过程

事件循环不是循环,它是状态变化的过程

Eventloop 事件循环的阶段

  • times (前端需要知道)
  • I/O callbacks
  • idle , prepare
  • poll (前端需要知道)
  • check (前端需要知道)
  • close callbacks

我们只需要清楚 times / poll / check 就够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1 times
2 poll
3 check


times => poll 会停留一段事件
poll => check
check => times


当我们执行 setTimeout(fn,2000) 时, 会把 fn 放入 times 的一个 数组里 (队列)同时记录 要在 2秒后执行 fn ,于是 js 就去做自己的事去了

setTimeout(fn,2000) 之后就会进入 poll 阶段 等待的过程中 他会去看时间,
刚才 js 说要在 2秒后执行 fn , 比如此时等了 500毫秒,发现不需要执行, 继续等 1000毫秒再去看, 继续等, 等到 2秒的时候 发现时间到了,
赶紧进入 times 阶段执行 fn, 虽然 poll之后要经过 check 阶段 ,但稍后再说
执行完 fn 之后 又回到 poll 阶段 继续等。 一般来说有最长等待时间 假设3秒 后就会进入 check 然后进入 times 然后继续进入 poll
所以 大部分时间都是在 poll 阶段 用来等

check 阶段是什么?

  • Node.js 除了 setTimeout 还有 setImmediate(fn2)
  • 当你调用 setImmediate(fn2) 的时候 你的 fn2 不会进入 times 阶段,会进入 check阶段
  • check 阶段也是有 一个队列的 主要是用来存 setImmediate 执行的函数的

来个考题, 先执行 fn 还是先执行 fn2

1
2
3
4
5
6
7
8
9
10
11
setTimeout(fn,0)
setImmediate(fn2)
// 答案是 fn2

/*
setTimeout(fn,0) 执行后 会进入 times 阶段 把 fn 放入 times 的队列里
然后看到下一句代码
setImmediate(fn2) 会把 fn2 放入 check 阶段的队列里 , 并立即执行队列里的fn2。
于是JS开始做事了, 它看到 check 队列里有个需要立即执行的 于是就不停了 ,直接 fn2执行掉
然后到 times 阶段 刚好发现 times 队列里 有个 0秒后执行,于是 执行 fn
*/

一个坑 开启 Eventloop() 这是需要消耗时间的

1
2
3
4
5
6
7
8
9
10
11
12
// 在 node环境下 运行这两句
setTimeout(()=>{console.log('fn')},0)
setImmediate(()=>{console.log('fn2')})

// 你会惊讶的发现 有的时候 fn fn2 有的时候 fn2 fn

// 为什么?
如果说 开启 Eventloop 很快 那么就是你分析的顺序 fn2 fn
如果 Eventloop 开启很慢 我们在执行js的时候 可能还没进入 times 实际上是先把 fn 加到 times 队列里 然后在开始第一阶段

这就刚好解释了 为什么顺序是不确定的?
就是看 进入 times 阶段的时候 fn 就存在 还是 进入 times 阶段的时候 fn 不存在

如何解决这种情况

1
2
3
4
5
6
setTimeout(()=>{
setTimeout(()=>{console.log('fn')},0)
setImmediate(()=>{console.log('fn2')})
},1000)

// 这样每次都是 fn2 fn

process.nextTick 是放到什么阶段

  • 首先它不属于 Eventloop 的任何一个阶段,官网说的,其他都是瞎扯的
  • process.nextTick 意思就是 当前阶段结束后马上执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setTimeout(()=>{
setTimeout(()=>{console.log('fn')},0)
setImmediate(()=>{console.log('fn2')})
process.nextTick(()=>{console.log('fn3')})
},1000)

// 注意!!! nextTick 不属于 Eventloop 任何阶段 , 它的意思是当前阶段马上执行
// setImmediate 会在 setTimeout之前
// setImmediate 是从 poll 阶段 进入 check 才执行
// nextTick 是当前阶段 poll 所以先 nextTick 在 check 阶段里的 然后 times 里的
/*
fn3
fn2
fn
*/

面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
setImmediate(()={
console.log("setImmediate1")
setTimeout(()=>{
console.log("setTimeout1")

},0)
})

setTimeout(()={
console.log("setTimeout2")
setImmediate(()=>{
console.log("setImmediate2")

},0)
})

答案是:
setImmediate1
setTimeout2
setTimeout1
setImmediate2

// 务必画图分析

总结

Eventloop 状态转移

  • Node.js 有六个阶段 简化为三个
    • times
    • poll
    • check
  • Chrome (一会,马上)(宏任务,微任务)

Node.js

  • setTimeout => times
  • setImmediate => check
  • nextTick => 当前阶段结束后执行

Chrome

  • setTimeout => 宏任务
  • promise.then => 微任务
    • 注意! 面试喜欢问 await
      1
      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
async function async1(){
console.log(1)
await async2()
console.log(2)
}

async function async2(){
console.log(3)
}

async1();

new Promise(function(resolve){
console.log(4)
resolve();
}).then(function(){
console.log(5)
})

/*
1
3
4
2
5
*/

首先改写所有的 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
29
30
31
32
33
async function async1(){
console.log(1)
async2().then(f1)
function f1(){
console.log(2)
}
}

async function async2(){
console.log(3)
}

async1();

new Promise(function(resolve){
console.log(4)
resolve();
}).then(f2)

function f2(){
console.log(5)
}

/*
在看 首先 async1()执行 打印1
async2() 执行 打印3
async2().then() 把 f1 存入 微任务
new Promsie 开始 打印4
把 new Promise().then(f2) 的 f2 存入 微任务
开始执行微任务队列里的内容
f1 执行 打印 2
f2 执行 打印 5
*/

面试

  • 一定要画图
  • 一定要画图
  • 一定要画图

个性化题目的面试如何处理

只有某些公司会问,80%的公司不会问的题

  • PWA
  • echarts / d3.js
  • three.js
  • flutter
  • SSR

如何准备

平时用不到,公司太小众了, 但是面试问?

  • 先看公司招聘启事,如果明确说了,用到某某技术,你就去看文档,搞出一个 hello world
  • 因为大部分跟你一起面试的人根本就不会对这些东西做任何准备,你只要比他们多准备一点点,就可以了
  • 你不在乎你比别人多厉害,而在乎只要比别人厉害一点
  • 如果你没做,被问到,你连接话的地方都没有那岂不是很尴尬。所以你做个Hello World 都能改善局面

Node-JS专精11_04async_await问题

问题整理

async / await 是 promise 的语法糖如何用 promise 实现 async/await

  • 答:有的语法糖好改写,有的语法糖不好改写。这个语法糖就不好改写,因为这是语言层面的改动,而不是API层面的。可以举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// API层面语法糖
如 promise 的 catch 改写成 promise.then(null,fn)



// await 改写
async function fn(){
const res = await ajax();
console.log(res)
}

function fn2(){
ajax().then((res)=>{
console.log(res)
})
}

有些 await 需要等待上一个 await 的结果,有些不用,如何让不用同步的 await 异步执行

  • 答:其实非常简单
1
2
3
4
5
6
7
8
9
10
11
12
13
async function fn(){
await getUser()
await getProducts();
// 你想要 getOthers 不等前两个同步结果,非常简单 把它放到 前面就行了
getOthers();
}

// 处理方式
async function fn2(){
getOthers();
await getUser()
await getProducts();
}

如何实现一个监控,调用异步请求一直等待,监控某个值变了就执行回调

  • 答:不太清楚你的场景,不过 Vue2 已经做到了这一点,用 Object.defineProperty ,也可以用Proxy方案
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
ajax = function(){
return new Promise((resolve,reject)=>{
resolve({
data:{name:'a'}
})
})
}

var data ={
name:'sss'
}

var vm = {}

Object.defineProperty(vm,'name',{
set(newValue){
data.name = newValue
},
get(){
return data.name
}
})

ajax().then(res=>{
console.log(res.data);
vm.name = res.data.name
})

代码题

1
2
3
4
5
6
7
8
9
10
let a = 0;
let test = async()=>{
a = a + await 10;
console.log(a)
}

test();
console.log(++a);

// test 是个异步的 所以不会执行 先去打印
  • 答:倒数第二行有坑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
答案是 
1
10

为什么?是10 你一定认为是 11

// 改写下代码继续运行 打印 test时 a +
let a = 0;
let test = async()=>{
a = (console.log('a:'+ a),a) + await 10;
console.log(a)
}

test();
console.log(++a);

// 原因就是 我们一直认为 await 的右边会立刻执行 await 是等一会执行的
但是如果他用 加号的时候 就不能这样算 因为你不是 `a + await 10` 而是先确定 a的值

题外话

只有在一行代码的情况下

  • 用 await 最好的
  • 如果情况变复杂了,还是要 Promise
  • 并行的情况,也要用 Promise.all

Node-JS专精11_03async_await

async / await

常用用法

1
2
3
4
5
6
7
8
const fn = async () => {
// makePromise 返回 promise的函数
const temp = await makePromise()
return temp + 1;
}

// 这个 makePromise 如果成功了就会返回一个值
// 如果失败了,就会报错,你就必须用 try / catch 才能拿到那个值,但是也不一定后面会说

优点

  • 完全没有缩进,就像是写同步代码

封装一个 async 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function 摇骰子(){
return Math.floor(Math.random()*6)+1
}

// 如果需要 reject , 直接 throw Error('xxx')

// 使用

async function fn(){
const result = await 摇骰子();
console.log(result)
}

// 如果需要处理错误,可以 try catch

fn(); // 直接打印摇骰子结果

但是如果想要 3秒后得到摇骰子的结果 就就没法直接写,必须有一个 Promise

实现3秒后摇骰子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function 摇骰子3秒后(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(Math.floor(Math.random()*6)+1)
},3000)
})
}

async function fn2(){
const result = await 摇骰子3秒后();
console.log(result)
}

fn2()

抛出错误 / 解惑错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function 摇骰子(){
throw new Error('骰子坏了')
}

async function fn(){
try {
var result = await 摇骰子()
console.log(result);
} catch(e){
console.log(e)
}
}

fn();

为什么需要 async

  • await 所在的函数不就是 async 函数吗? 但是为什么还是要加上 async声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function 摇骰子(){
throw new Error('骰子坏了')
}
// 删掉 async
function fn(){
try {
var result = await 摇骰子()
console.log(result);
} catch(e){
console.log(e)
}
}

fn()
  • 原因是在 await出现之前,有些人自己写了 await
1
2
3
4
// 旧代码,自己实现的 await
function fn(){
var res = await(摇骰子())
}

为什么加了 async

  • 由于那些自己实现 await 的人来说 ,如果 await 不配合 async 直接发布它的代码就不能用,不兼容
  • 如何兼容呢? 于是JS设计者想到的是 在外面包一层 来加以区别
  • 最终原因只有一个:兼容旧代码里,普通函数里的 await(xxx) 所以在所有出现 await 的地方外面加了一个 async

await的错误处理

常见方式

  • 一个字:丑
1
2
3
4
5
6
7
8
function fn(){
try {
var result = await axios.get('/xxx')
console.log(result);
} catch(e){
console.log(e)
}
}

正确姿势

  • then 和 await 结合使用,在 then 里处理异步错误, await只接受成功的结果
  • 能处理90%的情况
1
2
3
const result = await axios.get('/xxx').then(null,errorHandler)
console.log(result)
// 错误处理放在 then里面

细节

  • 可以把 4xx/5xx等常见错误用拦截器全局处理
  • await只关心成功,失败全部交给 errorHandler
  • errorHandler 也可以放在拦截器里

示例代码

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
ajax = function(){
return new Promise((resolve,reject)=>{
/*
// 成功处理
resolve({
data:{name:'a'}
})
*/
reject({
response:{status:403}
})
})
}

var error = (e)=>{
console.log(e)
console.log('提示用户没有权限')
throw e
}

async function fn(){
const response = await ajax().then(null,error)
console.log(response)
}


fn()

await 的传染性

1
2
3
4
5
console.log(1)
await console.log(2)
console.log(3)

导致 3 要等一会才被打印,因为有 await, 导致它下面的代码变成异步任务

分析

1
2
3
console.log(3) 变成异步任务了
Promise 同样具有传染性 (同步变异步)
谁没有传染性 : 回调

await 应用场景

多次处理一个结果

1
2
3
const s1 = await makePromise();
const s2 = handlerR1(r1)
const s3 = handlerR2(r2)

串行

  • 天生串行

    1
    2
    3
    4
    5
    6
    7
    async function fn(){
    await ajax1();
    await ajax2();
    await ajax3();
    await ajax4();
    await ajax5();
    }
  • 循环的时候有 bug

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 实际上它是并行的
    async function fn(){
    var array = [ajax1, ajax2, ajax3]
    for(let i=0;i<array.length;i++){
    await array[i]
    }
    }

    /*
    跟你直接这样 不一样
    await ajax1();
    await ajax2();
    await ajax3();
    */

    // 用 await 循环
    搜索关键字 for-await-of

并行

  • await Promise.all() 就是并行了

Node-JS专精11_02Promise使用场景

Promise使用场景

多次处理一个结果

  • 摇骰子.then(v => v1).then(v1 => v2)
    • 1 2 3 返回小 / 4 5 6 返回大
  • 在BOSS直聘上投递阿里简历流程:
    • 必须先有网
    • 有网才能使用 BOSS直聘app或 pc
    • 登录
    • 从众多公司里搜索 阿里的 岗位
    • 投递简历

串行

一道网传面试题

1
2
3
4
5
页面有两个按A和B,以及一个输入框,
A按钮点击后发送一个请求,返回一个字符串A
B按钮点击后也发送一个请求,返回一个字符串B
返回后会把返回的字符串赋值给输入框,但是 A 和 B发送的请求时间点不同,
点击按钮的顺序也不一定,B要比A先返回,而最终效果要求是 输入框字符的顺序是 AB

这道题实际就是日常你做的如下需求

1
2
3
4
5
6
7
8
9
你打开了  百度搜索,先输入 vue,(此时已经发送了 请求a ) ,然后输入 react (此时已经发送了 请求b)

好巧不巧的是 请求a 10秒返回结果 请求b 1秒返回结果
此时 你如果按照正常的展示逻辑

你肯定先展示 react的结果 , 然后在展示 vue的结果
此时用户觉得你神经病 , 我明明搜的 react 你给我展示 vue的结果

此时你就要保证就算 react的结果先回来,我也要等到 vue的结果展示了之后在展示

正确处理的姿势:只要有一个结果返回了 就把之前的请求取消掉 ajax.cancel

生活中的高铁进站例子

1
2
3
a 和 b 两个人,分别去高铁app上买了去 北京的票 如C2212
b 比 a先买到票,但是检票的时候,a站在 b前面。
所以 无论如何,b都要在 a之后进入车厢

代码实例

  • 点击 a 按钮后立刻点击 b按钮
  • 3秒后 输入框显示 “bbb”
  • 5秒后显示 “aaa”

我们想要的结果是,不管那个结果回来,都最后显示”bbb”

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button id="a">a</button>
<button id="b">b</button>
<input id="input" type="text">
<script>
let ajax1 = function(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("aaa")
},5000)
})
}

let ajax2 = function(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("bbbb")
},3000)
})
}

a.onclick = ()=>{
ajax1().then((s)=>{
input.value = s
})
}

b.onclick = ()=>{
ajax2().then((s)=>{
input.value = s
})
}
</script>
</body>
</html>

生活中的点餐取餐例子

1
2
3
4
5
6
7
8
9
10
11
餐厅有两个窗口 点餐 / 取餐
a 和 b 两个人 先后进入餐厅的点餐队列进行点餐 a 在 b 之前点餐了
a 点餐制作耗时 15分钟
b 点餐制作耗时 10分钟

a 和 b 点餐完成后, 又依次站在 取餐队列里 a在 b之前
10分钟后 b的餐好了,
但是 a 在 b之前, b 无法取餐
15分钟 a 的餐好了
a 取餐成功
于是轮到 b , b取餐成功

代码示例

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button id="a">a</button>
<button id="b">b</button>
<input id="input" type="text">
<script>
let ajax1 = function(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("aaa")
},5000)
})
}

let ajax2 = function(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("bbbb")
},3000)
})
}

var 取餐队伍 = [];
var 吧台 = [];
var 问 = ()=>{
var lastN = 吧台[吧台.length -1][0];
var lastS = 吧台[吧台.length -1][1];
if(取餐队伍[0][0] === lastN){
取餐队伍[0][1](lastS);
取餐队伍.shift();
吧台.pop();
问();
}
}

a.onclick = ()=>{
let n = Math.random();
ajax1().then((s)=>{
吧台.push([n,s])
问();
})
取餐队伍.push([n,(s)=>{
input.value = s;
}])
}

b.onclick = ()=>{
let n = Math.random();
ajax2().then((s)=>{
吧台.push([n,s])
问();
})
取餐队伍.push([n,(s)=>{
input.value = s;
}])
}

</script>
</body>
</html>

并行

  • Promise.all([task1,task2]) 不好用(可以自己写一个,参考上一篇文章)
  • Promise.allSettled 又太新

Promise的错误处理

其实挺好用的

  • promise.then(s1,f1) 即可
  • 如果我想全局处理(以 axios 为例 参考它的拦截器)
    • axios 作弊表 拦截器章节
      1
      2
      3
      4
      5
      6
      7
      axios.interceptors.response.use(function (response) {
      // Do something with response data
      return response;
      }, function (error) {
      // Do something with response error
      return Promise.reject(error);
      });

语法糖

  • promise.then(s1).catch(f1)

错误处理之后

  • 如果你没有继续抛错,那么错误不再出现
  • 如果你继续抛出错误, 那么后续回调就继续处理错误
1
2
3
4
// 
Promise.resolve(`{'name':'frank'}`)
.then( s => JSON.parse(s)) // parse 失败 json 的 key必须是双引号
.then( null, (reason) => console.log("err:" + reason)) // 继续 promise 里处理

但是有人对 Promise 不满意

Node-JS专精11_01Promise精讲

Promise

先看代码

1
2
3
4
5
6
7
8
9
10
11
12
function 摇骰子(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
const result = Math.floor(Math.random()*6+1)
resolve(result)
}, 3000);
})
}

// 摇骰子() // 此时返回的是一个 promise

摇骰子().then((res)=>{console.log(res)}) // 3秒后返回摇骰子的结果

这是一个异步任务,那么问题来了 异步任务什么时候执行?

  • 改写了一下
1
2
3
4
5
6
7
8
function 摇骰子(){
return new Promise((resolve, reject)=>{
const result = Math.floor(Math.random()*6+1)
resolve(result)
})
}

摇骰子().then((res)=>{console.log(res)}) // 会立刻返回 摇骰子的结果,但是它是什么时候开始呢?

面试中的 微任务/宏任务

看这样一段代码

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
43
44
45
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button id="btn1">btn1</button>
<button id="btn2">btn2</button>
<script>
// Promise
function 摇骰子(){
return new Promise((resolve, reject)=>{
const result = Math.floor(Math.random()*6+1)
resolve(result)
})
}

btn1.onclick=()=>{
摇骰子().then(n=>console.log(`摇到了${n}`))
console.log("end");
// 执行结果:很明显 我们知道 摇骰子 是异步任务 所以会晚于 同步代码 console.log
/*
"end"
"摇到了1"
*/
}

btn2.onclick=()=>{
setTimeout(()=>{console.log("timeout1"),0})
摇骰子().then(n=>console.log(`摇到了${n}`))
setTimeout(()=>{console.log("timeout2"),0})
// 执行结果
// 如果我们还有其他的异步任务 比如 setTimeout 那么谁会优先呢?
// 答案是 先打出骰子的点数
/*
"摇到了1"
"timeout1"
"timeout2"
*/
}
</script>
</body>
</html>

微任务/宏任务这些前端名词的前因后果

  • 本来是没有 Promise的 在 ES6之前,ES6之后才有了 Promise这种异步
  • 以前的JS非常单纯只有两个东西
    • 当前任务 执行的代码(同步代码)
    • 延时任务 setTimeout / setInterval

ES6以前

  • 当前代码 如console.log(1)
  • 异步队列 setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(1)
console.log(2)
setTimeout(()=>{console.log("timeout 1")},0)
setTimeout(()=>{console.log("timeout 2")},0)
console.log(3)
console.log(4)

/*
1
2
3
4
timeout 1
timeout 2
*/

两次 setTimeout 不会立刻执行,而是放入异步队列里
当前代码执行完了 就去看异步队列了

所以以前的js非常好理解,就是我有个当前要执行的任务,和有个等待执行的任务

ES6之后

前端觉得回调太蛋疼了,于是加了个 Promise

  • 宏任务 – 异步队列1(原来的异步队列 setTimeout)
  • 微任务 – 异步队列2 (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
// 伪代码
ajax.then(s1)
setTimeout(()=>{console.log("timeout")},0)
console.log(1);

/*
依然 console.log 放入同步任务
setTimeout 放入异步任务

此时 ,如果不引入其他东西 ajax.then(s1) 就显得特别智障
假设只有一个异步任务 那么实际就是 把 s1 加入异步队列1 , 然后把第二行的 setTimeout 加入异步队列1 s1之后

于是你惊讶的发现,那我要 then 干什么,有病吧? 直接 setTimeout(s1,0) 不就完了

所以他们就想了一个 让我们的 then 更有用的办法 ,在加一个 异步队列2

于是乎上面的代码
s1 放入 异步队列2
setTimeout 放入 异步队列1

当 同步代码执行完
先去读取 异步队列2 里的任务 去执行
等 异步队列2 里的任务执行完了, 再去读取 异步队列1 里的任务 去执行
*/

于是分别给 异步队列1 / 异步队列2 取了个名字叫做 宏任务 / 微任务

如果队列里有任务,先把 微任务 做完,再去做 宏任务

Promise 其他API

继续以摇骰子为例

1
2
3
4
5
6
7
8
function 摇骰子(){
return new Promise((resolve, reject)=>{
const result = Math.floor(Math.random()*6+1)
resolve(result)
})
}
// 随机的
摇骰子().then((res)=>{console.log(res)})
  • Promise.resolve(result) 制造一个成功(或失败)

    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
    // 制造成功
    // 你想造假一直 摇6

    function 摇6的骰子(){
    return new Promise((resolve, reject)=>{
    resolve(6)
    })
    }

    function 摇6的骰子2(){
    return Promise.resolve(6)
    }

    // 摇6的骰子
    // 摇6的骰子().then((res)=>{console.log(res)})
    // 摇6的骰子2().then((res)=>{console.log(res)})


    // 制造失败
    function 摇骰子返回失败(){
    return Promise.resolve(
    new Promise((resolve,reject)=>reject())
    )
    }

    摇骰子返回失败().then((res)=>{console.log(res)})
  • Promise.reject(reason) 制造一个失败

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 摇坏的骰子(){
    return new Promise((resolve, reject)=>{
    reject('坏了')
    })
    }

    function 摇坏的骰子2(){
    return Promise.reject('坏了')

    }
    摇坏的骰子().then(null,(reason)=>console.log(reason))
    摇坏的骰子2().then(null,(reason)=>console.log(reason))
  • Promise.all(数组) 等待全部成功,或有一个失败

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 所有人都成功了,才叫成功 返回所有成功的结果
    Promise.all([Promise.resolve(1),Promise.resolve(2),Promise.resolve(3)]).then((res)=>{console.log(res)})
    // 打印 [1,2,3]

    // 某个失败 返回失败
    Promise.all([Promise.resolve(1),Promise.resolve(2),Promise.reject('错了3')]).then((res)=>{console.log(res)},(err)=>{console.log(err)})
    // 打印 "错了3"

    Promise.all([Promise.resolve(1"),Promise.reject('错了2'),Promise.reject('错了3')]).then((res)=>{console.log(res)},(err)=>{console.log(err)})
    // 打印 "错了2"

    Promise.all([Promise.reject("错了1"),Promise.reject('错了2'),Promise.reject('错了3')]).then((res)=>{console.log(res)},(err)=>{console.log(err)})
    // 打印 "错了1"
    • 所以 Promise.all 非常没用,因为有一个失败就返回了不等所有的结果
    • 请使用 Promise.allSettled
  • Promise.race(数组) 等待第一个状态改变

    1
    同时发很多请求,只要又一个成功了,就成功,或者一个失败了就失败
  • Promise.allSettled(数组) 等待全部状态改变,目前处于 Stage-4(板上钉钉的实现)

    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
    Promise.allSettled([Promise.resolve(1),Promise.resolve(2),Promise.resolve(3)]).then((res)=>{console.log(res)},(err)=>{console.log(err)})
    // resolve 打印
    /*
    [
    {status: "fulfilled", value: 1},
    {status: "fulfilled", value: 2},
    {status: "fulfilled", value: 3}
    ]
    */

    Promise.allSettled([Promise.resolve(1),Promise.reject('错了2'),Promise.resolve(3)]).then((res)=>{console.log(res)},(err)=>{console.log(err)})
    // reject 打印
    /*
    [
    {status: "fulfilled", value: 1},
    {status: "rejected", reason: "错了2"},
    {status: "fulfilled", value: 3}
    ]
    */

    Promise.allSettled([Promise.reject("错了1"),Promise.reject('错了2'),Promise.reject('错了3')]).then((res)=>{console.log(res)},(err)=>{console.log(err)})

    // reject 打印
    /*
    [
    {status: "rejected", reason: "错了1"},
    {status: "rejected", reason: "错了2"},
    {status: "rejected", reason: "错了3"}
    ]
    */

由于Promise.allSettled很多浏览器不支持,我们依旧无法使用

所以用 Promise.all 实现

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 假设还是三个 promise 任务  第一个和第二个会失败
task1 = () => new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("第一扇门关了")
},3000)
})

task2 = () => new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("第二扇门关了")
},4000)
})

task3 = () => new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("第三扇门开了")
},5000)
})

// 如果这样
// 发现第一个失败就 结束了
Promise.all([task1(),task2(),task3()])
.then(null,(reason)=>console.log(reason))


// 正确姿势 把每个任务都 then一下 因为 Promise.then 会返回新的 promise
Promise.all([
task1().then(()=>({"status":"ok"}),()=>({status:"not ok"})),
task2().then(()=>({"status":"ok"}),()=>({status:"not ok"})),
task3().then(()=>({"status":"ok"}),()=>({status:"not ok"})),
])
.then((result)=>console.log(result))
/*
[
{status: "not ok"},
{status: "not ok"},
{status: "ok"}
]
*/

// 优化代码 因为写的太重复了
setSuccessResult = (promise) => promise.then(
(value)=>({status:"ok",value}),
(reason)=>({status:"not ok",reason}),
)
Promise.all([
setSuccessResult(task1()),
setSuccessResult(task2()),
setSuccessResult(task3()),
])
.then((result)=>console.log(result))
// 打印结果
/*
[
{status: "not ok", reason: "第一扇门关了"}
{status: "not ok", reason: "第二扇门关了"}
{status: "ok", value: "第三扇门开了"}
]
*/

// 还能再次简化
setSuccessResultList = (promiseList) => promiseList.map(
promise => promise.then(
(value)=>({status:"ok",value}),
(reason)=>({status:"not ok",reason})
)
)

Promise.all(setSuccessResultList([task1(),task2(),task3()]))
.then((result)=>console.log(result))

// 再次简化 你不是不支持 Promise.allSettled 吗?
Promise.allSettled2 = function(promiseList){
return Promise.all(setSuccessResultList(promiseList))
}

// 这其实就是 allSettled2 的实现
Promise.allSettled2([task1(),task2(),task3()])
.then((result)=>console.log(result))

Node-JS专精10_02在浏览器支持nextTick

代码仓库

我们使用了 process.nextTick 代替 setTimeout

  • 浏览器不支持 nextTick
  • 如果你用过vue, 你知道vue提供了一个 nextTick,那么 vue是如何做到即支持 node又支持 浏览器呢?

MutationObserver

它本来是监听 DOM树 的更新的,它有一个特别厉害的地方就是 当DOM树更新就会调用一个异步的回调,而这个回调的优先级比 setTimeout 要高

示例代码

  • 你可以拷贝代码在 jsbin里运行
  • 通过实例可以看到无论 setTimeout 在 span改变之前还是之后都没 MutationObserver 的优先级高
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<span id="aaa">1</span>
<br>
<button id="btn">+1</button>
<script>

var span = document.getElementById('aaa');

var config = { attributes: true, childList: true, subtree: true };

var callback = function() {
console.log("observer")
};

var observer = new MutationObserver(callback);
observer.observe(span, config);


btn.onclick = function(){
setTimeout(()=>{console.log("timeout1")},0)
span.innerText += "s";
setTimeout(()=>{console.log("timeout2")},0)
}
/*
每次点击button 始终打印
observer
timeout1
timeout2
*/
</script>
</body>
</html>

我们自己实现一个 nextTick

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button id="btn">+1</button>
<script>

btn.onclick = function(){
setTimeout(()=>{console.log("timeout1")},0)
nextTick(()=>{console.log("nextTick")})
setTimeout(()=>{console.log("timeout2")},0)
}
/*
每次点击button 始终打印
nextTick
timeout1
timeout2
*/

function nextTick(fn){
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {characterData: true})

// 更改文本内容触发 dom树更新 触发回调
counter = counter + 1
textNode.data = String(counter)
}
</script>
</body>
</html>

兼容node和浏览器的nextTick

1
2
3
4
5
6
7
8
9
10
11
12
13
function nextTick(fn){
if( process !== undefined && typeof process.nextTick === 'function'){
return process.nextTick(fn);
}else{
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {characterData: true})
// 更改文本内容触发 dom树更新 触发回调
counter = counter + 1
textNode.data = String(counter)
}
}

代码仓库

重构代码

  • 注意每次优化都跑一下测试用例保证代码正确性
  • 注意每次优化都跑一下测试用例保证代码正确性
  • 注意每次优化都跑一下测试用例保证代码正确性

src/promise.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class Promise2{
state = "pending";
callbacks = [];
private resolveOrReject(state, data, i){
if(this.state !== "pending") return;
this.state = state;
nextTick(() => {
this.callbacks.forEach(handle=>{
if(typeof handle[i] === "function"){
let x;
try {
x = handle[i].call(undefined,data);
} catch (e) {
return handle[2].reject(e);
}
handle[2].resolveWith(x);
}
})
});
}
resolve(result){
this.resolveOrReject("fulfilled",result,0);
}
reject(reason){
this.resolveOrReject("rejected",reason,1);
}
constructor(fn){
if(typeof fn !== 'function'){
throw new Error("我只接受函数")
}

fn(this.resolve.bind(this), this.reject.bind(this));
}
then(succeed?,fail?){
// handle除了记录成功和失败 还要记录成功和失败的后续
const handle = [];
if(typeof succeed === 'function'){
handle[0] = succeed;
}
if(typeof fail === 'function'){
handle[1] = fail;
}

// 记录then之后的后续
handle[2] = new Promise2(()=>{});

this.callbacks.push(handle);
// 把函数推进 callBacks 里面

return handle[2];
}
resolveWithSelf(){
this.reject(new TypeError())
}
resolveWithPromise(x){
x.then(
(result)=>{
this.resolve(result)
},
(reason)=>{
this.reject(reason)
}
)
}
private getThen(x){
let then;
try{
then = x.then;
}catch(e){
return this.reject(e);
}
return then;
}
resolveWithThenable(x){
try {
x.then(
y => {
this.resolveWith(y)
},
r =>{
this.reject(r);
}
);
} catch (error) {
this.reject(error)
}
}
resolveWithObject(x){
let then = this.getThen(x);
if(then instanceof Function){
this.resolveWithThenable(x);
} else {
this.resolve(x);
}
}
resolveWith(x){
if( this === x){
this.resolveWithSelf();
} else if ( x instanceof Promise2){
this.resolveWithPromise(x);
} else if ( x instanceof Object){
this.resolveWithObject(x);
} else {
this.resolve(x);
}
}
}

export default Promise2

function nextTick(fn){
if( process !== undefined && typeof process.nextTick === 'function'){
return process.nextTick(fn);
}else{
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {characterData: true})
// 更改文本内容触发 dom树更新 触发回调
counter = counter + 1
textNode.data = String(counter)
}
}

区分宏任务和微任务

MacroTask

  • setTimeout

MicroTask(优先级高)

  • process.nextTick(nodeJS里用) / MutationObserver(浏览器里用)
  • setImmediate(只能在IE浏览器里用,nodeJS虽然也有这个API但是兼容性不好)

整体完成后的代码

Node-JS专精10_01Promise2_7之后的规范

参考规范文档

代码实现

src/promise.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Promise2{

succeed = null;
fail = null;
state = "pending";
callbacks = [];

resolve(result){
if(this.state !== "pending") return;
this.state = "fulfilled";
setTimeout(() => {
// 遍历 callbacks 调用所有的 handle[0]
this.callbacks.forEach(handle=>{
if(typeof handle[0] === "function"){
// x 是之前成功的返回值
const x = handle[0].call(undefined,result);
handle[2].resolveWith(x);
}
})
});
}
reject(reason){
if(this.state !== "pending") return;
this.state = "rejected";
setTimeout(() => {
this.callbacks.forEach(handle=>{
if(typeof handle[1] === "function"){
// x 是之前失败的返回值
const x = handle[1].call(undefined,reason);
handle[2].resolveWith(x);
}
})
});
}
constructor(fn){
if(typeof fn !== 'function'){
throw new Error("我只接受函数")
}

fn(this.resolve.bind(this), this.reject.bind(this));
}
then(succeed?,fail?){
// handle除了记录成功和失败 还要记录成功和失败的后续
const handle = [];
if(typeof succeed === 'function'){
handle[0] = succeed;
}
if(typeof fail === 'function'){
handle[1] = fail;
}

// 记录then之后的后续
handle[2] = new Promise2(()=>{});

this.callbacks.push(handle);
// 把函数推进 callBacks 里面

return handle[2];
}

resolveWith(x){
if( this === x){
this.reject(new TypeError())
} else if ( x instanceof Promise2){
x.then(
(result)=>{
this.resolve(result)
},
(reason)=>{
this.reject(reason)
}
)
} else if ( x instanceof Object){
let then;
try{
then = x.then;
}catch(e){
this.reject(e);
}
if(then instanceof Function){
try {
x.then(
y => {
this.resolveWith(y)
},
r =>{
this.reject(r);
}
);
} catch (error) {
this.reject(error)
}
} else {
this.resolve(x);
}
} else {
this.resolve(x);
}
}
}

export default Promise2

test/index.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import * as chai from "chai";
import * as sinon from "sinon";
import * as sinonChai from "sinon-chai";
chai.use(sinonChai);
const assert = chai.assert;
import Promise from "../src/promise";

describe("Promise",()=>{
it("是一个类",()=>{
assert.isFunction(Promise);
assert.isObject(Promise.prototype);
})
it("new Promise() 如果接受的不是一个函数就报错",()=>{
assert.throw(()=>{
// @ts-ignore
new Promise();
})
assert.throw(()=>{
// @ts-ignore
new Promise(1);
})
assert.throw(()=>{
// @ts-ignore
new Promise(false);
})
})

it("new Promise(fn) 会生成一个对象,对象有 then 方法",()=>{
const promise = new Promise(()=>{})
assert.isFunction(promise.then);
})

it("new Promise(fn) 中的 fn立即执行",()=>{
let fn = sinon.fake();
new Promise(fn);
assert(fn.called);
})
it("new Promise(fn) 中的 fn 执行的时候接受 resolve 和 reject 两个函数",(done)=>{
new Promise((resolve,reject)=>{
assert.isFunction(resolve);
assert.isFunction(reject);
done();
})
})

it("promise.then(success) 重的 success 会在 resolve 被调用的时候执行",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve,reject)=>{
// 该函数没有执行
assert.isFalse(success.called);
resolve();
setTimeout(() => {
// 该函数执行了
assert.isTrue(success.called);
done();
});
})
// @ts-ignore
promise.then(success);
})

it("promise.then(null,fail) 重的 fail 会在 reject 被调用的时候执行",(done)=>{
const fail = sinon.fake();
const promise = new Promise((resolve,reject)=>{
// 该函数没有执行
assert.isFalse(fail.called);
reject();
setTimeout(() => {
// 该函数执行了
assert.isTrue(fail.called);
done();
});
})
// @ts-ignore
promise.then(null,fail);
})

it("2.2.1",()=>{
const promise = new Promise((resolve,reject)=>{
resolve();
})
promise.then(false,null);
assert(1 === 1);
})

it("2.2.2",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve,reject)=>{
assert.isFalse(success.called);
resolve(233);
resolve(2333);
setTimeout(() => {
assert(promise.state === "fulfilled")
assert.isTrue(success.called);
assert.isTrue(success.calledOnce);
assert(success.calledWith(233));
done();
},0);
})
promise.then(success);
})

it("2.2.3",(done)=>{
const fail = sinon.fake();
const promise = new Promise((resolve,reject)=>{
assert.isFalse(fail.called);
reject(233);
reject(2333);
setTimeout(() => {
assert(promise.state === "rejected")
assert.isTrue(fail.called);
assert.isTrue(fail.calledOnce);
assert(fail.calledWith(233));
done();
},0);
})
promise.then(null,fail);
})

it("2.2.4 在我的代码执行完之前,不得调用 then 后面的两个函数 success",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve)=>{
resolve();
})
promise.then(success);
assert.isFalse(success.called);
setTimeout(() => {
assert.isTrue(success.called);
done();
}, 0);
})

it("2.2.4 在我的代码执行完之前,不得调用 then 后面的两个函数 fail",(done)=>{
const fail = sinon.fake();
const promise = new Promise((resolve,reject)=>{
reject();
})
promise.then(null,fail);
assert.isFalse(fail.called);
setTimeout(() => {
assert.isTrue(fail.called);
done();
}, 0);
})

it("2.2.5 不带入额外的this",(done)=>{
const fn = sinon.fake();
const promise = new Promise((resolve)=>{
resolve();
})
promise.then(function(){
"use strict";
assert(this === undefined);
done();
});
})

it("2.2.6 then可以在同一个promise里被多次调用(链式调用)",(done)=>{
const promise = new Promise((resolve)=>{
resolve();
})
const callbacks = [sinon.fake(),sinon.fake(),sinon.fake()]
promise.then(callbacks[0]);
promise.then(callbacks[1]);
promise.then(callbacks[2]);
setTimeout(() => {
assert(callbacks[0].called);
assert(callbacks[1].called);
assert(callbacks[2].called);
assert(callbacks[1].calledAfter(callbacks[0]));
assert(callbacks[2].calledAfter(callbacks[1]));

done();
});
})

it("2.2.6.2 then可以在同一个promise里被多次调用(链式调用) reject",(done)=>{
const promise = new Promise((resolve,reject)=>{
reject();
})
const callbacks = [sinon.fake(),sinon.fake(),sinon.fake()]
promise.then(null,callbacks[0]);
promise.then(null,callbacks[1]);
promise.then(null,callbacks[2]);
setTimeout(() => {
assert(callbacks[0].called);
assert(callbacks[1].called);
assert(callbacks[2].called);
assert(callbacks[1].calledAfter(callbacks[0]));
assert(callbacks[2].calledAfter(callbacks[1]));

done();
});
})

it("2.2.7 then必须返回一个 promise ",()=>{
const promise = new Promise((resolve)=>{
resolve();
})
const promise2 = promise.then(()=>{},()=>{});
assert(promise2 instanceof Promise);
});

it("2.2.7.1 如果onFulfilled或onRejected返回一个值x, 运行Promise Resolution Procedure [[Resolve]](promise2, x)",(done)=>{
const promise1 = new Promise((resolve)=>{
resolve();
})
promise1.then(()=>"成功",()=>{}).then(result=>{
assert.equal(result,"成功");
done();
})
})

it("2.2.7.2 x 是一个 Promise实例",(done)=>{
const promise1 = new Promise((resolve)=>{
resolve();
})
const fn = sinon.fake();
const promise2 = promise1.then(()=> new Promise(resolve=>resolve()),()=>{})
promise2.then(fn);

/*
注意 setTimeout 里的 延时时间
如果是0 测试不通过
如果是10 就通过
因为这里涉及 js 的 宏任务 微任务
宏任务一般是:包括整体代码script,setTimeout,setInterval。
微任务:Promise,process.nextTick(node实现的) / setImmediate(这个是IE实现的)
*/
setTimeout(() => {
assert(fn.called);
done();
},10);
})

})

宏任务/微任务

上述实现的一个测试代码

  • 由于我们的 Promise 使用的 setTimeout 实现的
  • 所以测试的时候那个 setTimeout 里的 延时时间设置为0会不通过
  • 因为微任务比宏任务的优先级更高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
it("2.2.7.2  x 是一个 Promise实例",(done)=>{
const promise1 = new Promise((resolve)=>{
resolve();
})
const fn = sinon.fake();
const promise2 = promise1.then(()=> new Promise(resolve=>resolve()),()=>{})
promise2.then(fn);

/*
注意 setTimeout 里的 延时时间
如果是0 测试不通过
如果是10 就通过
因为这里涉及 js 的 宏任务 微任务
宏任务一般是:包括整体代码script,setTimeout,setInterval。
微任务:Promise,process.nextTick(node实现的) / setImmediate(这个是IE实现的)
*/
setTimeout(() => {
assert(fn.called);
done();
},10);
})

改用node环境来写代码

  • 使用process.nextTick 代替 setTimeout
  • yarn add --dev @types/node
  • 添加一个配置,告诉ts你写的是node应用,参考ts官网
  • tsconfig.json
    1
    2
    3
    4
    5
    {
    "compilerOptions": {
    "types" : ["node", "mocha"]
    }
    }

src/promise_node.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
class Promise2{
state = "pending";
callbacks = [];

resolve(result){
if(this.state !== "pending") return;
this.state = "fulfilled";
process.nextTick(() => {
// 遍历 callbacks 调用所有的 handle[0]
this.callbacks.forEach(handle=>{
if(typeof handle[0] === "function"){
let x;
try {
// x 是之前成功的返回值
x = handle[0].call(undefined,result);
} catch (e) {
return handle[2].reject(e);
}
handle[2].resolveWith(x);
}
})
});
}
reject(reason){
if(this.state !== "pending") return;
this.state = "rejected";
process.nextTick(() => {
this.callbacks.forEach(handle=>{
if(typeof handle[1] === "function"){
let x;
try {
// x 是之前失败的返回值
x = handle[1].call(undefined,reason);
} catch (e) {
return handle[2].reject(e);
}
handle[2].resolveWith(x);
}
})
});
}
constructor(fn){
if(typeof fn !== 'function'){
throw new Error("我只接受函数")
}

fn(this.resolve.bind(this), this.reject.bind(this));
}
then(succeed?,fail?){
// handle除了记录成功和失败 还要记录成功和失败的后续
const handle = [];
if(typeof succeed === 'function'){
handle[0] = succeed;
}
if(typeof fail === 'function'){
handle[1] = fail;
}

// 记录then之后的后续
handle[2] = new Promise2(()=>{});

this.callbacks.push(handle);
// 把函数推进 callBacks 里面

return handle[2];
}

resolveWith(x){
if( this === x){
this.reject(new TypeError())
} else if ( x instanceof Promise2){
x.then(
(result)=>{
this.resolve(result)
},
(reason)=>{
this.reject(reason)
}
)
} else if ( x instanceof Object){
let then;
try{
then = x.then;
}catch(e){
this.reject(e);
}
if(then instanceof Function){
try {
x.then(
y => {
this.resolveWith(y)
},
r =>{
this.reject(r);
}
);
} catch (error) {
this.reject(error)
}
} else {
this.resolve(x);
}
} else {
this.resolve(x);
}
}
}

export default Promise2

test/index_node.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import * as chai from "chai";
import * as sinon from "sinon";
import * as sinonChai from "sinon-chai";
chai.use(sinonChai);
const assert = chai.assert;
import Promise from "../src/promise_node";

describe("Promise",()=>{
it("2.2.7.1 如果onFulfilled或onRejected返回一个值x, 运行Promise Resolution Procedure [[Resolve]](promise2, x)",(done)=>{
const promise1 = new Promise((resolve)=>{
resolve();
})
promise1.then(()=>"成功",()=>{}).then(result=>{
assert.equal(result,"成功");
done();
})
})

it("2.2.7.1.2 success 的返回值是一个 Promise实例",(done)=>{
const promise1 = new Promise((resolve)=>{
resolve();
})
const fn = sinon.fake();
const promise2 = promise1.then(()=> new Promise(resolve=>resolve()),()=>{})
promise2.then(fn);

/*
注意 setTimeout 里的 延时时间
如果是0 测试不通过
如果是10 就通过
因为这里涉及 js 的 宏任务 微任务
宏任务一般是:包括整体代码script,setTimeout,setInterval。
微任务:Promise,process.nextTick(node实现的) / setImmediate(这个是IE实现的)
*/
setTimeout(() => {
assert(fn.called);
done();
},0);
})

it("2.2.7.1.2 success 的返回值是一个 Promise实例 且失败了",(done)=>{
const promise1 = new Promise((resolve)=>{
resolve();
})
const fn = sinon.fake();
const promise2 = promise1.then(()=> new Promise((resolve,reject)=>reject()),()=>{})
promise2.then(null,fn);

setTimeout(() => {
assert(fn.called);
done();
},0);
})

it("2.2.7.1.2 fail 的返回值是一个 Promise实例",(done)=>{
const promise1 = new Promise((resolve,reject)=>{
reject();
});
const fn = sinon.fake();
const promise2 = promise1
.then(
null,
()=> new Promise(resolve=>resolve())
)
promise2.then(fn,null);

setTimeout(() => {
assert(fn.called);
done();
},0);
})

it("2.2.7.1.2 fail 的返回值是一个 Promise实例 且失败了",(done)=>{
const promise1 = new Promise((resolve,reject)=>{
reject();
});
const fn = sinon.fake();
const promise2 = promise1
.then(
null,
()=> new Promise((resolve,reject)=>reject())
)
promise2.then(null, fn);

setTimeout(() => {
assert(fn.called);
done();
},0);
})

it("2.2.7.1.2 如果 success 抛出一个异常e , promise2 必须被拒绝",(done)=>{
const promise1 = new Promise((resolve,reject)=>{
resolve();
});
const fn = sinon.fake();
const error = new Error();
const promise2 = promise1
.then(
()=> new Promise(()=>{
throw error;
})
)
promise2.then(null, fn);

setTimeout(() => {
assert(fn.called);
assert(fn.calledWith(error));
done();
},0);
})

it("2.2.7.1.2 如果 fail 抛出一个异常e , promise2 必须被拒绝",(done)=>{
const promise1 = new Promise((resolve,reject)=>{
reject();
});
const fn = sinon.fake();
const error = new Error();
const promise2 = promise1
.then(
null,
()=> new Promise(()=>{
throw error;
})
)
promise2.then(null, fn);

setTimeout(() => {
assert(fn.called);
assert(fn.calledWith(error));
done();
},0);
})
})

代码仓库

Node-JS专精09_03参考Promise规范完成Promise

参考规范

src/promise.ts

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
43
44
45
46
47
48
49
50
51
class Promise2{

succeed = null;
fail = null;
state = "pending";
callbacks = [];

resolve(result){
if(this.state !== "pending") return;
this.state = "fulfilled";
setTimeout(() => {
// 遍历 callbacks 调用所有的 handle[0]
this.callbacks.forEach(handle=>{
if(typeof handle[0] === "function"){
handle[0].call(undefined,result);
}
})
});
}
reject(reason){
if(this.state !== "pending") return;
this.state = "rejected";
setTimeout(() => {
this.callbacks.forEach(handle=>{
if(typeof handle[1] === "function"){
handle[1].call(undefined,reason);
}
})
});
}
constructor(fn){
if(typeof fn !== 'function'){
throw new Error("我只接受函数")
}

fn(this.resolve.bind(this), this.reject.bind(this));
}
then(succeed?,fail?){
const handle = [];
if(typeof succeed === 'function'){
handle[0] = succeed;
}
if(typeof fail === 'function'){
handle[1] = fail;
}
this.callbacks.push(handle);
// 把函数推进 callBacks 里面
}
}

export default Promise2

测试代码

test/index.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import * as chai from "chai";
import * as sinon from "sinon";
import * as sinonChai from "sinon-chai";
chai.use(sinonChai);
const assert = chai.assert;
import Promise from "../src/promise";

describe("Promise",()=>{
it("是一个类",()=>{
assert.isFunction(Promise);
assert.isObject(Promise.prototype);
})
it("new Promise() 如果接受的不是一个函数就报错",()=>{
assert.throw(()=>{
// @ts-ignore
new Promise();
})
assert.throw(()=>{
// @ts-ignore
new Promise(1);
})
assert.throw(()=>{
// @ts-ignore
new Promise(false);
})
})

it("new Promise(fn) 会生成一个对象,对象有 then 方法",()=>{
const promise = new Promise(()=>{})
assert.isFunction(promise.then);
})

it("new Promise(fn) 中的 fn立即执行",()=>{
let fn = sinon.fake();
new Promise(fn);
assert(fn.called);
})
it("new Promise(fn) 中的 fn 执行的时候接受 resolve 和 reject 两个函数",(done)=>{
new Promise((resolve,reject)=>{
assert.isFunction(resolve);
assert.isFunction(reject);
done();
})
})

it("promise.then(success) 重的 success 会在 resolve 被调用的时候执行",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve,reject)=>{
// 该函数没有执行
assert.isFalse(success.called);
resolve();
setTimeout(() => {
// 该函数执行了
assert.isTrue(success.called);
done();
});
})
// @ts-ignore
promise.then(success);
})

it("promise.then(null,fail) 重的 fail 会在 reject 被调用的时候执行",(done)=>{
const fail = sinon.fake();
const promise = new Promise((resolve,reject)=>{
// 该函数没有执行
assert.isFalse(fail.called);
reject();
setTimeout(() => {
// 该函数执行了
assert.isTrue(fail.called);
done();
});
})
// @ts-ignore
promise.then(null,fail);
})

it("2.2.1",()=>{
const promise = new Promise((resolve,reject)=>{
resolve();
})
promise.then(false,null);
assert(1 === 1);
})

it("2.2.2",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve,reject)=>{
assert.isFalse(success.called);
resolve(233);
resolve(2333);
setTimeout(() => {
assert(promise.state === "fulfilled")
assert.isTrue(success.called);
assert.isTrue(success.calledOnce);
assert(success.calledWith(233));
done();
},0);
})
promise.then(success);
})

it("2.2.3",(done)=>{
const fail = sinon.fake();
const promise = new Promise((resolve,reject)=>{
assert.isFalse(fail.called);
reject(233);
reject(2333);
setTimeout(() => {
assert(promise.state === "rejected")
assert.isTrue(fail.called);
assert.isTrue(fail.calledOnce);
assert(fail.calledWith(233));
done();
},0);
})
promise.then(null,fail);
})

it("2.2.4 在我的代码执行完之前,不得调用 then 后面的两个函数 success",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve)=>{
resolve();
})
promise.then(success);
assert.isFalse(success.called);
setTimeout(() => {
assert.isTrue(success.called);
done();
}, 0);
})

it("2.2.4 在我的代码执行完之前,不得调用 then 后面的两个函数 fail",(done)=>{
const fail = sinon.fake();
const promise = new Promise((resolve,reject)=>{
reject();
})
promise.then(null,fail);
assert.isFalse(fail.called);
setTimeout(() => {
assert.isTrue(fail.called);
done();
}, 0);
})

it("2.2.5 不带入额外的this",(done)=>{
const fn = sinon.fake();
const promise = new Promise((resolve)=>{
resolve();
})
promise.then(function(){
"use strict";
assert(this === undefined);
done();
});
})

it("2.2.6 then可以在同一个promise里被多次调用(链式调用)",(done)=>{
const promise = new Promise((resolve)=>{
resolve();
})
const callbacks = [sinon.fake(),sinon.fake(),sinon.fake()]
promise.then(callbacks[0]);
promise.then(callbacks[1]);
promise.then(callbacks[2]);
setTimeout(() => {
assert(callbacks[0].called);
assert(callbacks[1].called);
assert(callbacks[2].called);
assert(callbacks[1].calledAfter(callbacks[0]));
assert(callbacks[2].calledAfter(callbacks[1]));

done();
});
})

it("2.2.6.2 then可以在同一个promise里被多次调用(链式调用) reject",(done)=>{
const promise = new Promise((resolve,reject)=>{
reject();
})
const callbacks = [sinon.fake(),sinon.fake(),sinon.fake()]
promise.then(null,callbacks[0]);
promise.then(null,callbacks[1]);
promise.then(null,callbacks[2]);
setTimeout(() => {
assert(callbacks[0].called);
assert(callbacks[1].called);
assert(callbacks[2].called);
assert(callbacks[1].calledAfter(callbacks[0]));
assert(callbacks[2].calledAfter(callbacks[1]));

done();
});
})

})

代码仓库

Node-JS专精09_02使用chai和sinon

使用chai进行测试

步骤

  • yarn global add typescript ts-node mocha 全局安装
  • 创建目录 promise-demo
  • yarn init -y / npm init -y
  • yarn add chai mocha --dev
  • yarn add @types/chai @types/mocha --dev 添加对应库的类型声明文件
  • 创建 test/index.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import * as chai from "chai";

    const assert = chai.assert;

    describe("Chai 的使用",()=>{
    it("可以测试相等",()=>{
    assert(1 === 1);

    // 因为我们用的是 ts, 你在任何一行在上 // @ts-ignore 那么ts就不会管这行符合不符合逻辑
    // 之前如果不加会报错, 因为 2 永远不等于 3 ,你这样写是没意义的
    // 由于你是在测试,所以经常会写这种代码
    // @ts-ignore
    assert( 2 === 3)
    })
    })
  • 添加运行命令 mocha -r ts-node/register test/**/*.ts 运行测试

简要说明

  • mocha 是用来提供 describe / it这两个函数 以及,yarn test 后命令行里的漂亮输出的
  • chai 是用来提供 assert 的

为了以后方便把 测试命令添加到 package.json里

1
2
3
4
5
6
7
8
9
10
11
"scripts":{
"test":"mocha -r ts-node/register test/**/*.ts"
},

// 然后运行 yarn test 结果报错了
// 因为开始的时候 我们是全局安装的 我们直接输入命令会在全局找
// 而 yarn test 会默认从本地找模块

// 所以要把依赖加入本地

yarn add typescript ts-node --dev

完善基本功能

src/promise.ts

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
class Promise2{
succeed = null;
fail = null;
resolve(){
setTimeout(() => {
this.succeed();
});
}
reject(){
setTimeout(() => {
this.fail();
});
}
constructor(fn){
if(typeof fn !== 'function'){
throw new Error("我只接受函数")
}

fn(this.resolve.bind(this), this.reject.bind(this));
}
then(succeed,fail){
this.succeed = succeed;
this.fail = fail;
}
}

export default Promise2

test/index.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import * as chai from "chai";

const assert = chai.assert;
import Promise from "../src/promise";

describe("Promise",()=>{
it("是一个类",()=>{
assert.isFunction(Promise);
assert.isObject(Promise.prototype);
})
it("new Promise() 如果接受的不是一个函数就报错",()=>{
assert.throw(()=>{
// @ts-ignore
new Promise();
})
assert.throw(()=>{
// @ts-ignore
new Promise(1);
})
assert.throw(()=>{
// @ts-ignore
new Promise(false);
})
})

it("new Promise(fn) 会生成一个对象,对象有 then 方法",()=>{
const promise = new Promise(()=>{})
assert.isFunction(promise.then);
})

it("new Promise(fn) 中的 fn立即执行",()=>{
let called = false;
const promise = new Promise(()=>{
called = true;
})
// @ts-ignore
assert(called === true);
})
it("new Promise(fn) 中的 fn 执行的时候接受 resolve 和 reject 两个函数",()=>{
let called = false;
const promise = new Promise((resolve,reject)=>{
called = true;
assert.isFunction(resolve);
assert.isFunction(reject);
})
// @ts-ignore
assert(called === true);
})

it("promise.then(success) 重的 success 会在 resolve 被调用的时候执行",(done)=>{
let called = false;
const promise = new Promise((resolve,reject)=>{
// 该函数没有执行
assert(called === false);
resolve();
setTimeout(() => {
// 该函数执行了
assert(called === true);
done();
});
})
// @ts-ignore
promise.then(()=>{
called = true;
})
})
})

使用 sinon 测试函数

上面我们经常这样来检查函数是不是被调用

1
2
3
4
5
6
7
8
it("new Promise(fn) 中的 fn立即执行",()=>{
let called = false;
const promise = new Promise(()=>{
called = true;
})
// @ts-ignore
assert(called === true);
})

步骤

  • yarn add sinon sinon-chai --dev
  • yarn add @types/sinon @types/sinon-chai --dev

基本用法

1
2
3
4
5
6
7
8
9
10
11
import * as chai from "chai";
import * as sinon from "sinon";
import * as sinonChai from "sinon-chai";
chai.use(sinonChai);
const assert = chai.assert;

it("new Promise(fn) 中的 fn立即执行",()=>{
let fn = sinon.fake();
new Promise(fn);
assert(fn.called);
})

使用 chai 改写测试用例

test/index.ts

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import * as chai from "chai";
import * as sinon from "sinon";
import * as sinonChai from "sinon-chai";
chai.use(sinonChai);
const assert = chai.assert;
import Promise from "../src/promise";
import Sinon = require("sinon");

describe("Promise",()=>{
it("是一个类",()=>{
assert.isFunction(Promise);
assert.isObject(Promise.prototype);
})
it("new Promise() 如果接受的不是一个函数就报错",()=>{
assert.throw(()=>{
// @ts-ignore
new Promise();
})
assert.throw(()=>{
// @ts-ignore
new Promise(1);
})
assert.throw(()=>{
// @ts-ignore
new Promise(false);
})
})

it("new Promise(fn) 会生成一个对象,对象有 then 方法",()=>{
const promise = new Promise(()=>{})
assert.isFunction(promise.then);
})

it("new Promise(fn) 中的 fn立即执行",()=>{
let fn = sinon.fake();
new Promise(fn);
assert(fn.called);
})
it("new Promise(fn) 中的 fn 执行的时候接受 resolve 和 reject 两个函数",(done)=>{
new Promise((resolve,reject)=>{
assert.isFunction(resolve);
assert.isFunction(reject);
done();
})
})

it("promise.then(success) 重的 success 会在 resolve 被调用的时候执行",(done)=>{
const success = sinon.fake();
const promise = new Promise((resolve,reject)=>{
// 该函数没有执行
assert.isFalse(success.called);
resolve();
setTimeout(() => {
// 该函数执行了
assert.isTrue(success.called);
done();
});
})
// @ts-ignore
promise.then(success);
})
})

/*
describe("Chai 的使用",()=>{
it("可以测试相等",()=>{
assert(1 === 1);

// 因为我们用的是 ts, 你在任何一行在上 // @ts-ignore 那么ts就不会管这行符合不符合逻辑
// 之前如果不加会报错, 因为 2 永远不等于 3 ,你这样写是没意义的
// 由于你是在测试,所以经常会写这种代码

// @ts-ignore
assert( 2 === 3)
})
})
*/

Node-JS专精09_01手写Promise

Promise

答题方法论

顺序

  • 该技术解决什么问题 why
  • 该技术是怎么解决它的 how
  • 该技术有什么优点 pros
  • 该技术有什么缺点 cons
  • 如何解决这些缺点 more

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
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err) }
else {
files.forEach(function (filename, fileIndex){
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height)
.write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
  • 有没有可能是这个程序员水平不行
    • 是不是代码写错了,不一定非要嵌套吧?

改写上面的代码

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
fs.readdir(source, (err, files)=> {
travalFiles = () => {
if(err){ return console.log('Error: 找不到目录 ' + err) }
files.forEach(gmFile)
}
gmFile = (filename) => {
console.log(filename)
gm(source + filename).size(afterGetSize)
}
afterGetSize = (err, values) => {
if (err) return console.log('无法读取文件尺寸: ' + err)
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach((width, widthIndex) => resize(width, aspect))
}
resize = (width, aspect) => {
height = Math.round(width / aspect)
console.log('将' + filename + '的尺寸变为 ' + width + 'x' + height)
this.resize(width, height)
.write(dest + 'w' + width + '_' + filename, (err) =>
err && console.log('Error writing file: ' + err)
)
}
travalFiles(err, files)
})
  • 但是现状就是:如果你用回调大家就会写出低频的这种 回调地狱的代码

001所以如果面试官问题Promise解决了什么问题?

你还是要回答回调地狱

002那么就会继续问Promise是怎么解决这个回调地狱的问题

003 Promise的优势

  • 减少缩进(异步的代码变成同步的书写方式)
    • 把函数里的函数变成 then下面的then
  • 消除if(err)
    • 错误处理单独放在一个函数里
    • 如果不处理,就一直向后抛
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
f1(xxx,function f2(a){
f3(yyy,function f4(b){
f5(a+b,function f6(){})
})
})


// 变成这样
f1(xxx)
.then(f2) // f2内部调用 f3
.then(f4) // f4内部调用 f5
.then(f6)
提问: f5怎么得到 a和b
答案: f2的输出作为 f4的输入

// 消除if(err)
f1(xxx)
.then(f2,error1) // f2内部调用 f3
.then(f4,error2) // f4内部调用 f5
.then(f6,error3)
.then(null,errorAll)
// 最后一个可以写出 .catch

004用户怎么用Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 之前
function 摇色子(fn){
setTimeout(()=>{
const result = Math.floor(Math.random()*6+1)
fn(result) // 等价于 fn.call(null, result)
},3000)
}
摇色子(n=>console.log(`摇到了${n}`))

// 现在
function 摇色子(){
// new Promise 接受一个函数,返回一个 Promise 实例
return new Promise((resolve, reject)=>{
setTimeout(()=>{
const result = Math.floor(Math.random()*6+1)
resolve(result)
},3000)
})
}
摇色子().then(n=>console.log(`摇到了${n}`))

Promise的完整API是什么

Promise是一个类

  • JS里类实际就是个函数
  • 类属性 length (可忽略) 因为我们知道它只接受一个参数
  • 类方法:all / allSettled / race / reject / resolve
  • 对象属性:then(重要) finally / catch
  • 对象内部属性: state = pending / fulfilled / rejected

如何实现一个Promise

Promise/A+ 规范文档

开始写代码

  • 按照文档写测试用例
  • 先让用例失败
  • 然后让用例通过
  • 直到把文档的所有情况都考虑清楚

使用chai

更厉害的测试方案

面试技巧

  • 弱势面试:漏出破绽让面试官问,然后立刻给出解决方案
  • 强势面试:什么破绽都不留,直接把标准答案搞出来