做爲前端人員要回答這個問題,須要瞭解這三個知識點: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
連帶出來這麼多知識點,之後面試被問到它的原理時,但願可以幫助到你
【參考】:
===🧐🧐 文中不足,歡迎指正 🤪🤪===