平常開發過程當中,時不時會遇到要同時預加載幾張圖片,而且等都加載完再幹活的狀況,結合 Promise 和 async/await 代碼會優雅不少,但也容易遇到坑,今天就來簡單聊聊。javascript
先從最基本的 ES5 提及,基本思路就是作一個計數器,每次 image 觸發 onload 就加一,達到次數後觸發回調函數。html
var count = 0, imgs = []; function loadImgs(imgList, cb) { imgList.forEach(function(url, i) { imgs[i] = new Image(); imgs[i].onload = function() { if( ++count === imgList.length) { cb && cb() } } imgs[i].src = url; }) } 複製代碼
調用方法:前端
loadImgs(["xxx/a.png","xxx/b.png"],function() { console.log("開始幹活"); }) 複製代碼
這樣作基本功能是能知足的,可是這種回調的方式跳來跳去,代碼顯得比較混亂。java
俗話說,異步編程的最高境界,就是根本不用關心它是否是異步。能用同步的方式寫出異步的代碼,纔是好的編碼體驗。因而乎,到 Promise 和 async/await 出場了。node
讓咱們用 Promise 和 async/await 來改寫一下。(注意,這個例子有個很大的問題)webpack
async function loadImgs(imgList, cb) { console.log("start") for( var i =0; i<imgList.length; i++) { await imgLoader(imgList[i], i); console.log("finish"+i) } cb(); } async function imgLoader(url, num){ return new Promise((resolve, reject) => { console.log("request"+num) setTimeout(resolve, 1000); // let img = new Image(); // img.onload = () => resolve(img); // img.onerror = reject; console.log("return"+num) }) } loadImgs(["xxx/a.png","xxx/b.png"],function() { console.log("開始幹活"); }) 複製代碼
爲了方便在 node 環境中運行代碼,這裏我用 setTimeout 代替了真正的圖片加載。git
運行的結果是:程序員
start
request0
return0
finish0
request1
return1
finish1
開始幹活
複製代碼
有沒有發現問題,雖然咱們指望的是用同步代碼的形式寫出異步的效果,雖然咱們用了 async/await Promise 等吊炸天的東西,可是實際運行的結果倒是同步的。 request0 finish 以後,request1 才發出。es6
這樣的代碼雖然語義清晰,通俗易懂,但等圖片一張一張順序加載是咱們不能接受的,同時發出幾個請求異步加載是咱們的目標。github
產生這種錯誤的緣由是 async/await 其實只是語法糖並非說加了就異步了,其本質上是爲了解決回調嵌套過多的問題。
N 年前,經過分發 jQuery 武器,你們捲起袖子加入了前端大潮,然而他們遇到的一個大問題就是」回調地獄「。
好比下面這個例子,發完三個 ajax 請求以後才能開始幹活。
$.ajax({ url: "xxx/xxx", data: 123, success: function () { $.ajax({ url: "xxx/xxx2", data:456, success: function () { $.ajax({ url: "xxx/xxx3", data:789, success: function () { // 終於完了能夠開始幹事情了 } }) } }) } }) 複製代碼
這個還只是把簡單的代碼結構寫出來,括號就多到眼花,若是再加上業務邏輯、錯誤處理等,那就是實實在在的」地獄「。
Promise 的出現大大改善了回調地獄,寫法也更加接近同步。
簡單來講,Promise 就是一個容器,裏面保存着某個已經發生將來纔會結束的事件,當事件結束時,會自動調用一個統一的接口告訴你。
var promise = new Promise(function(resolve, reject) { $.ajax({ url: "xxx/xxx3", success: function () { resolve(rs) }, }) } // 調用的時候 promise.then(function(rs){ // 返回另外一個 Promise return new Promise(...) }) .then(function(rs){ // 又返回另外一個 Promise return new Promise(...) }) .then(function(rs){ // 開始幹活 }) .catch(function(err){ // 出錯了 }); 複製代碼
Promise 的構造函數有兩個參數,都是 javascript 引擎提供的,不用本身實現,分別是 resolve 和 reject。
then 方法能夠接受兩個函數做爲參數,分別對應 resolve 和 reject 時的處理,其中 reject 是可選的。
promise.then(function(value) { // success }, function(error) { // failure }); 複製代碼
Promise 至少把廣大開發者從回調地獄中拯救出來,把回調變爲鏈式調用。
注意這裏只是拿 ajax 作例子,實際上 jQuery 的 ajax 已經 Promise 化,能夠直接相似 Promise 的用法。
$.ajax({ url: "test.html", context: document.body }).done(function() { $( this ).addClass( "done" ); }); 複製代碼
這種寫法已經比回調函數的寫法要直觀多了,可是仍是有一些嵌套,不夠直觀。
Promise 和 async/await 之間其實還有一個 Generator,用的也很少,簡單說下,形式是這樣的:
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true } 複製代碼
Generator 函數要用 * 來標識,用 yield 表示暫停,經過 yield 把函數分割出好多個部分,每調用一次 next 會返回一個對象,表示當前階段的信息 (value 屬性和 done 屬性)。value 屬性是 yield 語句後面表達式的值,表示當前階段的值;done 屬性是一個布爾值,表示 Generator 函數是否執行完畢,便是否還有下一個階段。
關於 Generator 的詳細信息能夠參考 www.ruanyifeng.com/blog/2015/0…
async/await 其實 Generator 的語法糖,用 async 這種更明確的標識代替 *,用 await 代替 yield。
說了這麼多,咱們終於明白 async/await 是爲了能用同步的方式寫出異步的代碼,同時解決回調地獄。
因此在多圖片異步加載這個場景下,咱們指望的應該是多個異步操做都完成以後再告訴咱們。
async function loadImgs(imgList){ let proList = []; for(var i=0; i<imgList.length; i++ ){ let pro = new Promise((resolve, reject) => { console.log("request"+i) setTimeout(resolve, 2000); console.log("return"+i) }) proList.push(pro) } return Promise.all(proList) .then( ()=>{ console.log("finish all"); return Promise.resolve(); }) } async function entry(imgList, cb) { await loadImgs(imgList); cb(); } entry(["xxx/a.png","xxx/b.png"], function(){ console.log("開始幹活") }) 複製代碼
運行結果是:
request0
return0
request1
return1
finish all
開始幹活
複製代碼
會看到一開始就立馬打印出
request0
return0
request1
return1
複製代碼
過了兩秒以後,纔打印出 finish all
。
上面咱們都是在 node 命令行裏面運行的,在理解整個過程以後,讓咱們在瀏覽器裏面實際試試,因爲兼容性問題,咱們要藉助 webpack 轉換一下。
上代碼:
function loadImgs(imgList){ let proList = []; for(var i=0; i<imgList.length; i++ ){ let pro = new Promise((resolve, reject) => { let img = new Image(); img.onload = function(){ resolve(img) } img.src = imgList[i]; }) proList.push(pro) } return Promise.all(proList) .then( (rs)=>{ console.log("finish all"); return Promise.resolve(rs); }) } async function entry(imgList, cb) { try { let rs = await loadImgs(imgList); cb(rs); } catch(err) { console.log(err) cb([]) } } var imgUrlList = [ "http://111.231.236.41/vipstyle/cartoon/v4/release/pic/index/recomment-single-s3.png", "http://111.231.236.41/vipstyle/cartoon/v4/release/pic/index/recomment-single-s2.png" ] entry(imgUrlList, function(rs){ console.log("開始幹活") console.log(rs) }) 複製代碼
注意, await 命令後的 Promise 對象是有可能 rejected 的,因此最好放到 try...catch 塊中執行。
須要用 webpack 轉換下,能夠參考咱們 webpack.config.js:
module.exports = { entry: ['./index.js'], output: { filename: 'bundle.js' }, devtool: 'sourcemap', watch: true, module: { loaders: [{ test: /index.js/, exclude: /(node_modules)/, loader: 'babel', query: { presets: ['es2015', 'stage-3'], plugins: [ ["transform-runtime", { "polyfill":false, "regenerator":true }] ] } }] } } 複製代碼
跑完以後寫個頁面在瀏覽器運行一下,打開 console,能夠看到
返回的結果有兩個圖片對象,是咱們指望的。
再看看 network,檢查下是不是併發的:
ok,搞定。
其實到上面那一步關於 async/await 異步加載圖片的相關東西已經講完了,這裏咱們回過頭來看下生成的文件,會發現特別的大,就那麼幾行代碼生成的文件竟然有 80k。
把 webpack 具體打了哪些包打印出來看看:
其中,咱們原本的 index.js 只有 4.08k ,可是 webpack 爲了支持 async/await 打包了一個 24k 的 runtime.js 文件,除此以外爲了支持 es6 語法還打包了一大堆別的文件進去。
若是你在打包的時候使用了 babel-polyfill
最後出來的文件能夠達到可怕的 200k。
因而我想起了 TypeScript。
TypeScript 具備優秀的自編譯能力,不須要額外引入 babel,並且比 babel 作的更好。以我上面的代碼爲例,安裝 TypeScript 以後,不須要任何修改,只要把後綴名改爲 ts,直接就能夠開始編譯。
來感覺一下:
bundle-ts.js 就是用 TypeScript 編譯出來的,只有 5.5k。
看一下編譯出來的文件中 async/await 的實現,不到 40 行,乾淨利落。
TypeScript 編譯出的文件跟你使用了多少特性有關係,而 bable 可能一開始就會給你打包一堆進去,即便你如今還沒用到,並且一些實現上 TypeScript 也要比 bable 更好。
固然,這裏並非說用 TypeScript 就必定比 bable 好,仍是要根據項目實際狀況來,但 TypeScript 絕對值得你去花時間瞭解一下。
有時候咱們不能單從表面看問題,而要從一個事情的演化來看,好比 async/await 咋一看異步,就認爲加了就異步,這樣很容易走入誤區。有空多想一想背後的故事,會有更深入的認識,你我共勉。
記錄一些所思所想,寫寫科技與人文,寫寫生活狀態,寫寫讀書心得,主要是扯淡和感悟。 歡迎關注,交流。
微信公衆號:程序員的詩和遠方
公衆號ID : MonkeyCoder-Life