天天看點

【JS】817- 15 道 Promise 題,你都懂嗎?

【JS】817- 15 道 Promise 題,你都懂嗎?

作者:心有猛虎qy

Promise是ES6中的特性,現在很多前端架構像AngularJS,Vue等在HTTP請求之後都是傳回的Promise處理,是以Promise是必須要掌握的一個知識點。

本文将為大家分享15道由易到難的ES6 Promise題, 幫助你快速了解Promise。

基礎題

01

const promise = new Promise((resolve, reject) => {
    console.log(1)
    resolve()
    console.log(2)
})
promise.then(() => {
    console.log(3)
})
console.log(4)      

解析:

Promise 構造函數是同步執行的,promise.then 中的函數是異步執行的。

運作結果:

// => 1
// => 2
// => 4
// => 3      

02

const first = () => (new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
        console.log(7);
        setTimeout(() => {
            console.log(5);
            resolve(6);
        }, 0)
        resolve(1);
    });
    resolve(2);
    p.then((arg) => {
        console.log(arg);
    });

}));

first().then((arg) => {
    console.log(arg);
});
console.log(4);      

解析:

這道題主要了解js執行機制。

第一輪事件循環,先執行宏任務,主script,new Promise立即執行,輸出 3,執行p這個new Promise操作,輸出 7,發現setTimeout,将回調函數放入下一輪任務隊列(Event Quene),p的then,暫且命名為then1,放入微任務隊列,且first也有then,命名為then2,放入微任務隊列。執行console.log(4),輸出 4,宏任務執行結束。

再執行微任務,執行then1,輸出 1,執行then2,輸出 3.

第一輪事件循環結束,開始執行第二輪。第二輪事件循環先執行宏任務裡面的,也就是setTimeout的回調,輸出 5.resolve(6)不會生效,因為p的Promise狀态一旦改變就不會再變化了。

運作結果:

// => 3
// => 7
// => 4
// => 1
// => 2
// => 5      

03

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})

console.log('promise1', promise1)
console.log('promise2', promise2)

setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)      

運作結果:

promise1 Promise {<pending>}
promise2 Promise {<pending>}
Uncaught (in promise) Error: error!!!
    at <anonymous>
promise1 Promise {<resolved>: "success"}
promise2 Promise {<rejected>: Error: error!!!
    at <anonymous>}      

解釋:promise 有 3 種狀态:pending、fulfilled 或 rejected。狀态改變隻能是 pending->fulfilled 或者 pending->rejected,狀态一旦改變則不能再變。上面 promise2 并不是 promise1,而是傳回的一個新的 Promise 執行個體。

04

const promise = new Promise((resolve, reject) => {
  resolve('success1')
  reject('error')
  resolve('success2')
})

promise
  .then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })      

解析:

構造函數中的 resolve 或 reject 隻有第一次執行有效,多次調用沒有任何作用,呼應代碼二結論:promise 狀态一旦改變則不能再變。

運作結果:

then: success1      

05

Promise.resolve(1)
  .then((res) => {
    console.log(res)
    return 2
  })
  .catch((err) => {
    return 3
  })
  .then((res) => {
    console.log(res)
  })      

解析:

promise 可以鍊式調用。提起鍊式調用我們通常會想到通過 return this 實作,不過 Promise 并不是這樣實作的。promise 每次調用 .then 或者 .catch 都會傳回一個新的 promise,進而實作了鍊式調用。

運作結果:

1
2      

06

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('once')
    resolve('success')
  }, 1000)
})

const start = Date.now()
promise.then((res) => {
  console.log(res, Date.now() - start)
})
promise.then((res) => {
  console.log(res, Date.now() - start)
})      

解析:

promise 的 .then 或者 .catch 可以被調用多次,但這裡 Promise 構造函數隻執行一次。或者說 promise 内部狀态一經改變,并且有了一個值,那麼後續每次調用 .then 或者 .catch 都會直接拿到該值。

運作結果:

once
success 1005
success 1007      

07

Promise.resolve()
  .then(() => {
    return new Error('error!!!')
  })
  .then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })      

解析:

.then 或者 .catch 中 return 一個 error 對象并不會抛出錯誤,是以不會被後續的 .catch 捕獲,需要改成其中一種:

return Promise.reject(new Error('error!!!'))

throw new Error('error!!!')

因為傳回任意一個非 promise 的值都會被包裹成 promise 對象,即 return new Error('error!!!') 等價于 return Promise.resolve(new Error('error!!!'))。

運作結果:

then:  Error: error!!!
    at <anonymous>      

08

const promise = Promise.resolve()
  .then(() => {
    return promise
  })
promise.catch(console.error)      

解析:.then 或 .catch 傳回的值不能是 promise 本身,否則會造成死循環。類似于:

process.nextTick(function tick () {
  console.log('tick')
  process.nextTick(tick)
})      

運作結果:

TypeError: Chaining cycle detected for promise #<Promise>      

09

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)      

解析:

.then 或者 .catch 的參數期望是函數,傳入非函數則會發生值穿透。

運作結果:

1      

10

Promise.resolve()
  .then(function success (res) {
    throw new Error('error')
  }, function fail1 (e) {
    console.error('fail1: ', e)
  })
  .catch(function fail2 (e) {
    console.error('fail2: ', e)
  })      

解析:

.then 可以接收兩個參數,第一個是處理成功的函數,第二個是處理錯誤的函數。

.catch 是 .then 第二個參數的簡便寫法,但是它們用法上有一點需要注意:.then 的第二個處理錯誤的函數捕獲不了第一個處理成功的函數抛出的錯誤,而後續的 .catch 可以捕獲之前的錯誤。

當然以下代碼也可以:

Promise.resolve()
  .then(function success1 (res) {
    throw new Error('error')
  }, function fail1 (e) {
    console.error('fail1: ', e)
  })
  .then(function success2 (res) {
  }, function fail2 (e) {
    console.error('fail2: ', e)
  })      

運作結果:

fail2:  Error: error
    at success (<anonymous>)      

11

process.nextTick(() => {
  console.log('nextTick')
})
Promise.resolve()
  .then(() => {
    console.log('then')
  })
setImmediate(() => {
  console.log('setImmediate')
})
console.log('end')      

解析:

process.nextTick 和 promise.then 都屬于 microtask,而 setImmediate 屬于 macrotask,在事件循環的 check 階段執行。事件循環的每個階段(macrotask)之間都會執行 microtask,事件循環的開始會先執行一次 microtask。

運作結果:

end
nextTick
then
setImmediate      

程式設計題

上面題目太基礎,沒有挑戰性?那就來點有難度的!

12

紅燈3秒亮一次,綠燈1秒亮一次,黃燈2秒亮一次;如何使用Promise讓三個燈不斷交替重複亮燈?(海康威視筆試題)

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}      

分析:

先看題目,題目要求紅燈亮過後,綠燈才能亮,綠燈亮過後,黃燈才能亮,黃燈亮過後,紅燈才能亮……是以怎麼通過Promise實作?

換句話說,就是紅燈亮起時,承諾2s秒後亮綠燈,綠燈亮起時承諾1s後亮黃燈,黃燈亮起時,承諾3s後亮紅燈……這顯然是一個Promise鍊式調用,看到這裡你心裡或許就有思路了,我們需要将我們的每一個亮燈動作寫在then()方法中,同時傳回一個新的Promise,并将其狀态由pending設定為fulfilled,允許下一盞燈亮起。

function red() {
  console.log('red');
}

function green() {
  console.log('green');
}

function yellow() {
  console.log('yellow');
}


let myLight = (timer, cb) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      cb();
      resolve();
    }, timer);
  });
};


let myStep = () => {
  Promise.resolve().then(() => {
    return myLight(3000, red);
  }).then(() => {
    return myLight(2000, green);
  }).then(()=>{
    return myLight(1000, yellow);
  }).then(()=>{
    myStep();
  })
};
myStep();      
// output:
// => red
// => green
// => yellow
// => red
// => green
// => yellow
// => red      

13

請實作一個mergePromise函數,把傳進去的數組按順序先後執行,并且把傳回的資料先後放到數組data中。

const timeout = ms => new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, ms);
});

const ajax1 = () => timeout(2000).then(() => {
    console.log('1');
    return 1;
});

const ajax2 = () => timeout(1000).then(() => {
    console.log('2');
    return 2;
});

const ajax3 = () => timeout(2000).then(() => {
    console.log('3');
    return 3;
});

const mergePromise = ajaxArray => {
    // 在這裡實作你的代碼

};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
    console.log('done');
    console.log(data); // data 為 [1, 2, 3]
});      
// 要求分别輸出
// 1
// 2
// 3
// done
// [1, 2, 3]      

分析:

這道題主要考察用Promise控制異步流程,首先ajax1,ajax2,ajax3都是函數,隻是這些函數執行後會傳回一個Promise,按照題目要求隻要順序執行這三個函數就好了,然後把結果放到data中;

答案:

const mergePromise = ajaxArray => {
  // 在這裡實作你的代碼
  // 儲存數組中的函數執行後的結果
  var data = [];

  // Promise.resolve方法調用時不帶參數,直接傳回一個resolved狀态的 Promise 對象。
  var sequence = Promise.resolve();

  ajaxArray.forEach(item => {
    // 第一次的 then 方法用來執行數組中的每個函數,
    // 第二次的 then 方法接受數組中的函數執行後傳回的結果,
    // 并把結果添加到 data 中,然後把 data 傳回。
    sequence = sequence.then(item).then(res => {
      data.push(res);
      return data;
    });
  });

// 周遊結束後,傳回一個 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(儲存數組中的函數執行後的結果) 也會作為參數,傳入下次調用的 then 方法中。
  return sequence;
};      

14

現有8個圖檔資源的url,已經存儲在數組urls中,且已有一個函數function loading,輸入一個url連結,傳回一個Promise,該Promise在圖檔下載下傳完成的時候resolve,下載下傳失敗則reject。

要求:任何時刻同時下載下傳的連結數量不可以超過3個。

請寫一段代碼實作這個需求,要求盡可能快速地将所有圖檔下載下傳完成。

var urls = ['https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg', 'https://www.kkkk1000.com/images/getImgData/gray.gif', 'https://www.kkkk1000.com/images/getImgData/Particle.gif', 'https://www.kkkk1000.com/images/getImgData/arithmetic.png', 'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif', 'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg', 'https://www.kkkk1000.com/images/getImgData/arithmetic.gif', 'https://www.kkkk1000.com/images/wxQrCode2.png'];

function loadImg(url) {
    return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = () => {
            console.log('一張圖檔加載完成');
            resolve();
        }
        img.onerror = reject;
        img.src = url;
    })
};      

解析

題目的意思是需要先并發請求3張圖檔,當一張圖檔加載完成後,又會繼續發起一張圖檔的請求,讓并發數保持在3個,直到需要加載的圖檔都全部發起請求。

用Promise來實作就是,先并發請求3個圖檔資源,這樣可以得到3個Promise,組成一個數組promises,然後不斷調用Promise.race來傳回最快改變狀态的Promise,然後從數組promises中删掉這個Promise對象,再加入一個新的Promise,直到全部的url被取完,最後再使用Promise.all來處理一遍數組promises中沒有改變狀态的Promise。

function limitLoad(urls, handler, limit) {
  // 對數組做一個拷貝
    const sequence = […urls];

  let promises = [];      
//并發請求到最大數
  promises = sequence.splice(0, limit).map((url, index) => {
    // 這裡傳回的 index 是任務在 promises 的腳标,用于在 Promise.race 之後找到完成的任務腳标
    return handler(url).then(() => {
      return index;
    });
  });

  // 利用數組的 reduce 方法來以隊列的形式執行
  return sequence.reduce((last, url, currentIndex) => {
    return last.then(() => {
      // 傳回最快改變狀态的 Promise
      return Promise.race(promises)
    }).catch(err => {
      // 這裡的 catch 不僅用來捕獲前面 then 方法抛出的錯誤
      // 更重要的是防止中斷整個鍊式調用
      console.error(err)
    }).then((res) => {
      // 用新的 Promise 替換掉最快改變狀态的 Promise
      promises[res] = handler(sequence[currentIndex]).then(() => {
        return res
      });
    })
  }, Promise.resolve()).then(() => {
    return Promise.all(promises)
  })

}

limitLoad(urls, loadImg, 3);

/*
因為 limitLoad 函數也傳回一個 Promise,是以當 所有圖檔加載完成後,可以繼續鍊式調用

limitLoad(urls, loadImg, 3).then(() => {
    console.log('所有圖檔加載完成');
}).catch(err => {
    console.error(err);
})      

*/

15

封裝一個異步加載圖檔的方法

解析:

function loadImageAsync(url) {
    return new Promise(function(resolve,reject) {
        var image = new Image();
        image.onload = function() {
            resolve(image) 
        };
        image.onerror = function() {
            reject(new Error('Could not load image at' + url));
        };
        image.src = url;
     });
}      

總結

這一部分題目我從網上收集來的一些練習題,幾道程式設計題我自己第一遍做也覺得沒那麼好做,但是做完感覺獲益匪淺,加深了我對Promise特性和如何更好使用Promise的了解。

是以Promise真正的學習,還是要結合具體場景實際開發運用。

Promise功能看似簡單,但認真整理卻發現Promise内容确實挺多的,自己花了一整天時間去查閱、學習、了解、跑代碼、然後整理Promise的筆記。

【JS】817- 15 道 Promise 題,你都懂嗎?

繼續閱讀