ES6新特性:JavaScript中內建的延遲物件Promise

方方和圓圓發表於2016-07-20

Promise的基本使用:

利用Promise是解決JS非同步執行時候回撥函式巢狀回撥函式的問題, 更簡潔地控制函式執行流程;

通過new例項化Promise,  建構函式需要兩個引數, 第一個引數為函式執行成功以後執行的函式resolve, 第二個函式為函式執行失敗以後執行的函式reject:

new Promise(function(resolve , reject) {
});

通過Promise,我們把回撥函式用線性的方式寫出來,而不是一層套一層, 這個函式有四層回撥;

fn("args", function(a) {
    fn1("foo", function(b) {
        fn2("bar", function(c) {
            fn3("baz", function(d) {
                alert("回撥成功,獲知的內容為:"+a+b+c+d)
            })
        })
    })
})

以上的Demo只有包含成功的回撥, 如果失敗的回撥也算的話, 也就更麻煩了;

如果使用Promise的方式,我們可以改裝成線性的程式碼, 更加符合閱讀的習慣,只要在then函式下寫邏輯即可;

new Promise(function(resolve , reject) {
    resolve(1);
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve , reject) {
        resolve(2);
    });
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve , reject) {
        resolve(3);
    });
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve , reject) {
        resolve(4);
    });
}).then(function(val) {
    console.log(val);
});

這是一個ajax非同步獲取資料的例子, 我們使用了回撥函式

<html>
<head>
    <meta charset="utf-8">
</head>
<body>
<script>
    var callback = function(res) {
        console.log(res);
    };
    var ajax = function(url, callback) {
        var r = new XMLHttpRequest();
        r.open("GET", url, true);
        r.onreadystatechange = function () {
            if (r.readyState != 4 || r.status != 200) return;
            var data = JSON.parse(r.responseText);
            callback(data);
        };
        r.send();
    };
    //執行請求:
    ajax("http://www.filltext.com?rows=10&f={firstName}", callback);
    //再做別的事情;
</script>
</body>
</html>

因為ES6內建了Promise, 我們可以把以上的callback改寫成promise的方式, 首先ajax函式返回一個Promise物件;

<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <script>
        var callback = function(res) {
            console.log(res);
        };
        var ajax = function(url) {
            return new Promise(function(resolve, reject) {
                var r = new XMLHttpRequest();
                r.open("GET", url, true);
                r.onreadystatechange = function () {
                    if (r.readyState != 4 || r.status != 200) return;
                    var data = JSON.parse(r.responseText);
                    resolve(data);
                };
                r.send();
            })
        };
        //執行請求:
        ajax("http://www.filltext.com?rows=10&f={firstName}").then(function(data) {
            callback(data);
        });
        //再做別的事情;
    </script>
</body>
</html>

Promise例項的三種狀態:

每一個例項化的Promise都有三個狀態;pending(等待)  rejected(拒絕)  resolved(解決) ,預設的狀態為pending,如果執行了resolve(), 那麼這個promise的狀態會變為resolve,如果執行了reject(), 那麼這個promise的狀態就會變成rejected, 而且這些狀態是不可撤銷的,一經更改,不會再變了;

then方法:

promise有一個then方法,then方法接收兩個引數, 第一個為函式的成功回撥, 第二個為函式的失敗回撥:

var promise = new Promise(function(resolve , reject) {
    resolve(); //執行成功回撥;
});
console.log(0);
promise.then(function(val) {
    console.log(1); 
}, function() {
    console.log("失敗");
});
console.log("2");
var promise = new Promise(function(resolve , reject) {
    reject();
});
console.log(0);
promise.then(function(val) {
    console.log(1);
}, function() {
    console.log("失敗");
});
console.log("2");

then方法每一次都是返回不同的Promise例項,then的第一個引數是成功回撥, 這個成功回撥的引數為: 上一個Promise例項執行resolve方法的引數;

一般來說, then方法會返回當前的promise, 如果在then方法裡面return 一個新的Promise例項,那麼此時的then返回的就是新的Promise例項, 利用這個特性,就可以實現多層回撥

new Promise(function(resolve , reject) {
    resolve(1);
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve , reject) {
        resolve(2);
    });
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve , reject) {
        resolve(3);
    });
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve , reject) {
        resolve(4);
    });
}).then(function(val) {
    console.log(val);
});

不管程式碼是非同步還是同步的, 都可以用Promise的then方法, 同步的程式碼直接寫在then方法第一個引數, 把需要引數通過resolve傳給下一個then方法,

如果是非同步的程式碼, 就直接return一個Promise例項:

new Promise(function(resolve , reject) {
    resolve(1);
}).then(function(val) {
    console.log(val);
    return 2;
}).then(function(val) {
    console.log(val);
    return 3;
}).then(function(val) {
    console.log(val);
    return new Promise(function(resolve,reject) {
        //非同步操作些這裡
        resolve(4);
    });
}).then(function(val) {
    console.log(val);
    return 5;
}).then(function(val) {
    console.log(val);
});

catch方法:

catch方法和失敗回撥時一樣的, 如果上一個非同步函式丟擲了錯誤了, 錯誤會被捕獲, 並執行catch方法或者失敗回撥;

var promise = new Promise(function(resolve , reject) {
    resolve(); //執行成功回撥;
});
console.log(0);
promise.then(function(val) {
    console.log("成功");
    throw new Error("heheda");
}).catch(function(e) {
    console.log(e);
}).then(function() {
    console.log("繼續執行");
});

Promise中的錯誤是會一層層傳遞的, 如果錯誤沒有沒有被捕獲, 會一直傳遞給下一個promise物件, 直到被捕獲為止, 然後繼續往下執行:

new Promise(function(resolve , reject) {
    resolve(1);
}).then(function(val) {
        console.log(val);
        return new Promise(function(resolve , reject) {
            throw new Error("err");
        });
    }).then(function(val) {
        console.log(val);
        return new Promise(function(resolve , reject) {
            resolve(3);
        });
    }).then(function(val) {
        console.log(val);
        return new Promise(function(resolve , reject) {
            resolve(4);
        });
    }).then(function(val) {
        console.log(val);
    }).catch(function(err) {
        console.log(err);
    }).then(function() {
        console.log("繼續執行")
    })

建構函式Promise的四個方法:

建構函式Promise有四個方法, Promise.all, Promise.race, Promise.reject, Promise.resolve:

Promise.all(iterable)
返回一個promise物件,當iterable引數裡所有的promise都被解決後,該promise也會被解決

要注意all方法是Promise函式的方法,不是例項的方法, 引數是一個陣列, 陣列裡面全是Promise的例項 :

var p0 = new Promise(function(resolve) {
    setTimeout(function() {
        resolve(0)
    },1000);
})
var p1 = new Promise(function(resolve) {
    setTimeout(function() {
        resolve(1)
    },2000);
})
var p2 = new Promise(function(resolve) {
    setTimeout(function() {
        resolve(2)
    },3000);
})
Promise.all([p0,p1,p2]).then(function(arr) {
    console.log(arr)
})

Promise.race(iterable)

當iterable引數裡的任意一個子promise被成功或失敗後,父promise馬上也會用子promise的成功返回值或失敗詳情作為引數呼叫父promise繫結的相應控制程式碼,並返回該promise物件。

Promise.reject(reason)

呼叫Promise的rejected控制程式碼,並返回這個Promise物件。

Promise.resolve(value)

用成功值value解決一個Promise物件。如果該value為可繼續的(thenable,即帶有then方法),返回的Promise物件會“跟隨”這個value,採用這個value的最終狀態;否則的話返回值會用這個value滿足(fullfil)返回的Promise物件。

官方的例子:

<html>
<head>
    <meta charset="utf-8">
</head>
<body>
<div id="log"></div>
<script>
    'use strict';
    var promiseCount = 0;
    function testPromise() {
        var thisPromiseCount = ++promiseCount;

        var log = document.getElementById('log');
        log.insertAdjacentHTML('beforeend', thisPromiseCount + ') 開始(同步程式碼開始)<br/>');

        // 我們建立一個新的promise: 然後用'result'字串解決這個promise (3秒後)
        var p1 = new Promise(function (resolve, reject) {
            // 解決函式帶著解決(resolve)或拒絕(reject)promise的能力被執行
            log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Promise開始(非同步程式碼開始)<br/>');

            // 這只是個建立非同步解決的示例
            window.setTimeout(function () {
                // 我們滿足(fullfil)了這個promise!
                resolve(thisPromiseCount)
            }, Math.random() * 2000 + 1000);
        });

        // 定義當promise被滿足時應做什麼
        p1.then(function (val) {
            // 輸出一段資訊和一個值
            log.insertAdjacentHTML('beforeend', val + ') Promise被滿足了(非同步程式碼結束)<br/>');
        });

        log.insertAdjacentHTML('beforeend', thisPromiseCount + ') 建立了Promise(同步程式碼結束)<br/><br/>');
    }
    testPromise();
</script>
</body>
</html>

既然有了Promise , 我們就可以把封裝XMLHttpRequest封裝成GET方法, 方便使用:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

然後使用:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
});

假資料的地址可以自己設定, 可以通過控制檯請求, 注意跨域的問題;

封裝XMLHttpRequest成Promise非同步載入圖片的案例:https://github.com/mdn/promises-test/blob/gh-pages/index.html

其他:

以上只是Promise的一些基礎知識, 還有一些其他的知識點, 因為能力有限不一一介紹了(Promise.resolve的不同引數, 與Generator一起使用, Promise的附加方法, 等等等等);

把Promise的執行流程畫出來, 對Promise的理解會好一點, Promise還是比較繞的

瀏覽器支援情況:

Chrome 32, Opera 1,Firefox 29, Safari 8 ,Microsoft Edge, 這些瀏覽器以上都預設支援;

參考

Promises/A+ 規範: https://promisesaplus.com/

ES6Promises的polyfill : https://github.com/stefanpenner/es6-promise#readme

html5rocks:http://www.html5rocks.com/en/tutorials/es6/promises/

阮老師:http://es6.ruanyifeng.com/#docs/promise

相關文章