1. 程式人生 > >非同步解決方案Promise

非同步解決方案Promise

回撥地獄

callback hell最大的問題不是因為縮排。它引起的問題比縮排要大得多。

根本

它真正的問題是剝奪了程式設計師使用return和throw等捕捉錯誤和返回值的能力。
程式的執行流程是基於一個函式在執行過程中呼叫另一個函式時候會產生函式呼叫棧,而回調函式不是執行在棧上的,因此不能使用return和throw。

舉例

function A(){
    setTimeout(req,5000);
}
function B(){
    ajax(url,response);
}

假設是這兩個函式有順序依賴的關係,我們要讓A發生後B才執行,我們要把它們連線到一起的話只能手工硬編碼:

function cb(){
    setTimout(function(){
        ajax(url,response);
    },500);
}

這種方法會使得程式碼脆弱,你要在回撥中捕獲錯誤,一旦你指定了所有可能的事件或者各種錯誤的處理函式,程式碼會變得非常複雜。
這就是信任問題

信任問題

信任問題就是你的函式的執行是交給第三方的,而不是在JS的控制範圍內。
不是你編寫的程式碼,不是你的直接控制下,而是第三方提供的工具。

控制反轉

這就是控制反轉: 把自己程式的一部分交給某個第三方。

因為對這個第三方的API (像ajax()),它可能會出現很多問題,比如


呼叫回撥過早
呼叫回撥過晚
不正確的呼叫回撥(呼叫次數太多或太少),
沒有把所需的引數成功返回給回撥函式等。
這可能會導致你回撥中的程式碼被錯誤執行了若干次而沒有提示!!!!!!!!
eg
本來你的ajax只是希望在<p>輸出一段話,
結果多次錯誤回撥導致輸出了N段話!
困境
你必須在這個回撥函式中建立大量邏輯來判斷處理這些可能的情況。。。

回到兩個挽救方法

分離回撥

function success(data){
    //
}
function failure(data){
    //
}
ajax(url,success,failure); 

一個用於成功的處理函式,一個用於錯誤的處理函式!
ES6就是這種分離回撥設計。

error-first風格,nodejs

回撥第一個引數作為錯誤物件(if exists)。
如果成功,error為 清空/置假
如果失敗,if(err)為真。


function cb(err,data){
    if(err){}
    ...
}

但這並沒有解決 重複呼叫回撥的問題。 你可能同時得到成功或失敗的結果! 或者都沒有!
事實上:你需要額外的寫更多的邏輯來處理回撥過快或者失敗或者太慢的問題,
所以我們需要一個通用的方法解決這些信任問題,這就是Promise

Promise

MDN中

定義

Promise是一些不必要現在知道的值的代理。
它允許你將handler和一個非同步的方法(動作)的最終成功值和失敗原因關聯起來。
這使得非同步方法返回的值就像一個同步方法一樣,而不是立即返回最終值,非同步方法返回的是一個promise,它會在將來的某個時間提供值。

用法

Promise 物件用於非同步呼叫返回值的集中處理。 Promise 物件表示一個現在、將來或永不可用的值。

var myFirstPromise=new Promise(function(resolve,reject){
    //當非同步程式碼執行成功時才會呼叫resolved,非同步失敗呼叫reject
    //會用setTImeout模擬非同步,實際編碼可能是XHR請求或H5的一些API
    setTimeout(function(){
        resolve('success');
    },250);
});

myFirstPromise.then(function(success){
    //success的值是上面呼叫的resolve方法出入的值
    console.log(success);
});

關鍵:惰性求值,延時計算的特性

必須理解下面這張圖!

這裡寫圖片描述

Promise處理現在值和將來值

現在值
就是我們當前可以用的值,比如

var x,y=2;
console.log(x+y);

我們計算x+y的時候對它做了一個假設,就是它們是現在值,是已經resolved的(準備好的)。

將來值
但是考慮到非同步在JS中的廣泛使用。
有的值是沒有被準備好的,我們就不能馬上用。
Promise的做法就是不管是現在還是未來的值,
我們都統一當做將來的值來處理。即便是add()
這樣的簡單的運算我們也看作是非同步的了。

例子

function add(xPromise,yPromise){
    return Promise.all([xPromise,yPromise]) //建立並返回第一個promise物件
    .then(function(values){
    //呼叫then,等待上面這個promise,再建立一個promise
    //values來源於之前resolved的promise陣列
        return values[0]+values[1]; //這個promise
    });
}
add(fetchX(),fetchY())
    .then(function(sum){
        console.log(sum)
    });

深刻理解

呼叫Promise結果可能是reject或resolve。
而通過Promise其實我們封裝了依賴於時間的狀態。
Promise本身與時間無關,它按照可預測的方式組合,不關心時序或底層的結果。
一旦Promise resolved了,它就永遠保持這個狀態。
此時他成了不變值。
這是Promise最需要理解也是最強大的一個概念。
你可以多次訪問這個值,可以多方檢視同一個Promise的決議情況!!

Promise 的then方法

Promise例項具有then方法,then方法的作用是給Promise例項新增狀態改變的的回撥函式。
then方法的第一個引數是Resolved,第二個嘗試是Rejected狀態的回撥函式。
then方法返回一個新的Promise例項

getJSON(url).then(function(json){
    return json.post;
}).then(function A(){
    //...
},function B(){
    //...
});

A方法會在Resolved時呼叫,
B方法會在Rejected時呼叫

重點

then上註冊的回撥都會在下一個非同步時機點上
依次被立即呼叫。

Promise對比事件機制

如果呼叫函式foo()會執行一個任務,我們不關心它的底層。
它可能立即完成任務也可能過一段時間才完成。
我們只需要知道他什麼時候結束,這樣就可以進行下一個任務了。
我們在典型的JS風格中,可能會利用事件來做。

function foo(x){
    ...
    return listener;
}
var evt=foo(42); 
evt.on('completion',function(){}); 
evt.on('failure',function(){});  

呼叫foo()顯式建立並返回事件訂閱物件,
並在上面註冊事件。
我們用這個evt物件實現了分離關注點,
其實和觀察者模式很像,它是一箇中立的第三方協商機制。

Promise”事件”

function foo(x){
    //這是一個revealing constructor ,傳入的函式會立即執行。兩個引數resolve和reject這就是promise的決議函式(resolution FUNCTION)
    return new Promise(function(resolve,reject){....});
}
var p=foo(42); //p存了這個promise物件
bar(p);  baz(p);

thenable鴨子型別

如何在promise中確定某個值是不是一個Promise值??
我們無法通過p instanceof Promise來檢查。
庫或框架會實現自己的Promise
或者有些瀏覽器沒有Promise的實現

duck typing

型別檢查,如果它看起來像只鴨子,叫起來像只鴨子,它就是一隻鴨子。

thenable鴨子型別檢測:
//它是一個物件或一個函式
//它有一個then方法

if(p!==null&&(typeof p==='object'||
            typeof p==='function')&&
            typeof p.then==='function'){
    //它是一個thenbale
}else {//它不是一個thenable} 

問題
A. 有可能你希望這個物件或函式完成一個promise,但你不希望它被當做promise
B 原型鏈的問題,如果有程式碼無意或惡意給Object.prototype 或者其他原生原型新增then()
你無法控制也無法預測。

new Promise的理解

一般我們使用這種方式:

new Promise(function(resolve,reject){
    /*Executor*/
})

executor function

這樣的方式,傳入了一個executor function, 這個函式一般是馬上被執行的(這個函式是同步的或立即呼叫的)。 傳遞resolve和reject函式作為引數給它。
這個executor function用於初始化一些非同步的工作,觸發一些非同步的任務,一旦這些任務完成了就會呼叫resolved 回撥函式,一旦失敗了呼叫reject回撥函式!

Promise.resolve的作用

Promise.resolve 相當有用,它可以將現有的物件轉為Promise物件。
我們不需要判斷了直接轉成Promise物件!
例如:

var jsPromise=Promise.resolve($.ajax('/whatever.json'));

因為jQuery生成的是Defered的物件,所以需要轉為Promise物件。

1.傳入Promise例項就會立即返回這個例項
2. 傳入thenable的物件
具有then方法的物件,會將這個物件轉為Promise物件,然後立即執行thenbale物件的then方法
3. 我們可以傳入非thenable或原始值到這個函式

var p1=Promise.resolve('Hello');

返回一個新的Promise物件,狀態為Resolved

現在我們可以解決Promise的信任問題

呼叫回撥過早

Promise不必擔心,因為即使是立即完成的promise。
對一個promise呼叫then的時候,即使這個promise已經resolution。
提供給then的回撥也總是非同步呼叫的!

不需要setTimeout(,,0) hack, Promise不會導致競態。

呼叫過晚

Promise建立物件呼叫resolve() 或reject() 的時候,
這個promise的then(…)註冊的觀察回撥會被自動排程。
見下面的Promise排程的解釋

Promise排程

看這個例子

p.then(function(){
    p.then(function(){
        console.log('C');
    });
    console.log('A');
});
p.then(function(){
    console.log('B');
})

這裡的”C”函式無法打斷或搶佔”B”,這是Promise的運作方式。
也就是說,兩個獨立的Promise連結的回撥的相對順序是無法可靠預測的
它們是獨立的。
我們要避免兩個獨立的Promise連結有依賴關係,好的實踐不會讓
多個回撥的順序有絲毫的關係~!
因為它們存在一個非同步任務佇列,這是回撥執行的順序!

回撥未呼叫

如果你對一個Promise註冊了一個完成回撥和拒絕回撥,promise 在resolution的時候總會選擇呼叫其中一個。
當你的js出現錯誤,可能回撥執行了,但你不知道! 這就是需要有個錯誤提示(傳遞)

“race” 這是一個解決如果Promise永遠不能被resolved的解決方法

Promise.race([
    foo(),
    timeoutPromise(3000);
])

呼叫次數過少或過多

回撥被呼叫的正確次數應該是1,。
Promise的定義方式使得它只能被resolved 一次,如果處於某種原因,
Promise建立的程式碼試圖call resolve(…) or reject(..)多次,
那這個promise只會接收第一次resolved,並忽略後面的呼叫。
但對於回撥:你註冊了多次,回撥就會被呼叫多次!!

回撥未呼叫