這篇文章帶給大家的內容是關於淺析es6中Promise(附實例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
Promise 的基本使用可以看阮一峰老師的 《ECMAScript 6 入門》。
我們來聊點其他的。
回呼
說起 Promise,我們一般都會從回呼或回呼地獄說起,那麼使用回呼到底會導致哪些不好的地方呢?
1. 回呼嵌套
使用回調,我們很有可能會將業務程式碼寫成如下這種形式:
doA( function(){ doB(); doC( function(){ doD(); } ) doE(); } ); doF();
當然這是一種簡化的形式,經過一番簡單的思考,我們可以判斷出執行的順序為:
doA() doF() doB() doC() doE() doD()
然而在實際的專案中,程式碼會更加雜亂,為了排查問題,我們需要繞過很多礙眼的內容,不斷的在函數間進行跳轉,使得排除問題的難度也在倍增。
當然之所以導致這個問題,其實是因為這種嵌套的書寫方式跟人線性的思考方式相違和,以至於我們要多花一些精力去思考真正的執行順序,嵌套而縮排只是這個思考過程中轉移注意力的細枝末節而已。
當然了,與人線性的思考方式相違和,還不是最糟糕的,實際上,我們還會在程式碼中加入各種各樣的邏輯判斷,就比如在上面這個例子中,doD() 必須在doC() 完成後才能完成,萬一doC() 執行失敗了呢?我們要重試 doC() 嗎?還是直接轉到其他錯誤處理函數?當我們將這些判斷都加入到這個流程中,很快程式碼就會變得非常複雜,以至於無法維護和更新。
2. 控制反轉
正常書寫程式碼的時候,我們理所當然可以控制自己的程式碼,然而當我們使用回呼的時候,這個回呼函數是否能接著執行,其實取決於使用回呼的那個API,就例如:
// 回调函数是否被执行取决于 buy 模块 import {buy} from './buy.js'; buy(itemData, function(res) { console.log(res) });
對於我們常會使用的fetch 這種API,一般是沒有什麼問題的,但是如果我們使用的是第三方的API呢?
當你呼叫了第三方的 API,對方是否會因為某個錯誤導致你傳入的回呼函數執行了多次?
為了避免這樣的問題,你可以在自己的回呼函數中加入判斷,可是萬一又因為某個錯誤這個回呼函數沒有執行呢?
萬一這個回呼函數有時同步執行有時會非同步執行呢?
我們總結一下這些情況:
#回呼函數執行多次
對於這些情況,你可能要在回呼函數中做些處理,並且每次執行回呼函數的時候都要做些處理,這就帶來了很多重複的程式碼。
我們先來看一個簡單的回呼地獄的範例。
取得目錄中的文件列表;
循環遍歷文件,使用
fs.stat取得文件資訊
比較找出最大文件;
以最大檔案的檔案名稱參數呼叫回呼。
程式碼為:
var fs = require('fs'); var path = require('path'); function findLargest(dir, cb) { // 读取目录下的所有文件 fs.readdir(dir, function(er, files) { if (er) return cb(er); var counter = files.length; var errored = false; var stats = []; files.forEach(function(file, index) { // 读取文件信息 fs.stat(path.join(dir, file), function(er, stat) { if (errored) return; if (er) { errored = true; return cb(er); } stats[index] = stat; // 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操作 if (--counter == 0) { var largest = stats .filter(function(stat) { return stat.isFile() }) .reduce(function(prev, next) { if (prev.size > next.size) return prev return next }) cb(null, files[stats.indexOf(largest)]) } }) }) }) }
使用方式為:
// 查找当前目录最大的文件 findLargest('./', function(er, filename) { if (er) return console.error(er) console.log('largest file was:', filename) });
你可以將上述程式碼複製到一個例如index.js
文件,然後執行
就可以列印出最大的文件的名稱。 看完這個例子,我們再來聊聊回調地獄的其他問題:
1.難以復用回呼的順序確定下來之後,想對其中的某些環節進行複用也很困難,牽一發而動全身。
###舉個例子,如果你想對###fs.stat### 讀取檔案資訊這段程式碼重複使用,因為回呼中引用了外層的變量,提取出來後還需要對外層的程式碼進行修改。 #########2.堆疊資訊被斷開#########我們知道,JavaScript 引擎維護了一個執行上下文棧,當函數執行的時候,會創建該函數的執行上下文壓入堆疊中,當函數執行完畢後,會將該執行上下文出棧。 ######如果A 函數中呼叫了B 函數,JavaScript 會先將A 函數的執行上下文壓入堆疊中,再將B 函數的執行上下文壓入堆疊中,當B 函數執行完畢,將B 函數執行上下文出棧,當A 函數執行完畢後,將A 函數執行上下文出棧。 ######這樣的好處在於,我們如果中斷程式碼執行,可以檢索完整的堆疊訊息,從中獲取任何我們想要獲取的資訊。 ###可是异步回调函数并非如此,比如执行 fs.readdir
的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。
此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。
(不过 Promise 并没有解决这个问题)
3.借助外层变量
当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。
之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。
Promise
Promise 使得以上绝大部分的问题都得到了解决。
1. 嵌套问题
举个例子:
request(url, function(err, res, body) { if (err) handleError(err); fs.writeFile('1.txt', body, function(err) { request(url2, function(err, res, body) { if (err) handleError(err) }) }) });
使用 Promise 后:
request(url) .then(function(result) { return writeFileAsynv('1.txt', result) }) .then(function(result) { return request(url2) }) .catch(function(e){ handleError(e) });
而对于读取最大文件的那个例子,我们使用 promise 可以简化为:
var fs = require('fs'); var path = require('path'); var readDir = function(dir) { return new Promise(function(resolve, reject) { fs.readdir(dir, function(err, files) { if (err) reject(err); resolve(files) }) }) } var stat = function(path) { return new Promise(function(resolve, reject) { fs.stat(path, function(err, stat) { if (err) reject(err) resolve(stat) }) }) } function findLargest(dir) { return readDir(dir) .then(function(files) { let promises = files.map(file => stat(path.join(dir, file))) return Promise.all(promises).then(function(stats) { return { stats, files } }) }) .then(data => { let largest = data.stats .filter(function(stat) { return stat.isFile() }) .reduce((prev, next) => { if (prev.size > next.size) return prev return next }) return data.files[data.stats.indexOf(largest)] }) }
2. 控制反转再反转
前面我们讲到使用第三方回调 API 的时候,可能会遇到如下问题:
回调函数执行多次
回调函数没有执行
回调函数有时同步执行有时异步执行
对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。
对于第二个问题,我们可以使用 Promise.race 函数来解决:
function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( "Timeout!" ); }, delay ); } ); } Promise.race( [ foo(), timeoutPromise( 3000 ) ] ) .then(function(){}, function(err){});
对于第三个问题,为什么有的时候会同步执行有的时候回异步执行呢?
我们来看个例子:
var cache = {...}; function downloadFile(url) { if(cache.has(url)) { // 如果存在cache,这里为同步调用 return Promise.resolve(cache.get(url)); } return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用 } console.log('1'); getValue.then(() => console.log('2')); console.log('3');
在这个例子中,有 cahce 的情况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。
然而如果将这种同步和异步混用的代码作为内部实现,只暴露接口给外部调用,调用方由于无法判断是到底是异步还是同步状态,影响程序的可维护性和可测试性。
简单来说就是同步和异步共存的情况无法保证程序逻辑的一致性。
然而 Promise 解决了这个问题,我们来看个例子:
var promise = new Promise(function (resolve){ resolve(); console.log(1); }); promise.then(function(){ console.log(2); }); console.log(3); // 1 3 2
即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。
PromiseA+ 规范也有明确的规定:
实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
Promise 反模式
1.Promise 嵌套
// bad loadSomething().then(function(something) { loadAnotherthing().then(function(another) { DoSomethingOnThem(something, another); }); });
// good Promise.all([loadSomething(), loadAnotherthing()]) .then(function ([something, another]) { DoSomethingOnThem(...[something, another]); });
2.断开的 Promise 链
// bad function anAsyncCall() { var promise = doSomethingAsync(); promise.then(function() { somethingComplicated(); }); return promise; }
// good function anAsyncCall() { var promise = doSomethingAsync(); return promise.then(function() { somethingComplicated() }); }
3.混乱的集合
// bad function workMyCollection(arr) { var resultArr = []; function _recursive(idx) { if (idx >= resultArr.length) return resultArr; return doSomethingAsync(arr[idx]).then(function(res) { resultArr.push(res); return _recursive(idx + 1); }); } return _recursive(0); }
你可以写成:
function workMyCollection(arr) { return Promise.all(arr.map(function(item) { return doSomethingAsync(item); })); }
如果你非要以队列的形式执行,你可以写成:
function workMyCollection(arr) { return arr.reduce(function(promise, item) { return promise.then(function(result) { return doSomethingAsyncWithResult(item, result); }); }, Promise.resolve()); }
4.catch
// bad somethingAync.then(function() { return somethingElseAsync(); }, function(err) { handleMyError(err); });
如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:
// good somethingAsync .then(function() { return somethingElseAsync() }) .then(null, function(err) { handleMyError(err); });
// good somethingAsync() .then(function() { return somethingElseAsync(); }) .catch(function(err) { handleMyError(err); });
红绿灯问题
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)
三个亮灯函数已经存在:
function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); }
利用 then 和递归实现:
function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); } var light = function(timmer, cb){ return new Promise(function(resolve, reject) { setTimeout(function() { cb(); resolve(); }, timmer); }); }; var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(2000, green); }).then(function(){ return light(1000, yellow); }).then(function(){ step(); }); } step();
promisify
有的时候,我们需要将 callback 语法的 API 改造成 Promise 语法,为此我们需要一个 promisify 的方法。
因为 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,如果没有错误,就是 null,所以我们可以直接写出一个简单的 promisify 方法:
function promisify(original) { return function (...args) { return new Promise((resolve, reject) => { args.push(function callback(err, ...values) { if (err) { return reject(err); } return resolve(...values) }); original.call(this, ...args); }); }; }
Promise 的局限性
1. 错误被吃掉
首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?
并不是,举个例子:
throw new Error('error'); console.log(233333);
在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:
const promise = new Promise(null); console.log(233333);
以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。
然而再举个例子:
let promise = new Promise(() => { throw new Error('error') }); console.log(2333333);
这次会正常的打印 233333
,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。
其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。
而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。
2. 单一值
Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。
说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:
Promise.all([Promise.resolve(1), Promise.resolve(2)]) .then(([x, y]) => { console.log(x, y); });
3. 无法取消
Promise 一旦新建它就会立即执行,无法中途取消。
4. 无法得知 pending 状态
当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
以上是淺析es6中Promise(附實例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!