JavaScript?Promise執(zhí)行流程深刻理解
手撕Promise
手寫一個Promise已經(jīng)是一個常見的手寫功能了,雖然實際工作上可能并不會用到。但是在面試時還是會經(jīng)常被提起的。
看完收獲
- 實現(xiàn)一個手寫promise
- 對promise執(zhí)行流程有著更深刻的理解
- 從底層理解proimse,應對各種面試題
Promise分析
作用
通過鏈式調(diào)用的方式,解決回調(diào)地獄的問題。
特點
是ES6中新增的引用類型,通過new關(guān)鍵詞來創(chuàng)建實例。
存在3個狀態(tài):
- pending
- fulfilled
- rejected
且狀態(tài)變成fulfilled或者rejected后不可再次變更,具有不可逆性。
new Promise創(chuàng)建實例的時候必須要傳入一個函數(shù),且這個函數(shù)會有2個參數(shù)resolve
和reject
總體實現(xiàn)
首先我們可以按照Promise的特點來實現(xiàn)一個簡單的總體結(jié)構(gòu)。
- 他是一個構(gòu)造函數(shù),每個創(chuàng)建的promise都有各自狀態(tài)和值,且狀態(tài)初始值為pending,值為undefined。
- 創(chuàng)建實例的時候需要傳入一個函數(shù),而這個函數(shù)可以接受2個函數(shù)作為參數(shù),這2個函數(shù)都有一個參數(shù),且都可以更改實例的狀態(tài)和值。
- 根據(jù)Promise的結(jié)構(gòu),我們可以發(fā)現(xiàn)then()、catch()方法在它的原型上。
function MyPromise(fn) { this.PromiseState = 'pending' this.PromiseResult = undefined; function resolve(data) { } function reject(error) { } } MyPromise.prototype.then = function(thenCallback) { } MyPromise.prototype.catch = function(catchCallback) { }
上述代碼很簡單,就定義了一個Promise的構(gòu)造函數(shù),有2個屬性(PromiseState、PromiseResult)和2個方法(resolve、reject)。
原型上增加了then方法和catch方法,且這2個方法都接收一個函數(shù)。
而每次通過new來創(chuàng)建Promise的函數(shù)時,傳入的函數(shù)會執(zhí)行,因此我們直接調(diào)用fn函數(shù),并傳入resolve和reject這2個函數(shù)。
resolve和reject初步實現(xiàn)
resolve的調(diào)用會更改promise的狀態(tài)和值,且狀態(tài)是不可逆的。也就是說,只能從pending變成fulfilled或者rejected。而resolve函數(shù)的參數(shù)值會變成promise實例的值,reject同理。 所以我們把resolve和reject簡單的寫完整.
function MyPromise(fn) { this.PromiseState = "pending"; this.PromiseResult = undefined; // 保存實例對象的this的值 const self = this; function resolve(data) { // 如果不使用self,這里內(nèi)部的this會指向window // 如果當前的promise實例不是pending的狀態(tài)就退出了,否則就更改當前的promise實例的狀態(tài)和值 if (self.PromiseState !== "pending") { return; } // 1.修改對象的狀態(tài)([[promiseState]]) // 2.設置對象結(jié)果值([[promiseResult]]) self.PromiseState = "fulfilled"; self.PromiseResult = data; } function reject(error) { if (self.PromiseState !== "pending") { return; } self.PromiseState = "rejected"; self.PromiseResult = error; } fn(resolve, reject); }
上面代碼中,resolve和reject就是用來更改當前實例的狀態(tài)的,如果當前狀態(tài)已經(jīng)改變了,即不為pending,那么就不再進行狀態(tài)變更和值變更了。
接下來就是驗證一下:
const p = new MyPromise((resolve, reject) => { resolve(1); reject(2); }); console.log(p);
發(fā)現(xiàn)我們執(zhí)行了resolve后再執(zhí)行reject并不會再次更改狀態(tài)。
但是還有一點,如果我們不執(zhí)行resolve或者reject,而是直接執(zhí)行throw呢?
先看看原生的Promise效果。
const p1 = new Promise((resolve, reject) => { throw 111; }); console.log(p1);
發(fā)現(xiàn)狀態(tài)會變成rejected。
那我們是不是可以在執(zhí)行fn(resolve,reject)的時候進行異常處理,將狀態(tài)變成rejected,值變成該異常信息。
// 如果throw異常,就需要捕獲到異常然后更改狀態(tài) try { // 同步調(diào)用執(zhí)行器函數(shù) fn(resolve, reject); } catch (e) { // 修改promise對象的狀態(tài)為失敗 reject(e); }
then方法的實現(xiàn)
then方法中我們知道它有2個參數(shù),且這2個參數(shù)都是函數(shù),第一個參數(shù)會在promise執(zhí)行resolve時調(diào)用,第二個參數(shù)會在promise執(zhí)行reject時調(diào)用。而.catch只不過是調(diào)用.then第二個參數(shù)的語法糖而已。
const p = new Promise((resolve, reject) => { resolve(1); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); console.log(p, 111); console.log(p1, 222);
但是我們查看原生的Promise后,可以發(fā)現(xiàn),.then的執(zhí)行會返回一個新的Promise實例。而決定調(diào)用.then的第幾個參數(shù),則是根據(jù)調(diào)用then的那個promise實例的狀態(tài)決定。
修改代碼如下:
MyPromise.prototype.then = function (thenCallback, catchCallback) { return new Promise((resolve, reject) => { // 調(diào)用回調(diào)函數(shù),要根據(jù)當前的promise實例來調(diào)用 if (this.PromiseState === "fulfilled") { const result = thenCallback(this.PromiseResult); resolve(result); } if (this.PromiseState === "rejected") { const result = catchCallback(this.PromiseResult); resolve(result); } }); };
我們的自定義函數(shù)上的原型的then返回了一個新的promise實例,而新的promise實例調(diào)用then的第幾個參數(shù)則根據(jù)調(diào)用then的promise實例的狀態(tài)決定(此處的this指向調(diào)用then的那個promise實例)。
我們需要拿到回調(diào)函數(shù)的執(zhí)行結(jié)果,然后再放入新的promise實例的resolve中更改新實例的狀態(tài)和值。
查看一下我們自己的效果:
const p = new MyPromise((resolve, reject) => { reject(1); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); console.log(p, 111); console.log(p1, 222);
這時候可能有人就會提出疑問,如果在上述的p中的回調(diào)里延時執(zhí)行resolve或者reject呢?
const p = new MyPromise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); console.log(p, 111); console.log(p1, 222);
我們可以發(fā)現(xiàn)p1的狀態(tài)變成了pending。
這是因為當執(zhí)行到setTimeout的時候,發(fā)現(xiàn)是一個異步函數(shù),然后會將這個函數(shù)掛起,繼續(xù)執(zhí)行下面的.then(),然后當時的p的狀態(tài)是pendding,因為不會執(zhí)行任何代碼。
那我們當然是要加一個pendding的判斷啦!
可是該怎么實現(xiàn)判斷的內(nèi)部代碼呢?
首先我們先來思考一下,promise是一個對象,他是按地址引用的。我們希望在定時器時間到的時候再去更改這個新的promise的狀態(tài)和值。因此我們需要訪問到上一個promise的狀態(tài)和值,并且在resolve或者reject執(zhí)行的時候訪問到,而不是定時器掛起的時候。
function MyPromise(fn) { this.PromiseState = "pending"; this.PromiseResult = undefined; this.callback = {}; // 保存實例對象的this的值 const self = this; function resolve(data) { if (self.PromiseState !== "pending") { return; } // 1.修改對象的狀態(tài)([[promiseState]]) // 2.設置對象結(jié)果值([[promiseResult]]) self.PromiseState = "fulfilled"; self.PromiseResult = data; if (self.callback.onResolved) { self.callback.onResolved(data); } } function reject(error) { if (self.PromiseState !== "pending") { return; } self.PromiseState = "rejected"; self.PromiseResult = error; if (self.callback.onReject) { self.callback.onReject(error); } } // 如果throw異常,就需要捕獲到異常然后更改狀態(tài) try { // 同步調(diào)用執(zhí)行器函數(shù) fn(resolve, reject); } catch (e) { // 修改promise對象的狀態(tài)為失敗 reject(e); } } MyPromise.prototype.then = function (thenCallback, catchCallback) { return new Promise((resolve, reject) => { // 調(diào)用回調(diào)函數(shù),要根據(jù)當前的promise實例來調(diào)用 if (this.PromiseState === "fulfilled") { const result = thenCallback(this.PromiseResult); resolve(result); } if (this.PromiseState === "rejected") { const result = catchCallback(this.PromiseResult); resolve(result); } // 判斷pending狀態(tài) if (this.PromiseState === "pending") { // 保存回調(diào)函數(shù) this.callback = { onResolved: function (data) { let result = thenCallback(data); resolve(result); }, onRejected: function (error) { let result = catchCallback(error); resolve(result); }, }; } }); };
const p = new MyPromise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); console.log(p, 111); console.log(p1, 222);
因為new Promise的實例中遇到了異步的setTimeout會將內(nèi)部的程序掛起,繼續(xù)執(zhí)行.then。
而這時promise實例的狀態(tài)還是pending,因此我們需要對pending的狀態(tài)進行處理,我們主要目的是,在setTimeout的回調(diào)函數(shù)執(zhí)行的時候,能夠執(zhí)行.then的函數(shù)參數(shù)。
這時候就需要進行一個巧妙的設計,我們需要在MyPromise構(gòu)造函數(shù)中增加一個callback的屬性,他是一個對象。
同時我們需要在pending的時候在.then里面將then的兩個參數(shù)傳給callback對象,同時傳入resolve的值或者是reject的值。
在pending的時候我們將函數(shù)傳給了實例的callback,當被掛起的setTimeout回調(diào)函數(shù)執(zhí)行的時候,這時候callback已經(jīng)有值了,所以會根據(jù)resolve或者是reject去執(zhí)行對應的函數(shù),并且將promise的實例的值傳給對應的函數(shù)。
那如我我對p多次調(diào)用.then或者.catch呢?
const p = new MyPromise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); const p2 = p.then((res) => { console.log(res, "成功2"); }); console.log(p1, p2);
我們發(fā)現(xiàn)我們的值執(zhí)行了一次,那我們看一下原生的,可以發(fā)現(xiàn)會執(zhí)行2次。
const p = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); const p2 = p.then((res) => { console.log(res, "成功2"); }); console.log(p1, p2);
原因很簡單,因為我們的callback屬性是一個對象,每次執(zhí)行.then都會覆蓋當前實例原先的callback。
怎么結(jié)局呢?很簡單,我們只要將每次的pending時設置callback改成向數(shù)組加入對象就好。
調(diào)用時我們通過遍歷去調(diào)用就好了。
function MyPromise(fn) { this.PromiseState = "pending"; this.PromiseResult = undefined; this.callbacks = []; // 保存實例對象的this的值 const self = this; function resolve(data) { if (self.PromiseState !== "pending") { return; } // 1.修改對象的狀態(tài)([[promiseState]]) // 2.設置對象結(jié)果值([[promiseResult]]) self.PromiseState = "fulfilled"; self.PromiseResult = data; //調(diào)用成功的回調(diào)函數(shù) self.callbacks.forEach((item) => { item.onResolved(data); }); } function reject(error) { if (self.PromiseState !== "pending") { return; } self.PromiseState = "rejected"; self.PromiseResult = error; //執(zhí)行失敗的回調(diào) self.callbacks.forEach((item) => { item.onRejected(error); }); } // 如果throw異常,就需要捕獲到異常然后更改狀態(tài) try { // 同步調(diào)用執(zhí)行器函數(shù) fn(resolve, reject); } catch (e) { // 修改promise對象的狀態(tài)為失敗 reject(e); } } MyPromise.prototype.then = function (thenCallback, catchCallback) { return new Promise((resolve, reject) => { // 調(diào)用回調(diào)函數(shù),要根據(jù)當前的promise實例來調(diào)用 if (this.PromiseState === "fulfilled") { const result = thenCallback(this.PromiseResult); resolve(result); } if (this.PromiseState === "rejected") { const result = catchCallback(this.PromiseResult); resolve(result); } // 判斷pending狀態(tài) if (this.PromiseState === "pending") { // 保存回調(diào)函數(shù) this.callbacks.push({ onResolved: thenCallback, onRejected: catchCallback, }); } }); };
const p = new MyPromise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); const p1 = p.then( (res) => { console.log(res, "成功"); }, (err) => { console.log(err, "失敗"); } ); const p2 = p.then((res) => { console.log(res, "成功2"); }); console.log(p1, p2);
然后我們就發(fā)現(xiàn)我們可以正常地調(diào)用了。
那如果.then返回的值是一個promise呢?
這時候我們就需要考慮一下,如果.then返回時一個promise實例或者是一個throw錯誤呢?
const p = new Promise((resolve, reject) => { resolve(1); }); const p1 = p.then( (res) => { console.log(res, "成功"); return new Promise((r, j) => { r(res); }); }, (err) => { console.log(err, "失敗"); } ); console.log(p1, "p1");
我們先看一下原生的效果,發(fā)現(xiàn)他時拿到了返回的promise的值。
那是不是就是在調(diào)用then的回調(diào)函數(shù)的時候判斷一下返回的是不是一個promise,如果是promise我們在進行特殊處理。
// 將.then的判定方式改成如下 if (this.PromiseState === "fulfilled") { try { const result = thenCallback(this.PromiseResult); if (result instanceof MyPromise) { result.then( (r) => { resolve(r); }, (j) => { reject(j); } ); } else { resolve(result); } } catch (e) { reject(e); } } if (this.PromiseState === "rejected") { try { const result = catchCallback(this.PromiseResult); if (result instanceof MyPromise) { result.then( (r) => { resolve(r); }, (j) => { reject(j); } ); } else { resolve(result); } } catch (e) { reject(e); } }
如果.then的回掉函數(shù)執(zhí)行后返回的是一個promise實例,但是我們.then默認就返回了一個實例,我們就直接取這個實例的resolve值或者是reject值,將返回值給then默認返回的promise實例。
異步方式同理,但是要注意this的指向問題。
// 判斷pending狀態(tài) if (this.PromiseState === "pending") { // 保存回調(diào)函數(shù) this.callbacks.push({ onResolved: function () { try { // 這里的self指向的是調(diào)用.then的promise實例 let result = thenCallback(self.PromiseResult); //判斷 if (result instanceof Promise) { result.then( (v) => { resolve(v); }, (r) => { reject(r); } ); } else { resolve(result); } } catch (e) { reject(e); } }, onRejected: function () { try { // 這里的self指向的是調(diào)用.then的promise實例 let result = catchCallback(self.PromiseResult); //判斷 if (result instanceof Promise) { result.then( (v) => { resolve(v); }, (r) => { reject(r); } ); } else { resolve(result); } } catch (e) { reject(e); } }, }); } });
我們發(fā)現(xiàn),代碼已經(jīng)重復了4次,我們就可以進行一個封裝。
MyPromise.prototype.then = function (thenCallback, catchCallback) { const self = this; return new Promise((resolve, reject) => { function run(type) { try { //獲取回調(diào)函數(shù)的執(zhí)行結(jié)果 let result = type(self.PromiseResult); //判斷 if (result instanceof Promise) { //如果是 Promise 類型的對象 result.then( (v) => { resolve(v); }, (j) => { reject(j); } ); } else { //結(jié)果的對象狀態(tài)為『成功』 resolve(result); } } catch (e) { reject(e); } } // 調(diào)用回調(diào)函數(shù),要根據(jù)當前的promise實例來調(diào)用 if (this.PromiseState === "fulfilled") { run(thenCallback); } if (this.PromiseState === "rejected") { run(catchCallback); } // 判斷pending狀態(tài) if (this.PromiseState === "pending") { // 保存回調(diào)函數(shù) this.callbacks.push({ onResolved: function () { run(thenCallback); }, onRejected: function () { run(catchCallback); }, }); } }); };
嗯,封裝完后的一個完整的.then。我們先看一下完整的效果。
const p1 = p.then( (res) => { console.log(res, "成功"); return new Promise((r, j) => { r(res); }); }, (err) => { console.log(err, "失敗"); } ); console.log(p1, "p1");
和原生的基本上是一模一樣了。
接下來就是對catch的實現(xiàn)了
但是我們知道,catch其實就是.then執(zhí)行第二個參數(shù)的一個語法糖。 因此,我們就可以將接收到的回調(diào)函數(shù)直接給.then的第二個參數(shù)。
MyPromise.prototype.catch = function(onRejected){ return this.then(undefined, onRejected); }
這時候我們就要注意了,我們給then的第一個參數(shù)賦值了一個undefined。但是我們并沒有對undefined進行處理。而且我們原生的Promise是可以傳入其他數(shù)據(jù)的,不一定是一個回調(diào)函數(shù)。
因此,我們需要在MyPromise的原型里的.then方法進行一個判斷:
if (typeof catchCallback !== "function") { catchCallback = (reason) => { throw reason; }; } if (typeof thenCallback !== "function") { thenCallback = (value) => value; }
如果傳入的不是一個函數(shù),我們就將它變成一個函數(shù),并且獲取的是上一個.then返回的promise實例的值。
總體上我們大致完成了,但是Promise還存在構(gòu)造函數(shù)的方法Promise.resolve(value)
和Promise.reject(error)
這個就很簡單了
//添加 resolve 方法 MyPromise.resolve = function(value){ //返回promise對象 return new MyPromise((r, j) => { if(value instanceof MyPromise){ value.then(v=>{ r(v); }, r=>{ j(r); }) }else{ //狀態(tài)設置為成功 resolve(value); } }); }
判斷resolve傳入的值是否是一個MyPromise實例,如果是,則根據(jù)這個Promise的狀態(tài)來返回,如果不是則直接調(diào)用resolve()
reject同理:
//添加 reject 方法 MyPromise.reject = function(error){ return new MyPromise((resolve, reject)=>{ reject(error); }); }
Promise.all的實現(xiàn)
Promise.all的實現(xiàn)基于我們原先封裝的MyPromise。
我們先來分析一下原先的Promise.all的方法,它接受一個promise數(shù)組,返回一個新的promise。如果數(shù)組中有一個狀態(tài)是rejected,那將直接返回一個rejected的promise實例。如果都成功了,則返回成功的promise。
MyPromise.all = function (promiseLists) { return new Promise((resolve, reject) => { let promiseResults = []; let count = 0; for (let i = 0; i < promiseLists.length; i++) { promiseLists.then( (v) => { count += 1; promiseResults[i] = v; if (count === promiseLists.length) { resolve(promiseResults); } }, (err) => { reject(err); } ); } }); };
實現(xiàn)上我們定義了一個promiseLists的形參用來接收promise的數(shù)組。因為返回的肯定是一個promise,所以我們直接返回了一個promise的實例。
我們定義了一個count用來計數(shù)成功執(zhí)行的個數(shù),promiseResults則用來接收成功的結(jié)果(要按照順序接收)。最后就是遍歷promiseLists的數(shù)組了,如果是resolve則count+1,并且將值往promiseResults里面塞。如果count的值和接受的數(shù)組長度一樣了,那就是全部的promise返回了fulfilled。如果有一個錯誤,則直接退出,并且將錯誤的promise的值傳給all返回的promise。
到此這篇關(guān)于JavaScript Promise執(zhí)行流程深刻理解的文章就介紹到這了,更多相關(guān)JavaScript Promise內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于預加載InstantClick的問題解決方法
本篇文章主要介紹了關(guān)于預加載InstantClick的問題解決方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09javascript簡單拖拽實現(xiàn)代碼(鼠標事件 mousedown mousemove mouseup)
javascript簡單拖拽,簡單拖拽實現(xiàn)2012-05-05js實現(xiàn)增加數(shù)字顯示的環(huán)形進度條效果
本文主要分享了js實現(xiàn)增加數(shù)字顯示的環(huán)形進度條效果的示例代碼。具有一定的參考價值,下面跟著小編一起來看下吧2017-02-02Base64(二進制)圖片編碼解析及在各種瀏覽器的兼容性處理
這篇文章主要介紹了Base64(二進制)圖片編碼解析及在各種瀏覽器的兼容性處理,需要的朋友可以參考下2017-02-02優(yōu)雅而高效的JavaScript?try...catch語句詳解(js異常處理)
這篇文章主要給大家介紹了關(guān)于JavaScript中try...catch語句的相關(guān)資料,也就是js異常處理方法,try...catch是JavaScript中的錯誤處理機制,它的作用是捕獲和處理可能發(fā)生的錯誤,以避免程序崩潰,需要的朋友可以參考下2024-01-01