面向面試題和實際使用談promise

来源:https://www.cnblogs.com/lunlunshiwo/archive/2018/04/16/8852984.html
-Advertisement-
Play Games

“金三銀四,金九銀十”,都是要收穫的季節。面對各種面試題,各種概念、原理都要去記,挺枯燥的。本文是面向面試題和實際使用談一下Promise。 Promise是什麼? Promise是JS非同步編程中的重要概念,非同步抽象處理對象,是目前比較流行Javascript非同步編程解決方案之一。這句話說的很明白了 ...


  “金三銀四,金九銀十”,都是要收穫的季節。面對各種面試題,各種概念、原理都要去記,挺枯燥的。本文是面向面試題和實際使用談一下Promise。

Promise是什麼?

  Promise是JS非同步編程中的重要概念,非同步抽象處理對象,是目前比較流行Javascript非同步編程解決方案之一。這句話說的很明白了,Promise是一種用於解決非同步問題的思路、方案或者對象方式。在js中,經常使用非同步的地方是Ajax交互。比如在es5時代,jQueryajax的使用success來完成非同步的:

$.ajax({
   url:'/xxx',
   success:()=>{},
   error: ()=>{}
})

  這種方法可以清楚的讓讀代碼的人明白那一部分是Ajax請求成功的回調函數和失敗的回調函數。但是問題來了,當一次請求需要連續請求多個介面時,這段代碼仿佛進入了一團亂麻中:

// 第一次 
$.ajax({
     url:'/xxx',
     success:()=>{
         // 第二次
         $.ajax({
             url:'/xxx',
             success:()=>{
               // 第三次
               $.ajax({
                  url:'/xxx',
                  success:()=>{
                   // 可能還會有
                  },
                  error: ()=>{}
                })
             },
             error: ()=>{}
        })
     },
     error: ()=>{}
}) 

  也許因為success和error這兩個函數的存在,理解這段代碼會很簡單,但是當我們更改需求的時候,這將成為一個棘手的問題。這就是回調地獄。

  當然,這是es5時代。當js這門語言發展到es6時代時,Promise的出現給非同步帶來了變革。Promise提供一個then,來為非同步提供回調函數:

$.ajax({
    url:'/xxx',
}).then( ()=>{
   // 成功的回調
}, ()=>{
  // 失敗的回調 
})

  而其先進之處則是,可以在then方法中繼續寫Promise對象並返回,然後繼續調用then來進行回調操作。

Promise的用法

  說完了Promise是什麼,下麵讓我們研究一下Promise怎麼使用。首先,Promise是一個對象,因此,我們使用new的方式新建一個。然後給它傳一個函數作為參數,這個函數呢也有兩個參數,一個叫resolve(決定),一個叫reject(拒絕),這兩個參數也是函數。緊接著,我們使用then來調用這個Promise:

const fn = new Promise(function (resolve, reject) {
  setTimeout(()=>{
    let num = Math.ceil(Math.random() * 10) // 假設num為7
    if (num > 5) {
      resolve(num) //返回7
    } else {
      reject(num)
    }
  },2000)
})
fn.then((res)=>{
  console.log(res) // 7
},(err)=>{
  console.log(err)
})

  這就是最簡單的Promise的使用。假設2秒鐘之後生成隨機數為7,因此resolve回調函數運行,then走第一個函數,console.log(7)。假設2秒鐘之後生成隨機數為3,因此reject回調函數運行,then走第二個函數,console.log(3)。

  那你可能說了,Promise要是就這點能耐也沒什麼大不了的啊?我們上面說了Promise的先進之處在於可以在then方法中繼續寫Promise對象並返回,然後繼續調用then來進行回調操作:

fn = new Promise(function (resolve, reject) {
  let num = Math.ceil(Math.random() * 10)
  if (num > 5) {
    resolve(num)
  } else {
    reject(num)
  }
})
// 第一次回調
fn.then((res)=>{
  console.log(`res==>${res}`)
  return new Promise((resolve,reject)=>{
    if(2*res>15){
      resolve(2*res)
    }else{
      reject(2*res)
    }
  })
},(err)=>{
  console.log(`err==>${err}`)
}).then((res)=>{ // 第二次回調
  console.log(res)
},(err)=>{
  console.log(`err==>${err}`)
})

  這就可以代替了上面類似es5時代的jQurey的success的嵌套式的回調地獄的產生,讓代碼清爽了許多。這裡的resolve就相當於以前的success。

Promise的原理

  在Promise的內部,有一個狀態管理器的存在,有三種狀態:pending、fulfilled、rejected。

    (1) promise 對象初始化狀態為 pending。

    (2) 當調用resolve(成功),會由pending => fulfilled。

    (3) 當調用reject(失敗),會由pending => rejected。

  因此,看上面的的代碼中的resolve(num)其實是將promise的狀態由pending改為fulfilled,然後向then的成功回掉函數傳值,reject反之。但是需要記住的是註意promsie狀態 只能由 pending => fulfilled/rejected, 一旦修改就不能再變(記住,一定要記住,下麵會考到)。

  當狀態為fulfilled(rejected反之)時,then的成功回調函數會被調用,並接受上面傳來的num,進而進行操作。promise.then方法每次調用,都返回一個新的promise對象 所以可以鏈式寫法(無論resolve還是reject都是這樣)。

Promise的幾種方法

then

  then方法用於註冊當狀態變為fulfilled或者reject時的回調函數:

// onFulfilled 是用來接收promise成功的值
// onRejected 是用來接收promise失敗的原因
promise.then(onFulfilled, onRejected);

  需要註意的地方是then方法是非同步執行的。

// resolve(成功) onFulfilled會被調用
const promise = new Promise((resolve, reject) => {
   resolve('fulfilled'); // 狀態由 pending => fulfilled
});
promise.then(result => { // onFulfilled
    console.log(result); // 'fulfilled' 
}, reason => { // onRejected 不會被調用
})

// reject(失敗) onRejected會被調用
const promise = new Promise((resolve, reject) => {
   reject('rejected'); // 狀態由 pending => rejected
});
promise.then(result => { // onFulfilled 不會被調用
}, reason => { // onRejected 
    console.log(rejected); // 'rejected'
})

catch

  catch在鏈式寫法中可以捕獲前面then中發送的異常。

fn = new Promise(function (resolve, reject) {
  let num = Math.ceil(Math.random() * 10)
  if (num > 5) {
    resolve(num)
  } else {
    reject(num)
  }
})
fn..then((res)=>{
  console.log(res)
}).catch((err)=>{
  console.log(`err==>${err}`)
})

  其實,catch相當於then(null,onRejected),前者只是後者的語法糖而已。

resolve、reject

  Promise.resolve 返回一個fulfilled狀態的promise對象,Promise.reject 返回一個rejected狀態的promise對象。

Promise.resolve('hello').then(function(value){
    console.log(value);
});

Promise.resolve('hello');
// 相當於
const promise = new Promise(resolve => {
   resolve('hello');
});

// reject反之

all

  但從字面意思上理解,可能為一個狀態全部怎麼樣的意思,讓我看一下其用法,就可以看明白這個靜態方法:

var   p1 = Promise.resolve(1),
      p2 = Promise.reject(2),
      p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((res)=>{
    //then方法不會被執行
    console.log(results);
}).catch((err)=>{
    //catch方法將會被執行,輸出結果為:2
    console.log(err);
});

  大概就是作為參數的幾個promise對象一旦有一個的狀態為rejected,則all的返回值就是rejected。

  當這幾個作為參數的函數的返回狀態為fulfilled時,至於輸出的時間就要看誰跑的慢了:

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s後輸出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s後輸出
    resolve(10)
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s後輸出
    resolve(5)
  },5000)
})
Promise.all([p1, p10, p5]).then((res)=>{
    console.log(res); // 最後輸出
})

  這段代碼運行時,根據看誰跑的慢的原則,則會在10s之後輸出[1,10,5]。over,all收工。

race

  promise.race()方法也可以處理一個promise實例數組但它和promise.all()不同,從字面意思上理解就是競速,那麼理解起來上就簡單多了,也就是說在數組中的元素實例那個率先改變狀態,就向下傳遞誰的狀態和非同步結果。但是,其餘的還是會繼續進行的。

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s後輸出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s後輸出
    resolve(10) //不傳遞
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s後輸出
    resolve(5) //不傳遞
  },5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
    console.log(res); // 最後輸出
})

  因此,在這段代碼的結尾我們的結果為

1s
1
5s
10s

  我們可以根據race這個屬性做超時的操作:

//請求某個圖片資源
let requestImg = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
    });
//延時函數,用於給請求計時
let timeOut = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('圖片請求超時');
        }, 5000);
    });

Promise.race([requestImg, timeout]).then((res)=>{
    console.log(res);
}).catch((err)=>{
    console.log(err);
});

Promise相關的面試題

1.

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

  輸出結果為:1,2,4,3。

  解題思路:then方法是非同步執行的。

2.

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

  輸出結果:success

  解題思路:Promise狀態一旦改變,無法在發生變更。

3.

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

  輸出結果:1

  解題思路:Promise的then方法的參數期望是函數,傳入非函數則會發生值穿透。

4.

setTimeout(()=>{
  console.log('setTimeout')
})
let p1 = new Promise((resolve)=>{
  console.log('Promise1')
  resolve('Promise2')
})
p1.then((res)=>{
  console.log(res)
})
console.log(1)

  輸出結果:

    Promise1
    1
    Promise2
    setTimeout

  解題思路:這個牽扯到js的執行隊列問題,整個script代碼,放在了macrotask queue中,執行到setTimeout時會新建一個macrotask queue。但是,promise.then放到了另一個任務隊列microtask queue中。script的執行引擎會取1個macrotask queue中的task,執行之。然後把所有microtask queue順序執行完,再取setTimeout所在的macrotask queue按順序開始執行。(具體參考https://www.zhihu.com/question/36972010

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

  輸出結果:1  2

  解題思路:Promise首先resolve(1),接著就會執行then函數,因此會輸出1,然後在函數中返回2。因為是resolve函數,因此後面的catch函數不會執行,而是直接執行第二個then函數,因此會輸出2。

6.

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('開始');
resolve('success');
}, 5000);
});
 
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
 
promise.then((res) => {
console.log(res, Date.now() - start);
});

  輸出結果:

    開始

    success 5002

    success 5002

  解題思路:promise 的.then或者.catch可以被調用多次,但這裡 Promise 構造函數只執行一次。或者說 promise 內部狀態一經改變,並且有了一個值,那麼後續每次調用.then 或者.catch都會直接拿到該值。

7.

let p1 = new Promise((resolve,reject)=>{
  let num = 6
  if(num<5){
    console.log('resolve1')
    resolve(num)
  }else{
    console.log('reject1')
    reject(num)
  }
})
p1.then((res)=>{
  console.log('resolve2')
  console.log(res)
},(rej)=>{
  console.log('reject2')
  let p2 = new Promise((resolve,reject)=>{
    if(rej*2>10){
      console.log('resolve3')
      resolve(rej*2)
    }else{
      console.log('reject3')
      reject(rej*2)
    }
  })
  return p2 }).then((res)
=>{ console.log('resolve4') console.log(res) },(rej)=>{ console.log('reject4') console.log(rej) })

  輸出結果:

    reject1
    reject2
    resolve3
    resolve4
    12

  解題思路:我們上面說了Promise的先進之處在於可以在then方法中繼續寫Promise對象並返回。

8.重頭戲!!!!實現一個簡單的Promise
function Promise(fn){
  var status = 'pending'
  function successNotify(){
      status = 'fulfilled'//狀態變為fulfilled
      toDoThen.apply(undefined, arguments)//執行回調
  }
  function failNotify(){
      status = 'rejected'//狀態變為rejected
      toDoThen.apply(undefined, arguments)//執行回調
  }
  function toDoThen(){
      setTimeout(()=>{ // 保證回調是非同步執行的
          if(status === 'fulfilled'){
              for(let i =0; i< successArray.length;i ++)    {
                  successArray[i].apply(undefined, arguments)//執行then裡面的回掉函數
              }
          }else if(status === 'rejected'){
              for(let i =0; i< failArray.length;i ++)    {
                  failArray[i].apply(undefined, arguments)//執行then裡面的回掉函數
              }
          }
      })
  }
  var successArray = []
  var failArray = []
  fn.call(undefined, successNotify, failNotify)
  return {
      then: function(successFn, failFn){
          successArray.push(successFn)
          failArray.push(failFn)
          return undefined // 此處應該返回一個Promise
      }
  }
}

  解題思路:Promise中的resolve和reject用於改變Promise的狀態和傳參,then中的參數必須是作為回調執行的函數。因此,當Promise改變狀態之後會調用回調函數,根據狀態的不同選擇需要執行的回調函數。

總結

  首先,Promise是一個對象,如同其字面意思一樣,代表了未來某時間才會知道結果的時間,不受外界因素的印象。Promise一旦觸發,其狀態只能變為fulfilled或者rejected,並且已經改變不可逆轉。Promise的構造函數接受一個函數作為參數,該參數函數的兩個參數分別為resolve和reject,其作用分別是將Promise的狀態由pending轉化為fulfilled或者rejected,並且將成功或者失敗的返回值傳遞出去。then有兩個函數作為Promise狀態改變時的回調函數,當Promise狀態改變時接受傳遞來的參數並調用相應的函數。then中的回調的過程為非同步操作。catch方法是對.then(null,rejectFn)的封裝(語法糖),用於指定發生錯誤時的回掉函數。一般來說,建議不要再then中定義rejected狀態的回調函數,應該使用catch方法代替。all和race都是競速函數,all結束的時間取決於最慢的那個,其作為參數的Promise函數一旦有一個狀態為rejected,則總的Promise的狀態就為rejected;而race結束的時間取決於最快的那個,一旦最快的那個Promise狀態發生改變,那個其總的Promise的狀態就變成相應的狀態,其餘的參數Promise還是會繼續進行的。

  當然在es7時代,也出現了await/async的非同步方案,這會是我們以後談論的。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這是我2015年入貓酷寫的一款內部工具,目的是為了開發人員可以查詢資料庫數據以調查線上bug,數據不僅限業務數據及日誌數據,其目的是為了避免開發人員直接鏈接生產環境資料庫,以免誤操作影響生產。 當時是用 Asp.net Mvc 寫的, 最近得空 用nodejs 重新寫了一遍,開源出來, 導出到exc ...
  • 先看段代碼: 思考一下,能給出準確的輸出順序嗎? 下麵我們一個一個的來瞭解 Event Loop 相關的知識點,最後再一步一步分析出本段代碼最後的輸出順序。 JavaScript是單線程 首先我們先瞭解下進程和線程的概念和關係: 進程: 運行的程式就是一個進程,比如你正在運行的瀏覽器,它會有一個進程 ...
  • 一、Date的構造函數 有四種形式的Date構造函數: 二、返回日期對應的毫秒數 1.Date.parse() Date.parse()接收一個日期字元串,返回該日期對應的毫秒數。 2.Date.UTC() Date.UTC()的參數參數分別為年份,基於0的月份(0-11),月中的哪一天(1-31) ...
  • 原始js中的排序不能滿足: arr.sort(sortNumber);arr.sort(function (a, b) { return b.name < a.name;}); 商城列表-積分由高到低由低到高排列: html: 參考:http://www.jb51.net/article/67458 ...
  • js在處理複雜數據的時候,可能會涉及到引用類型的對象或者數組的copy問題,下麵是兩種複製對象或數組的方法: 一、利用jquery自帶的方法,調用簡單方便 ...
  • 今天在做懶載入的時候遇到的問題,在網上搜索找到的答案不是很清晰,就來寫一下,方便以後使用。 直接上圖吧 官方連接:https://cn.vuejs.org/v2/guide/reactivity.html ...
  • 這個模塊還漏了一個稍微複雜點的API,就是app.render,首先看官網的定義: app.render(view, [locals], callback) view為對應的文件名,locals為一個配置對象,callback為解析完成的回調函數。 涉及到的全局屬性有 view:預設為一個內置模塊, ...
  • 對於前端初學者來說,css浮動部分的知識是一塊比較難以理解的部分,下麵我將把我學習過程中的心得分享給大家。 導讀: 1.css塊級元素講解 2.css中浮動是如何產生的 3.出現浮動後,如何清除浮動(本文將涉及到多種清除浮動的方法) 博客正文: 1.css塊級元素講解 常見的塊級元素主要有以下幾種: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...