從 Event Loop 到 Promise (常見問題分析)

寫在最前面

  • promise 做爲前端經常使用的工具,今天從底瞭解一下 promise 的使用和基礎知識。
    • 其中有出入或者錯誤的地方但願朋友們指出。

導航

  • 1、同步和異步
  • 2、單線程和多線程
  • 3、evet loop
  • 4、實戰,promise 題目分析

Promise

  • 什麼是 promise?html

    • 咱們先明確:Promise 對象用於表示一個異步操做的最終完成 (或失敗), 及其結果值.
  • 什麼是 async 和 await前端

    • async/await 使得異步代碼看起來像·同步代碼·,一句話總結,async 函數就是 Generator 函數的語法糖,返回了一個 promise.resolve() 的結果。阮一峯老師的 async 教程
  • 上面提到了一個異步的問題,咱們前端er都知道 JavaScript - 是單線程的,若是存在多個任務的時候,就會有任務隊列進行排隊,而後一一執行任務。git

不着急介紹 promise 的詳情,首先咱們從最開始的同步和異步講起:github

1、同步和異步

1.1 同步

簡單的理解ajax

  • 若是函數在返回結果的時候,調用者可以拿到預期的結果(即便會等待可是依然能拿到預期的結果),那麼這個函數就是同步的。
console.log('synchronous'); //咱們能當即獲得 synchronous
複製代碼

1.2 異步

簡單的理解chrome

  • 若是函數返回的時候,不能當即獲得預期的結果,而是經過必定的手段獲得的(好比回調函數 callback()), 這就是異步,好比經常使用的 promise 和 ajax 操做等。

來看一個圖數組

image

2、單線程和多線程

  • 簡單的瞭解了同步和異步的概念後,咱們看看什麼是單線程和多線程?

2.1 瀏覽器常駐線程

一個瀏覽器一般由如下幾個常駐的線程:promise

  1. 渲染引擎線程,負責頁面的渲染
  2. js引擎線程,負責js的解析和執行
  3. 定時觸發器線程,處理setInterval和setTimeout
  4. 事件觸發線程,處理DOM事件
  5. 異步http請求線程,處理http請求
  • 要注意其中渲染引擎js引擎線程是不能同時進行的,渲染線程在執行任務的時候,js引擎線程會被掛起。由於如果在渲染頁面的時候,js處理了DOM,瀏覽器就不知道該聽誰的了

2.2 JS 引擎

  1. 渲染引擎:Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trdent引擎,FireFox用的是Gecko引擎。不一樣的引擎對同一個樣式的實現不一致,就致使瀏覽器的兼容性問題。
  2. JS引擎:js引擎能夠說是js虛擬機,負責解析js代碼的解析和執行。一般有如下步驟:
    • 詞法解析:將源代碼分解位有意義的分詞
    • 語法分析:用語法分析器將分詞解析成語法樹
    • 代碼生成:生成機器能運行的代碼
    • 代碼執行
  • 固然不一樣瀏覽器的JS引擎也是不一樣的:Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

總結一點:JavaScript是單線程的,可是瀏覽器不是單線程的。一些I/O操做,定時器的計時和事件監聽是由其餘線程完成的。瀏覽器

3、消息隊列和事件循環

開局一張圖bash

image

導圖要表達的內容用文字來表述的話: 1.同步和異步任務分別進入不一樣的執行"場所" 2.同步的進入主線程,異步的進入Event Table並註冊回調函數到 Event Queue 中。 3.當主線程執行完畢之後,而後會去 Event Queue 查詢,時候若是存在的函數,放進主線程中繼續執行。 4.上述就是event loop的執行

說了這麼多文字,不如直接一段代碼更直白:

console.log('script start');

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function cb() {
    console.log('promise2');
});

console.log('script end');

// script start
// promise1
// script end
// promise2

複製代碼

分析這段代碼:

首先執行,打印 script start

而後進入 promise 函數打印 promise1,執行 resolve()
在 then 執行的時候咱們把異步回調放進了 event table 中註冊相關的回調函數。
new promise 執行完畢,回調函數cb() 進入Event Queue。

執行 打印 script end;

主線程從Event Queue讀取回調函數 cb 並執行。
複製代碼

3.1 宏任務和微任務

  • 記住一點,當同一個 event queue 中有 微任務 的時候,優先執行 微任務

macro-task(宏任務):包括總體代碼script,setTimeout,setInterval micro-task(微任務):Promise,process.nextTick

image

看一個栗子

3.2 思考一下代碼執行順序

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');
    /** * script start * async1 start * async2 * promise1 * script end * async1 end * promsise2 * setTimerout */
複製代碼

注意幾個點

一、js是單線程的。
二、promise被定義後是當即執行的,可是他的resolve是異步的。
三、promise的異步優先級高於setTimeout。
四、async會返回一個promise對象,await關鍵字會讓出線程。
複製代碼
  • 分析
- 定義異步函數 async1, 異步函數 async2
1. console.log('script start'); 執行 (1)`script start`

2. setTimeout 執行,異步放入異步隊列中,注意這是一個宏任務(咱們標記爲 macro1)

3. 執行 async1(), 打印 (2)`async1 start`, 執行 async1() 中的 await async2(): 打印 (3)`async2`;
遇到 await 後面的函數進入任務隊列,這裏又註冊一個微任務(咱們標記爲 mico1);到這裏 async1() 就執行完了

4. 執行 new Promise:打印 (4)`promise1`,執行 resolve();
而後在 then 中註冊回調函數,console.log('promise2') 函數進入任務隊列;
註冊 event queue(咱們標記爲 mico2).這裏 new Promise 就執行完了。

5. 執行 console.log('script end');, 打印 (5) `script end`;

6. 上面👆五步把主線程都執行完畢了,而後去event queue 查找有沒有註冊的函數;
咱們發現了(macro 1, mico1, mico2),按照優先執行微任務的原則,咱們按照這樣的順序執行 mico1 > mico2 > macro1。
 打印:(6) `async1 end` (7) `promise2` (8) `setTimeout`

複製代碼

[!warning]可能你會在不一樣瀏覽器發現不一樣結果,這是由於不一樣瀏覽器和版本的不一樣遵循的 promise 規則不一樣。這裏是按照較新版本的 chrome(68+) 執行的結果,具體參考(www.w3.org/2001/tag/do…

4、回到 promise

  • 這裏直接看幾道常見的題目來認識 promise 具體是什麼?

4.1 理解常見的狀態變化

  • 理解 resolve
const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
})
promise.then(() => {
    console.log(3);
})
console.log(4);
複製代碼

分析

首先Promise新建後當即執行,因此會先輸出1,2,而Promise.then()內部的代碼在當次事件循環的結尾當即執行,因此會先輸出4,最後輸出3.

QA:1 2 4 3

  • 理解狀態變化
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函數Promise對象的狀態從「未完成」變爲「成功」(即從pending變爲resolved),在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去; reject函數將Promise對象的狀態從「未完成」變爲「失敗」(即從pending變爲rejected),在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去。 而一旦狀態改變,就不會有再變

因此代碼中的reject('error');不會有做用。 Promise只能resolve一次,剩下的調用都會被忽略。 因此第二次resolve('success');也不會有做用。

QA:then:success1

4.2 手寫 promise

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

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

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

  • 基礎版
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
    this.state = PENDING;
    this.value = null;
    this.reason = null;

    const resolve = value => {
        if (this.state === PENDING) {
            this.state = FULFILLED;
            this.value = value;
        }
    };

    const reject = reason => {
        if (this.state === PENDING) {
            this.state = REJECTED;
            this.reason = reason;
        }
    };

    try {
        executor(resolve, reject);
    } catch (reason) {
        reject(reason);
    }
}
複製代碼
  • 添加 then 手法
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
    this.state = PENDING;
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = value => {
        if (this.state === PENDING) {
            this.state === FULFILLED;
            this.value === value;
            this.onFulfilledCallbacks.forEach(fuc =>{
                fuc();
            });
        }
    };

    const reject = reason => {
        if (this.state === PENDING) {
            this.state = REJECTED;
            this.reason === reason;
            this.onRejectedCallbacks.forEach(fuc =>{
                fuc();
            })
        }
    };

    try {
        executor(resolve, reject);
    } catch (reason) {
        reject(reason);
    }
}

/* - then方法接受兩個參數onFulfilled、onRejected,它們分別在狀態由PENDING改變爲FULFILLED、REJECTED後調用 - 一個promise可綁定多個then方法 - then方法能夠同步調用也能夠異步調用 - 同步調用:狀態已經改變,直接調用onFulfilled方法 - 異步調用:狀態仍是PENDING,將onFulfilled、onRejected分別加入兩個函數- - 數組onFulfilledCallbacks、onRejectedCallbacks, - 當異步調用resolve和reject時,將兩個數組中綁定的事件循環執行。 */

MyPromise.prototype.then = function(onFulfilled,onRejected){
    switch(this.state){
        case FULFILLED:
            onFulfilled(this.value);
            break;
        case REJECTED:
            onRejected(this.reason);
            break;
        case PENDING:
            this.onFulfilledCallbacks.push(()=>{
                onFulfilled(this.value);
            });
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            })
            break;
            
    }
}

// 因爲catch方法是then(null, onRejected)的語法糖,因此這裏也很好實現
MyPromise.prototype.catch = function(onRejected){
    return this.then(null, onRejected);
}
複製代碼

4.3 promsie.all

  • 所有成功,返回一個成功res的數組,若是有失敗就返回那個失敗的err
Promise.all = function(promises) {
    return new Promise(function(resolve, reject) {
      var resolvedCounter = 0
      var promiseNum = promises.length
      var resolvedValues = new Array(promiseNum)
      for (var i = 0; i < promiseNum; i++) {
        (function(i) {
          Promise.resolve(promises[i]).then(function(value) {
            resolvedCounter++
            resolvedValues[i] = value
            if (resolvedCounter == promiseNum) {
              return resolve(resolvedValues)
            }
          }, function(reason) {
            return reject(reason)
          })
        })(i)
      }
    })
複製代碼
  • 咱們想實現一個不論是成功仍是失敗都返回怎麼處理?
    • 優先在 all 處理前去處理一下咱們數組的值,錯誤的catch 錯誤,成功的捕獲成功就是
Promise.all([a,b,c].map(p => p.catch(e => {...})))
  .then(res => {...})
  .catch(err => {...});
複製代碼

4.4 圖片異步加載封裝

  • 實現一個圖片異步的封裝,成功就返回圖片,失敗返回錯誤提示,有一個屬性值 src
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;
     });
}   
複製代碼

衍生:這裏須要先併發請求3張圖片,當一張圖片加載完成後,又會繼續發起一張圖片的請求,讓併發數保持在3個,直到須要加載的圖片都所有發起請求。咱們應該怎麼作?

4.5 待續,promise 有不少有趣的實現,後續會繼續補充

參考

相關文章
相關標籤/搜索