做为前端人员要回答这个问题,须要了解这三个知识点:javascript
首先,js 是单线程的(重复三遍),所谓单线程, 通俗的讲就是,一根筋(比喻有点过度,哈哈)执行代码是一行一行的往下走(即所谓的同步), 若是上面的没执行完,就痴痴的等着(是否是很像恋爱中在路边等她/他的你,伪装 new 了个对象,啊哈哈哈,调皮一下很开心), 仍是举个 🌰 吧:前端
// 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
,例如:java
$.ajax({
url: 'http://xxx',
success: function(res) {
console.log(res);
},
});
复制代码
success 做为函数传递过去并不会当即执行,而是等请求成功了才执行,即回调函数(callback)git
const fs = require('fs');
fs.rename('旧文件.txt', '新文件.txt', err => {
if (err) throw err;
console.log('重命名完成');
});
复制代码
和网络请求相似,等到 IO 操做有告终果(不管成功与否)才会执行第三个参数:(err)=>{}
es6
从上面咱们就能够看出,实现异步的核心就是回调钩子,将 cb 做为参数传递给异步执行函数,当有告终果后在触发 cb。想了解更多,去看看 event-loop
机制吧。github
至于 async/await 是如何出现的呢,在 es6 以前,大多 js 数项目中会有相似这样的代码:面试
ajax1(url, () => {
ajax2(url, () => {
ajax3(url, () => {
// do something
});
});
});
复制代码
这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为回调地狱(callback hell),因此为了在写法上能更通俗一点,es6+陆续出现了 Promise
、Generator
、Async/await
,力求在写法上简洁明了,可读性强。ajax
=========================我是分割线==========================chrome
以上只是铺垫,下面在进入正题 👇,开始说道说道主角:async/await
编程
=========================我是分割线==========================
async/await
是参照 Generator
封装的一套异步处理方案,能够理解为 Generator
的语法糖,
因此了解 async/await
就不得不讲一讲 Generator
,
而 Generator
又依赖于迭代器Iterator
,
因此就得先讲一讲 Iterator
,
而 Iterator
的思想呢又来源于单向链表,
终于找到源头了:单向链表
wiki:链表(Linked list)是一种常见的基础数据结构,是一种线性表,可是并不会按线性的顺序储存数据,而是在每个节点里存到下一个节点的指针(Pointer)。因为没必要须按顺序储存,链表在插入的时候能够达到 o(1)的复杂度,比另外一种线性表顺序表快得多,可是查找一个节点或者访问特定编号的节点则须要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。
总结一下链表优势:
单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个连接指向列表中的下一个节点,而最后一个节点则指向一个空值。
单链特色:节点的连接方向是单向的;相对于数组来讲,单链表的的随机访问速度较慢,可是单链表删除/添加数据的效率很高。
理解 js 原型链/做用域链的话,理解这个很容易,他们是相通的。编程语言中,数组的长度时固定的,因此数组中的增长和删除比较麻烦,须要频繁的移动数组中的其余元素,而 js 做为一门动态语言,数组本质是一个相似数组的对象,是动态的,不须要预先分配内存
那么如何设计一个单向链表呢?这个取决于咱们须要哪些操做,一般有:
看到这些方法是否是有些许熟悉,当你用原生 js 或 jq 时常会用上面相似的方法,如今根据上面列出的方法进行实现一个单向链:
// 节点模型
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
Iterator
翻译过来就是**迭代器(遍历器)**让咱们先来看看它的遍历过程(相似于单向链表):
next
方法,将指针指向数据结构的第一个成员next
方法,将指针指向数据结构的第二个成员next
方法,直到它指向数据结构的结束位置一个对象要变成可迭代的,必须实现 @@iterator
方法,即对象(或它原型链上的某个对象)必须有一个名字是 Symbol.iterator
的属性(原生具备该属性的有:字符串、数组、类数组的对象、Set 和 Map):
属性 | 值 |
---|---|
[Symbol.iterator]: | 返回一个对象的无参函数,被返回对象符合迭代器协议 |
当一个对象须要被迭代的时候(好比开始用于一个 for..of
循环中),它的 @@iterator
方法被调用而且无参数,而后返回一个用于在迭代中得到值的迭代器
迭代器协议:产生一个有限或无限序列的值,而且当全部的值都已经被迭代后,就会有一个默认的返回值
当一个对象只有知足下述条件才会被认为是一个迭代器:
它实现了一个 next()
的方法,该方法必须返回一个对象,对象有两个必要的属性:
done
(bool)
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]
复制代码
了解了迭代器,下面能够进一步了解生成器了
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 一下,yuanyifeng 老师讲的es6 入门很是棒,我时不时的都会去看一看)
首先,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
连带出来这么多知识点,之后面试被问到它的原理时,但愿可以帮助到你
【参考】:
===🧐🧐 文中不足,欢迎指正 🤪🤪===