0%

Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

个人题解

构造函数是同步执行,then的时候是异步

1
2
3
4
5
6
7
8
9
10
11
12
13
const demo = function () {
return new Promise((reslove, reject) => {
console.log(1);
reslove(2);
console.log(3);
});
};

demo().then((res) => {
console.log(res);
});

//1 3 2

相关链接

一开始底牌尽处的人,注定是输家.

(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点

个人题解

  • callback->promise->generator/yield->async/await

    callback (最初的方案)

    最开始使用的是callback,比如浏览器端的dom事件,ajax.node上的io事件等等,但是有定位问题困难,callback hell,异常处理等问题

1
2
3
4
5
6
7
8
9
10
11
12
// 浏览器端
document.getElementById('btn').addEventListener('click',function(e){
// do something
})

// nodejs
fs.readFile('path',function(err,res)=>{
if(err){
return
}
// do something
})

promise

相比callback的回调写法,链式调用的写法更优雅,异常处理变得友好.

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

demo()
.then((res) => {
console.log(res); // something
return res + "then";
})
.then((res) => {
console.log(res);
throw new Error("err");
})
.catch((err) => {
console.log(err.message); // err
});

generator/yield

用的不多,不做评价.emmm 打扰了

async/await

异步终极解决方案,generator/yield语法糖,基于promise,同步的写法,同步代码的异常捕获能力.强!无敌!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const demo = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("something");
}, 1000);
});
};

async function test() {
try {
let res = await demo();
console.log(res); // some
throw new Error("error");
} catch (e) {
console.log(e); // error
}
}
test();

最高赞题解

JS 异步已经告一段落了,这里来一波小总结

1. 回调函数(callback)

setTimeout(() => {
// callback 函数体
}, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return

回调地狱的根本问题在于:

  • 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
  • 嵌套函数过多的多话,很难处理错误
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ajax('XXX1', () => {
    // callback 函数体
    ajax('XXX2', () => {
    // callback 函数体
    ajax('XXX3', () => {
    // callback 函数体
    })
    })
    })
    优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

2. Promise

Promise就是为了解决callback的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题

1
2
3
4
5
6
7
8
9
10
ajax('XXX1')
.then(res => {
// 操作逻辑
return ajax('XXX2')
}).then(res => {
// 操作逻辑
return ajax('XXX3')
}).then(res => {
// 操作逻辑
})

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

3. Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
```
### 4. Async/await
async、await 是异步的终极解决方案

优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
```javascript
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}

下面来看一个使用 await 的例子:

let a = 0
let b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

对于以上代码你可能会有疑惑,让我来解释下原因

首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

本文首发于我的博客:JS异步解决方案的发展历程以及优缺点

相关链接

文章链接
最高赞作者github
最高赞作者地址 JS异步解决方案的发展历程以及优缺点
阮一峰 Javascript异步编程的4种方法

题目

已知如下数组:
var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

个人题解

  • 递归;

    不想写

  • while循环遍历

    不想写

  • Array.prototype.flat

    1
    2
    3
    4
    5
    var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
    function arrayFlat(arr) {
    return Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b});
    }
    arrayFlat(arr) //
  • 骚操作

    1
    2
    3
    4
    5
    var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
    function saocaozuo(arr) {
    return [...new Set(arr.toString().split(","))].sort((a, b) => a - b);
    }
    saocaozuo(arr) //

相关链接

第 11 题:将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组
Array.prototype.flat() MDN

异步笔试题

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
  • promise2
  • async1 end
  • setTimeout

实际

  • cript start
  • async1 start
  • async2
  • promise1
  • script end
  • async1 end
  • promise2
  • setTimeout

错误的地方

  • 微任务的顺序自己理解有问题,promise1 跟async1 end 这两个地方,我以为是应该先执行then里面的内容,但实际还是按照微任务的队列进行执行.

解释

  • js是单线程,会先执行script start
  • settimeout 是宏任务,不会先执行,直接到async1(),输出async 1 start
  • 在async1 里面执行async2,输出async2,有await,让出线程
  • 执行new promise 输出 promise1
  • then方法等待,输出 script end
  • script宏任务队列清空,开始执行微任务,输出async1 end
  • 执行微任务队列,输出promise2
  • 微任务队列晴空,开始下一个宏任务 settimeout

最高赞题解

参考链接

ECMAScript 6 入门
从一道题浅说 JavaScript 的事件循环
async/await 执行顺序详解

(头条、微医)Async/Await 如何通过同步的方式实现异步

个人题解

  • 是promise的语法糖,async 返回的是promise,await 后面的语句相当于放在前一个promise的then 方法内.
  • 个人猜测是这样

最高赞题解

本质是单向链表吧

Async/Await 如何通过同步的方式实现异步
作为前端人员要回答这个问题,需要了解这三个知识点:

  • 同步
  • 异步
  • Async/Await
    首先,js 是单线程的(重复三遍),所谓单线程,
    通俗的讲就是,一根筋(比喻有点过分,哈哈)执行代码是一行一行的往下走(即所谓的同步),
    如果上面的没执行完,就痴痴的等着(是不是很像恋爱中在路边等她/他的你,假装 new 了个对象,啊哈哈哈,调皮一下很开心),
    还是举个 🌰 吧:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // chrome 75
    function test() {
    let d = Date.now();
    for (let i = 0; i < 1e8; i++) {}
    console.log(Date.now() - d); // 62ms左右
    }
    function test1() {
    let d = Date.now();

    console.log(Date.now() - d); // 0
    }
    test();
    test1();
    上面仅仅是一个 for 循环,而在实际应用中,会有大量的网络请求,它的响应时间是不确定的,这种情况下也要痴痴的等么?显然是不行的,因而 js 设计了异步,即 发起网络请求(诸如 IO 操作,定时器),由于需要等服务器响应,就先不理会,而是去做其他的事儿,等请求返回了结果的时候再说(即异步)。
    那么如何实现异步呢?其实我们平时已经在大量使用了,那就是 callback,例如:
    1
    2
    3
    4
    5
    6
    $.ajax({
    url: 'http://xxx',
    success: function(res) {
    console.log(res);
    },
    });
    success 作为函数传递过去并不会立即执行,而是等请求成功了才执行,即回调函数(callback)
    1
    2
    3
    4
    5
    const fs = require('fs');
    fs.rename('旧文件.txt', '新文件.txt', err => {
    if (err) throw err;
    console.log('重命名完成');
    });
    和网络请求类似,等到 IO 操作有了结果(无论成功与否)才会执行第三个参数:(err)=>{}

从上面我们就可以看出,实现异步的核心就是回调钩子,将 cb 作为参数传递给异步执行函数,当有了结果后在触发 cb。想了解更多,去看看 event-loop 机制吧。

至于 async/await 是如何出现的呢,在 es6 之前,大多 js 数项目中会有类似这样的代码:

1
2
3
4
5
6
7
ajax1(url, () => {
ajax2(url, () => {
ajax3(url, () => {
// do something
});
});
});

这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为回调地狱(callback hell),所以为了在写法上能更通俗一点,es6+陆续出现了 Promise、Generator、Async/await,力求在写法上简洁明了,可读性强。

=========================我是分割线==========================

以上只是铺垫,下面在进入正题 👇,开始说道说道主角:async/await

=========================我是分割线==========================

async/await 是参照 Generator 封装的一套异步处理方案,可以理解为 Generator 的语法糖,

所以了解 async/await 就不得不讲一讲 Generator,

而 Generator 又依赖于迭代器Iterator,

所以就得先讲一讲 Iterator,

而 Iterator 的思想呢又来源于单向链表,

终于找到源头了:单向链表

1. 单向链表

wiki:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序储存数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序储存,链表在插入的时候可以达到 o(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。

总结一下链表优点:

无需预先分配内存
插入/删除节点不影响其他节点,效率高(典型的例子:git commit、dom 操作)
单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
20200819112119
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接

单链特点:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。

理解 js 原型链/作用域链的话,理解这个很容易,他们是相通的。编程语言中,数组的长度时固定的,所以数组中的增加和删除比较麻烦,需要频繁的移动数组中的其他元素,而 js 作为一门动态语言,数组本质是一个类似数组的对象,是动态的,不需要预先分配内存

那么如何设计一个单向链表呢?这个取决于我们需要哪些操作,通常有:

  • append(element):追加节点
  • insert(element,index):在索引位置插入节点
  • remove(element):删除第一个匹配到的节点
  • removeAt(index):删除指定索引节点
  • removeAll(element):删除所有匹配的节点
  • get(index):获取指定索引的节点信息
  • set(element,index):修改指定索引的节点值
  • indexOf(element):获取某节点的索引位置
  • clear():清除所有节点
  • length():返回节点长度
  • printf():打印节点信息
    看到这些方法是不是有些许熟悉,当你用原生 js 或 jq 时常会用上面类似的方法,现在根据上面列出的方法进行实现一个单向链:
    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
    // 节点模型
    class LinkNode {
    constructor(element, next) {
    this.element = element;
    this.next = next;
    }
    }

    class LinkedList {
    constructor() {
    this._head = null;
    this._size = 0;
    this._errorBoundary = this._errorBoundary.bind(this);
    this._getNodeByIndex = this._getNodeByIndex.bind(this);
    this.append = this.append.bind(this);
    this.insert = this.insert.bind(this);
    this.remove = this.remove.bind(this);
    this.removeAt = this.removeAt.bind(this);
    this.removeAll = this.removeAll.bind(this);
    this.getElement = this.getElement.bind(this);
    this.setIndex = this.setIndex.bind(this);
    this.indexOf = this.indexOf.bind(this);
    this.clear = this.clear.bind(this);
    this.length = this.length.bind(this);
    this.printf = this.printf.bind(this);
    }

    // 边界检验
    _errorBoundary(index) {
    if (index < 0 || index >= this._size) {
    throw `超出边界(${0}~${this._size}),目标位置${index}不存在!`;
    }
    }
    // 根据索引获取目标对象
    _getNodeByIndex(index) {
    this._errorBoundary(index);
    let obj = this._head;
    for (let i = 0; i < index; i++) {
    obj = obj.next;
    }
    return obj;
    }
    // 追加节点
    append(element) {
    if (this._size === 0) {
    this._head = new LinkNode(element, null);
    } else {
    let obj = this._getNodeByIndex(this._size - 1);
    obj.next = new LinkNode(element, null);
    }
    this._size++;
    }
    // 在索引位置插入节点
    insert(element, index) {
    if (index === 0) {
    this._head = new LinkNode(element, this._head);
    } else {
    let obj = this._getNodeByIndex(index - 1);
    obj.next = new LinkNode(element, obj.next);
    }
    this._size++;
    }
    // 删除第一个匹配到的节点
    remove(element) {
    if (this._size < 1) return null;

    if (this._head.element == element) {
    this._head.element = this._head.next;
    this._size--;
    return element;
    } else {
    let temp = this._head;
    while (temp.next) {
    if (temp.next.element == element) {
    temp.next = temp.next.next;
    this._size--;
    return element;
    } else {
    temp = temp.next;
    }
    }
    }
    return null;
    }
    // 删除指定索引节点
    removeAt(index) {
    this._errorBoundary(index);
    let element = null;
    if (index === 0) {
    element = this._head.element;
    this._head = this._head.next;
    } else {
    let prev = this._getNodeByIndex(index - 1);
    element = prev.next.element;
    prev.next = prev.next.next;
    }
    this._size--;
    return element;
    }
    // 删除所有匹配的节点
    removeAll(element) {
    // 创建虚拟头节点,
    let v_head = new LinkNode(null, this._head);
    let tempNode = v_head;
    // let tempEle = null;
    while (tempNode.next) {
    if (tempNode.next.element == element) {
    tempNode.next = tempNode.next.next;
    this._size--;
    // tempEle = element;
    } else {
    tempNode = tempNode.next;
    }
    }
    this._head = v_head.next;
    }
    // 获取指定索引的节点信息
    getElement(index) {
    return this._getNodeByIndex(index).element;
    }
    // 修改指定索引的节点值
    setIndex(element, index) {
    this._errorBoundary(index);
    let obj = this._getNodeByIndex(index);
    obj.element = element;
    }
    // 获取某节点的索引位置
    indexOf(element) {
    let obj = this._head;
    let index = -1;
    for (let i = 0; i < this._size; i++) {
    if (obj.element == element) {
    index = i;
    break;
    }
    obj = obj.next;
    }
    return index;
    }
    // 清除所有节点
    clear() {
    this._head = null;
    this._size = 0;
    }
    // 返回节点长度
    length() {
    return this._size;
    }
    // 打印节点信息
    printf() {
    let obj = this._head;
    const arr = [];
    while (obj != null) {
    arr.push(obj.element);
    obj = obj.next;
    }
    const str = arr.join('->');
    return str || null;
    }
    }

    const obj = new LinkedList();
    obj.append(0);
    obj.append(1);
    obj.append(2);
    obj.printf();
    // "0->1->2"

    obj.insert(3, 3);
    obj.printf();
    // "0->1->2->3"

    obj.remove(3);
    obj.printf();
    // "0->1->2"

    obj.removeAt(0);
    obj.printf();
    // "1->2"

    obj.setIndex(0, 0);
    obj.printf();
    // "0->2"

    obj.indexOf(2);
    // 1

    obj.length();
    // 2

    obj.clear();
    obj.printf();
    // null

通过以上,我假装你明白什么是单向链表,并且能够用代码实现一个单向链表了,下一步开始说一说迭代器 Iterator

2. Iterator

Iterator 翻译过来就是迭代器(遍历器)让我们先来看看它的遍历过程(类似于单向链表):

创建一个指针对象,指向当前数据结构的起始位置
第一次调用指针对象的 next 方法,将指针指向数据结构的第一个成员
第二次调用指针对象的 next 方法,将指针指向数据结构的第二个成员
不断的调用指针对象的 next 方法,直到它指向数据结构的结束位置
一个对象要变成可迭代的,必须实现 @@iterator 方法,即对象(或它原型链上的某个对象)必须有一个名字是 Symbol.iterator 的属性(原生具有该属性的有:字符串、数组、类数组的对象、Set 和 Map):

属性
[Symbol.iterator] 返回一个对象的无参函数,被返回对象符合迭代器协议

当一个对象需要被迭代的时候(比如开始用于一个 for..of 循环中),它的 @@iterator 方法被调用并且无参数,然后返回一个用于在迭代中获得值的迭代器

迭代器协议:产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值

当一个对象只有满足下述条件才会被认为是一个迭代器:

它实现了一个 next() 的方法,该方法必须返回一个对象,对象有两个必要的属性:

done(bool)
true:迭代器已经超过了可迭代次数。这种情况下,value 的值可以被省略
如果迭代器可以产生序列中的下一个值,则为 false。这等效于没有指定 done 这个属性
value 迭代器返回的任何 JavaScript 值。done 为 true 时可省略
根据上面的规则,咱们来自定义一个简单的迭代器:

const makeIterator = arr => {
  let nextIndex = 0;
  return {
    next: () =>
      nextIndex < arr.length
        ? { value: arr[nextIndex++], done: false }
        : { value: undefined, done: true },
  };
};
const it = makeIterator(['人月', '神话']);
console.log(it.next()); // { value: "人月", done: false }
console.log(it.next()); // { value: "神话", done: false }
console.log(it.next()); // {value: undefined, done: true }
我们还可以自定义一个可迭代对象:

const myIterable = {};
myIterable[Symbol.iterator] = function*() {
  yield 1;
  yield 2;
  yield 3;
};

for (let value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

//or

console.log([...myIterable]); // [1, 2, 3]

了解了迭代器,下面可以进一步了解生成器了

3. Generator

Generator:生成器对象是生成器函数(GeneratorFunction)返回的,它符合可迭代协议和迭代器协议,既是迭代器也是可迭代对象,可以调用 next 方法,但它不是函数,更不是构造函数

生成器函数(GeneratorFunction):

function* name([param[, param[, … param]]]) { statements } –>

name:函数名
param:参数
statements:js 语句
调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象,当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield 的位置为止(让执行处于暂停状),yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行),调用 next() (再启动)方法时,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值,例如:

function* another() {
  yield '人月神话';
}
function* gen() {
  yield* another(); // 移交执行权
  const a = yield 'hello';
  const b = yield a; // a='world' 是 next('world') 传参赋值给了上一个 yidle 'hello' 的左值
  yield b; // b=! 是 next('!') 传参赋值给了上一个 yidle a 的左值
}
const g = gen();
g.next(); // {value: "人月神话", done: false}
g.next(); // {value: "hello", done: false}
g.next('world'); // {value: "world", done: false} 将 'world' 赋给上一条 yield 'hello' 的左值,即执行 a='world',
g.next('!'); // {value: "!", done: false} 将 '!' 赋给上一条 yield a 的左值,即执行 b='!',返回 b
g.next(); // {value: undefined, done: false}

看到这里,你可能会问,Generator 和 callback 有啥关系,如何处理异步呢?其实二者没有任何关系,我们只是通过一些方式强行的它们产生了关系,才会有 Generator 处理异步

我们来总结一下 Generator 的本质,暂停,它会让程序执行到指定位置先暂停(yield),然后再启动(next),再暂停(yield),再启动(next),而这个暂停就很容易让它和异步操作产生联系,因为我们在处理异步时:开始异步处理(网络求情、IO 操作),然后暂停一下,等处理完了,再该干嘛干嘛。不过值得注意的是,js 是单线程的(又重复了三遍),异步还是异步,callback 还是 callback,不会因为 Generator 而有任何改变

下面来看看,用 Generator 实现异步:

const promisify = require('util').promisify;
const path = require('path');
const fs = require('fs');
const readFile = promisify(fs.readFile);

const gen = function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
};

const g = gen();

const g1 = g.next();
console.log('g1:', g1);

g1.value
  .then(res1 => {
    console.log('res1:', res1);
    const g2 = g.next(res1);
    console.log('g2:', g2);
    g2.value
      .then(res2 => {
        console.log('res2:', res2);
        g.next(res2);
      })
      .catch(err2 => {
        console.log(err2);
      });
  })
  .catch(err1 => {
    console.log(err1);
  });
// g1: { value: Promise { <pending> }, done: false }
// res1: {
//   "a": 1
// }

// {
//   "a": 1
// }

// g2: { value: Promise { <pending> }, done: false }
// res2: {
//   "b": 2
// }

// {
//   "b": 2
// }

以上代码是 Generator 和 callback 结合实现的异步,可以看到,仍然需要手动执行 .then 层层添加回调,但由于 next() 方法返回对象 {value: xxx,done: true/false} 所以我们可以简化它,写一个自动执行器:

const promisify = require('util').promisify;
const path = require('path');
const fs = require('fs');
const readFile = promisify(fs.readFile);

function run(gen) {
  const g = gen();
  function next(data) {
    const res = g.next(data);
    // 深度递归,只要 `Generator` 函数还没执行到最后一步,`next` 函数就调用自身
    if (res.done) return res.value;
    res.value.then(function(data) {
      next(data);
    });
  }
  next();
}
run(function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  // {
  //   "a": 1
  // }
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
  // {
  //   "b": 2
  // }
});
说了这么多,怎么还没有到 async/await,客官别急,马上来了(其实我已经漏了一些内容没说:Promise 和 callback 的关系,thunk 函数,co 库,感兴趣的可以去 google 一下,ruanyifeng 老师讲的es6 入门非常棒,我时不时的都会去看一看)

4. Async/Await
首先,async/await 是 Generator 的语法糖,上面我是分割线下的第一句已经讲过,先来看一下二者的对比:

// Generator
run(function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
});

// async/await
const readFile = async ()=>{
  const res1 = await readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = await readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
  return 'done';
}
const res = readFile();

可以看到,async function 代替了 function*,await 代替了 yield,同时也无需自己手写一个自动执行器 run 了

现在再来看看async/await 的特点:

当 await 后面跟的是 Promise 对象时,才会异步执行,其它类型的数据会同步执行
执行 const res = readFile(); 返回的仍然是个 Promise 对象,上面代码中的 return ‘done’; 会直接被下面 then 函数接收到
res.then(data => {
console.log(data); // done
});
啊,终于完了,一个 async-await 连带出来这么多知识点,以后面试被问到它的原理时,希望能够帮助到你

相关链接

setTimeout、Promise、Async/Await 的区别

个人题解

  • setTimeout 只是一个异步的定时器,用来执行多少时间后的任务
  • promise 是一个规范,用then,catch,all,race等方法更好的处理异步内容
  • async/await 是一个promise的语法糖,不了解具体的实现

没想到是考察宏任务,微任务的,打扰了.草.

最高赞题解

setTimeout

1
2
3
4
5
6
console.log('script start')	//1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout
  1. Promise
  • Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    console.log('script start')
    let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
    }).then(function () {
    console.log('promise2')
    })
    setTimeout(function(){
    console.log('settimeout')
    })
    console.log('script end')
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
  • 当JS主线程执行到Promise对象时,

  • promise1.then() 的回调就是一个 task

  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue

  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中

  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}

console.log('script start');
async1();
console.log('script end')

// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

  • 举个例子:
1
2
3
4
async function func1() {
return 1
}
console.log(func1())
  • 在这里插入图片描述
  • 很显然,func1的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。
1
2
3
func1().then(res => {
console.log(res); // 30
})

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

更多可见setTimeout、Promise、Async/Await
浏览器的Tasks、microtasks、 queues 和 schedules #21
第 8 题:setTimeout、Promise、Async/Await 的区别 #33

ES5/ES6 的继承除了写法以外还有什么区别?

个人题解

都是基于原型链的写法,别的不知道.

最高题解

class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。(没有变量提升)

1
2
3
4
5
6
7
8
9
10
11
const bar = new Bar(); // it's ok
function Bar() {
this.bar = 42;
}

const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
constructor() {
this.foo = 42;
}
}

class 声明内部会启用严格模式。

1
2
3
4
5
6
7
8
9
10
11
12
// 引用一个未声明的变量
function Bar() {
baz = 42; // it's ok
}
const bar = new Bar();

class Foo {
constructor() {
fol = 42; // ReferenceError: fol is not defined
}
}
const foo = new Foo();

class 的所有方法(包括静态方法和实例方法)都是不可枚举的。

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
// 引用一个未声明的变量
function Bar() {
this.bar = 42;
}
Bar.answer = function() {
return 42;
};
Bar.prototype.print = function() {
console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']

class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []

class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Bar() {
this.bar = 42;
}
Bar.prototype.print = function() {
console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
constructor() {
this.foo = 42;
}
print() {
console.log(this.foo);
}
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor

必须使用 new 调用 class。

1
2
3
4
5
6
7
8
9
10
11
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok

class Foo {
constructor() {
this.foo = 42;
}
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'

class 内部无法重写类名。

 function Bar() {
  Bar = 'Baz'; // it's ok
  this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}  

class Foo {
  constructor() {
    this.foo = 42;
    Foo = 'Fol'; // TypeError: Assignment to constant variable
  }
}
const foo = new Foo();
Foo = 'Fol'; // it's ok

相关链接

第 7 期:ES5/ES6 的继承除了写法以外还有什么区别? #20

介绍下深度优先遍历和广度优先遍历,如何实现?

个人题解

没有做过相关的内容,猜测

深度优先遍历

先找到一个节点, 遍历到当前节点最深的目录

广度优先遍历

优先遍历当前一级的节点,然后再遍历下一层节点

最高赞题

  • 第五题问的是深度优先遍历和广度优先遍历,我是从dom节点的遍历来理解这个问题的
  • html代码
  • 20200807134451
  • 我将用深度优先遍历和广度优先遍历对这个dom树进行查找

深度优先遍历


深度优先遍历DFS 与树的先序遍历比较类似。
假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

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
/*深度优先遍历三种方式*/
let deepTraversal1 = (node, nodeList = []) => {
if (node !== null) {
nodeList.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
deepTraversal1(children[i], nodeList)
}
}
return nodeList
}
let deepTraversal2 = (node) => {
let nodes = []
if (node !== null) {
nodes.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
nodes = nodes.concat(deepTraversal2(children[i]))
}
}
return nodes
}
// 非递归
let deepTraversal3 = (node) => {
let stack = []
let nodes = []
if (node) {
// 推入当前处理的node
stack.push(node)
while (stack.length) {
let item = stack.pop()
let children = item.children
nodes.push(item)
// node = [] stack = [parent]
// node = [parent] stack = [child3,child2,child1]
// node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
// node = [parent, child1-1] stack = [child3,child2,child1-2]
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i])
}
}
}
return nodes
}

输出结果

20200807134542

广度优先遍历


广度优先遍历 BFS
从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let widthTraversal2 = (node) => {
let nodes = []
let stack = []
if (node) {
stack.push(node)
while (stack.length) {
let item = stack.shift()
let children = item.children
nodes.push(item)
// 队列,先进先出
// nodes = [] stack = [parent]
// nodes = [parent] stack = [child1,child2,child3]
// nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
// nodes = [parent,child1,child2]
for (let i = 0; i < children.length; i++) {
stack.push(children[i])
}
}
}
return nodes
}

输出结果

20200807134554

相关链接

第 5 题:介绍下深度优先遍历和广度优先遍历,如何实现? #9

介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

个人题解

Set

目前用到Set的功能,是用来去重

Map

跟object很像,比object多了一些实用的操作方法,object的key只能是字符串,这个的key值可以是对象,数字等

WeakSet 和 WeakMap

没用过

最高赞题解

Set

  1. 成员不能重复
  2. 只有健值,没有健名,有点类似数组。
  3. 可以遍历,方法有add, delete,has

weakSet

成员都是对象
成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
不能遍历,方法有add, delete,has

Map

本质上是健值对的集合,类似集合
可以遍历,方法很多,可以干跟各种数据格式转换

weakMap

  1. 直接受对象作为健名(null除外),不接受其他类型的值作为健名
  2. 健名所指向的对象,不计入垃圾回收机制
  3. 不能遍历,方法同get,set,has,delete

相关链接

第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
ECMAScript 6 入门 – Set 和 Map 数据结构