您好,登錄后才能下訂單哦!
本篇內容介紹了“JavaScript異步編程使用的方法”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
按照維基百科上的解釋:獨立于主控制流之外發生的事件就叫做異步。比如說有一段順序執行的代碼
void function main() { fA(); fB(); }();
fA => fB 是順序執行的,永遠都是 fA 在 fB 的前面執行,他們就是 同步 的關系。加入這時使用 setTimeout 將 fB 延后
void function main() { setTimeout(fA, 1000); fB(); }();
這時,fA 相對于 fB 就是異步的。main 函數只是聲明了要在一秒后執行一次 fA,而并沒有立刻執行它。這時,fA 的控制流就獨立于 main 之外。
JavaScript——天生異步的語言
因為 setTimeout 的存在,至少在被 ECMA 標準化的那一刻起,JavaScript 就支持異步編程了。與其他語言的 sleep 不同,setTimeout 是異步的——它不會阻擋當前程序繼續往下執行。
然而異步編程真正發展壯大,Ajax 的流行功不可沒。Ajax 中的 A(Asynchronous)真正點到了異步的概念——這還是 IE5、IE6 的時代。
回調函數——異步編程之痛
異步任務執行完畢之后怎樣通知開發者呢?回調函數是最樸素的,容易想到的實現方式。于是從異步編程誕生的那一刻起,它就和回調函數綁在了一起。
例如 setTimeout。這個函數會起一個定時器,在超過指定時間后執行指定的函數。比如在一秒后輸出數字 1,代碼如下:
setTimeout(() => { console.log(1); }, 1000);
常規用法。如果需求有變,需要每秒輸出一個數字(當然不是用 setInterval),JavaScript 的初學者可能會寫出這樣的代碼:
for (let i = 1; i < 10; ++i) { setTimeout(() => { // 錯誤! console.log(i); }, 1000); }
執行結果是等待 1 秒后,一次性輸出了所有結果。因為這里的循環是同時啟了 10 個定時器,每個定時器都等待 1 秒,結果當然是所有定時器在 1 秒后同時超時,觸發回調函數。
解法也簡單,只需要在前一個定時器超時后再啟動另一個定時器,代碼如下:
setTimeout(() => { console.log(1); setTimeout(() => { console.log(2); setTimeout(() => { console.log(3); setTimeout(() => { console.log(4); setTimeout(() => { console.log(5); setTimeout(() => { // ... }, 1000); }, 1000); }, 1000) }, 1000) }, 1000) }, 1000);
層層嵌套,結果就是這樣的漏斗形代碼。可能有人想到了新標準中的 Promise,可以改寫如下:
function timeout(delay) { return new Promise(resolve => { setTimeout(resolve, delay); }); } timeout(1000).then(() => { console.log(1); return timeout(1000); }).then(() => { console.log(2); return timeout(1000); }).then(() => { console.log(3); return timeout(1000); }).then(() => { console.log(4); return timeout(1000); }).then(() => { console.log(5); return timeout(1000); }).then(() => { // .. });
漏斗形代碼是沒了,但代碼量本身并沒減少多少。Promise 并沒能干掉回調函數。
因為回調函數的存在,循環就無法使用。不能循環,那么只能考慮遞歸了,解法如下:
let i = 1; function next() { console.log(i); if (++i < 10) { setTimeout(next, 1000); } } setTimeout(next, 1000);
注意雖然寫法是遞歸,但由于 next 函數都是由瀏覽器調用的,所以實際上并沒有遞歸函數的調用棧結構。
Generator——JavaScript 中的半協程
很多語言都引入了協程來簡化異步編程,JavaScript 也有類似的概念,叫做 Generator。
MDN 上的解釋:Generator 是一種可以中途退出之后重入的函數。他們的函數上下文在每次重入后會被保持。簡而言之,Generator 與普通 Function ***的區別就是:Generator 自身保留上次調用的狀態。
舉個簡單的例子:
function *gen() { yield 1; yield 2; return 3; } void function main() { var iter = gen(); console.log(iter.next().value); console.log(iter.next().value); console.log(iter.next().value); }();
代碼的執行順序是這樣:
請求 gen,得到一個迭代器 iter。注意此時并未真正執行 gen 的函數體。
調用 iter.next(),執行 gen 的函數體。
遇到 yield 1,將 1 返回,iter.next() 的返回值即為 { done: false, value: 1 },輸出 1
調用 iter.next()。從上次 yield 出去的地方繼續往下執行 gen。
遇到 yield 2,將 2 返回,iter.next() 的返回值即為 { done: false, value: 2 },輸出 2
調用 iter.next()。從上次 yield 出去的地方繼續往下執行 gen。
遇到 return 3,將 3 返回,return 表示整個函數已經執行完畢。iter.next() 的返回值即為 { done: true, value: 3 },輸出 3
調用 Generator 函數只會返回一個迭代器,當用戶主動調用了 iter.next() 后,這個 Generator 函數才會真正執行。
你可以使用 for ... of 遍歷一個 iterator,例如
for (var i of gen()) { console.log(i); }
輸出 1 2,*** return 3 的結果不算在內。想用 Generator 的各項生成一個數組也很簡單,Array.from(gen()) 或直接用 [...gen()] 即可,生成 [1, 2] 同樣不包含***的 return 3。
Generator 是異步的嗎
Generator 也叫半協程(semicoroutine),自然與異步關系匪淺。那么 Generator 是異步的嗎?
既是也不是。前面提到,異步是相對的,例如上面的例子
function *gen() { yield 1; yield 2; return 3; } void function main() { var iter = gen(); console.log(iter.next().value); console.log(iter.next().value); console.log(iter.next().value); }();
我們可以很直觀的看到,gen 的方法體與 main 的方法體在交替執行,所以可以肯定的說,gen 相對于 main 是異步執行的。然而此段過程中,整個控制流都沒有交回給瀏覽器,所以說 gen 和 main 相對于瀏覽器是同步執行的。
用 Generator 簡化異步代碼
回到最初的問題:
for (let i = 0; i < 10; ++i) { setTimeout(() => { console.log(i); }, 1000); // 等待上面 setTimeout 執行完畢 }
關鍵在于如何等待前面的 setTimeout 觸發回調后再執行下一輪循環。如果使用 Generator,我們可以考慮在 setTimeout 后 yield 出去(控制流返還給瀏覽器),然后在 setTimeout 觸發的回調函數中 next,將控制流交還回給代碼,執行下一段循環。
let iter; function* run() { for (let i = 1; i < 10; ++i) { setTimeout(() => iter.next(), 1000); yield; // 等待上面 setTimeout 執行完畢 console.log(i); } } iter = run(); iter.next();
代碼的執行順序是這樣:
請求 run,得到一個迭代器 iter。注意此時并未真正執行 run 的函數體。
調用 iter.next(),執行 run 的函數體。
循環開始,i 初始化為 1。
執行 setTimeout,啟動一個定時器,回調函數延后 1 秒執行。
遇到 yield(即 yield undefined),控制流返回到***的 iter.next() 之后。因為后面沒有其他代碼了,瀏覽器獲得控制權,響應用戶事件,執行其他異步代碼等。
1 秒后,setTimeout 超時,執行回調函數 () => iter.next()。
調用 iter.next()。從上次 yield 出去的地方繼續往下執行,即 console.log(i),輸出 i 的值。
一次循環結束,i 自增為 2,回到第 4 步繼續執行
……
這樣即實現了類似同步 sleep 的要求。
async、await——用同步語法寫異步代碼
上面的代碼畢竟需要手工定義迭代器變量,還要手工 next;更重要的是與 setTimeout 緊耦合,無法通用。
我們知道 Promise 是異步編程的未來。能不能把 Promise 和 Generator 結合使用呢?這樣考慮的結果就是 async 函數。
用 async 得到代碼如下
function timeout(delay) { return new Promise(resolve => { setTimeout(resolve, delay); }); } async function run() { for (let i = 1; i < 10; ++i) { await timeout(1000); console.log(i); } } run();
按照 Chrome 的設計文檔,async 函數內部就是被編譯為 Generator 執行的。run 函數本身會返回一個 Promise,用于使主調函數得知 run 函數什么時候執行完畢。所以 run() 后面也可以 .then(xxx),甚至直接 await run()。
注意有時候我們的確需要幾個異步事件并行執行(比如調用兩個接口,等兩個接口都返回后執行后續代碼),這時就不要過度使用 await,例如:
const a = await queryA(); // 等待 queryA 執行完畢后 const b = await queryB(); // 執行 queryB doSomething(a, b);
這時 queryA 和 queryB 就是串行執行的。可以略作修改:
const promiseA = queryA(); // 執行 queryA const b = await queryB(); // 執行 queryB 并等待其執行結束。這時同時 queryA 也在執行。 const a = await promiseA(); // 這時 queryB 已經執行結束。繼續等待 queryA 執行結束 doSomething(a, b);
我個人比較喜歡如下寫法:
const [ a, b ] = await Promise.all([ queryA(), queryB() ]); doSomething(a, b);
將 await 和 Promise 結合使用,效果更佳!
“JavaScript異步編程使用的方法”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。