Promise.then(fill) 中如何在 fill 的非同步邏輯中改變PromiseStatus?

我最近在考慮實現一個 Promise 將 blob 轉成 & 或者 Image,我本來考慮這樣一種實現但是沒有成功

Promise.resolve(dataURI).then( (uri) =&> {
const img = new Image();
img.onerror = () =&> {throw "not valid uri"};
img.src = uri;
return img;
})

但是 throw "not valid uri" 這裡並不會改變最後的 promise.status ,最後結果是 Promise.resolve(img);

請問用這場的鏈式調用有辦法在 Promise.resolve() 之後使用 resolve 或者 reject 么?

現在實現是調轉順序,但是好難看的感覺

function previewFilePromise(file) {
const readFilePromise = new Promise((resolve, reject) =&> {
const fr = new FileReader();
fr.onload = () =&> {
resolve(fr.result);
};
fr.onerror = () =&> {
reject(fr.error);
};
});
const result = new Promise((resolve, reject) =&> {
readFilePromise.then(src =&> {
const img = new Image();
img.onload = () =&> resolve(img);
img.onerror = () =&> reject(img);
img.src = src;
});
});
return result;
}


用函數把代碼分割開,不要混在一起。如果一個函數里有兩個 new Promise() ,說明你很有可能做錯了。

const readFileAsURL = file =&> new Promise((resolve, reject) =&> {
// 其實我覺得這裡應該用 URL.createObjectURL()
const fr = new FileReader();
fr.onload = () =&> resolve(fr.result);
fr.onerror = () =&> reject(fr.error);
fr.readAsDataURL(file);
});

const previewImage = url =&> new Promise((resolve, reject) =&> {
const img = new Image();
img.onload = () =&> resolve(img);
img.onerror = () =&> reject(new Error("Failed to load image"));
img.src = url;
});

readFileAsURL(file)
.then(previewImage)
.then(img =&> {
...
})
.catch((e) =&> {
...
});


Promise 被設計為只能一次性改變狀態。在一個 then callback 裡面,意味著這個 Promise 已經做完唯一一次的狀態改變,你已經不能再動那個 Promise 的狀態了。

然而 then 函數本身返回一個新的 Promise,那個 Promise 什麼時候發生狀態改變以及狀態改變為成功還是失敗可以由 then callback 控制。這也就是為什麼大家用 then chain 來串聯多個操作。

例子:

Promise.resolve(「step 1」)

.then((input) =&> {

console.log(input);

if (Math.random() &> 0.5) {

throw new Error(「error in 「 + input);

}

return 「step 2」;

}).then((input) =&> {

console.log(input);

}, (error) =&> {

console.log(error.message);

});

// 傻逼知乎!在手機 app 上輸入的所有縮緊都沒有了。明明 app 不支持代碼格式,但又不讓人用 web。


第二種寫法是對的,雖然我沒明白 file 參數是幹嘛的。這就是對 event listener 範式(比如 onload 和 onerror)進行 promisify 的一般方法。

不能在 resolve 之後 reject,要 reject 的方法就是先別 resolve——這就是你第二種方法所寫的。看起來比較冗長是因為 FileReader 和 Image 的 API 需要被 promisify。


不知道我的理解對不對

你這邊似乎是像分別判斷 fileReader 讀取文件的成功與否和 img 載入成功與否

只要有一項沒有載入成功 就reject

但是你可能忽略了一點, Promise 的狀態是不可逆的,也就是說你 resolve 或者 reject 一個 Promise 以後就不能再改了

你這種做法可以使用 Promise 的鏈式調用

例:

new Promise((res, rej) =&> {
fr.onload = () =&> res();
fr.onerror = () =&> rej();
}).then((val) =&> new Promise((res, rej) =&> {
img.onload = () =&> res();
img.onerror = () =&> rej();
})).then(() =&> {
console.log("DONE!");
}).catch(e =&> {
console.log("ERROR!");
});

第一個Promise resolve 之後會執行第一個 then

如果你在這個 then 里返回了一個新的 promise,就可以依次鏈式調用,依次第二個 then 第三個。。

如果你採用這種鏈式調用的方法,那麼最後的 catch 會捕捉每一個 Promise 的reject,只要有一個被 reject 了,後面的 then 就不會觸發,而是觸發最後的 catch

當然如果你想分別為每一個 Promise 指定不同的 catch 方法可以這麼寫

new Promise((res, rej) =&> {
if (XXX) res();
else rej();
}).then(val =&> {
new Promise((res, rej) =&> {
if (XXX) res();
else rej();
}).then(val =&> {
new Promise and so on....
}).catch(e =&> {});
}).catch(e =&> {});

其實講道理我推薦用 async + await ,想要分別處理和集中處理只需要調整 try catch 的位置。。。

例:

// 集中處理
(async()=&>{
try{
let resVal1 = await new Promise((res, rej) =&> {if (XXX) res() else rej()});
let resVal2 = await new Promise((res, rej) =&> {if (XXX) res() else rej()});
}catch(e){
console.log("error value:", e);
}
})();
// 分別處理
(async()=&>{
try{
let resVal1 = await new Promise((res, rej) =&> {if (XXX) res() else rej()});
}catch(e){
console.log("error value1:", e);
}
try{
let resVal2 = await new Promise((res, rej) =&> {if (XXX) res() else rej()});
}catch(e){
console.log("error value2:", e);
}
})();

當然如果你用第二種方法並且用 let 或者 const 記得考慮作用域的問題。。。try 是有單獨作用域的。。。。


題主你第二部分的思路是對的。

你的問題按照你自己的話說就是第二段代碼太難看,實際上就是可讀性太差。

我給你看下,思路完全照搬你的二段的思路,怎麼改寫可讀性會比較好。我看到你用了ES6的語法,所以用了async function,實際上是個語法糖,不用的話也行。

async function previewFilePromise(file) {

const reader = new FileReader();

let readSrcP = new Promise((resolve, reject) =&> {
reader.onload = resolve;
reader.onerror = reject;
});

readSrcP =
readSrcP
.then( () =&> fr.result)
.catch(() =&> Promise.reject(fr.error));

reader.read(file);

const src = await readSrcP;

const img = new Image();

let showImgP = new Promise((resolve, reject) =&> {
img.onload = resolve;
img.onerror = reject;
});

showImgP = showImgP.then( () =&> img)
.catch(() =&> Promise.reject(img));

img.src = src;
img.load();

return await showImgP;
}

你的代碼問題在於,試圖用最少的代碼做最多的事,而且對Promise的一些習慣寫法不熟悉。

因此寫出來的代碼非常反直覺,並不是直接對應你的思路。(要理解你第二個Promise的機制,需要來回跳轉很多次)

還有的問題是有回調嵌套(最深處有三層),這本來是Promise應該解決的問題。

拿你的第一個Promise來舉例子,你的第一個Promise幹了三件事:

1)建立fr對象

2)把 回調型非同步代碼 變為 Promise型非同步代碼(Promise化)

3)是綁定非同步代碼的結果到Promise

實際上,你這裡最最重要的工作是Promise化。這是讀你的代碼的人要抓的最關鍵的點。因此你務必要把這三件事的流程分開。

在改過的例子里,完全按照你的思路。但是把這三件事分成了邏輯上完全不相干的三個簡單部分。每個部分都很簡單。讀代碼時,可以直接讀任意一段,不需要參考之前之後的其他代碼片段就知道在做什麼:

比如這段,很明顯很乾凈的就是promise化,不看上下文就知道是promise化。而且少一層回調會容易看很多。

let readSrcP = new Promise((resolve, reject) =&> {
reader.onload = resolve;
reader.onerror = reject;
});

而這一段,就是綁定回調結果。一段代碼只做一件事,而且盡量不要上下文相關。用最標準的大家都熟悉的方法來寫。

readSrcP =
readSrcP
.then( () =&> fr.result)
.catch(() =&> Promise.reject(fr.error));

題主還沒參加工作吧?一般進個技術型公司干一段時間自然這些都會學會。比如我們組的一個關於可讀性的經驗法則:

把一句代碼翻譯成自然語言(中文/英文),如果 這句話是上下文無關的,沒有逗號,沒有從句,那麼這段代碼也一般是可讀的.


const previewFilePromise = file =&>
new Promise((resolve, reject) =&> {
const fr = new FileReader();
fr.onload = () =&> resolve(fr.result)
fr.onerror = () =&> reject(fr.error)
})
.then(src =&> new Promise((resolve, reject) =&> {
const img = new Image();
img.onload = () =&> resolve(img);
img.onerror = () =&> reject(img);
img.src = src;
})

在.then回調里如果需要非同步地resolve/reject整個promise,那麼讓其返回一個new Promise即可。

&> 如果一個函數里有兩個 new Promise() ,說明你很有可能做錯了。

函數里有兩個new Promise不是問題。兩個或以上的Promise變成嵌套了的才是問題,也就是題主第二種寫法。

Promise的正確用法是讓非同步過程能像同步過程一樣寫,有時候就需要利用Promise的這個特性:.then里返回新Promise可以將原Promise的控制權交給新Promise。如果寫成外層Promise套內層Promise(題主的第二種寫法),那麼其實本質還是回調地獄。


Promise 的好處就是將非同步操作解耦,避免回調地獄。所以正確的寫法應該是將每個非同步操作寫成 promise風格,通過 then 連接起來。所以新手常犯的一個錯誤是把 promise當成了非同步回調。

錯誤寫法:

asyncFunA().then(()=&>{

return asyncFunB().then(()=&>{

return asyncFunC().then(result=&>{

return result

})

})

})

正確寫法 asyncFunA().then(()=&>{return asyncFunB()}).then(()=&>{return asyncFunC()}).then(res=&> return res)

正確寫法和錯誤寫法運行結果一致,但很明顯第二種清晰很多。


單純就從語義上來說,Promise已經resolve了,肯定是無法再改變對應PromiseStatus。

再就著需求多說兩句,要實現題主的需求最主流的無非就兩種方式,第一種內嵌式 which 題主已經實現了

const foo = (arg) =&> {

return new Promise((resolve, reject) =&> {

new Promise((res, rej) =&> {

// 文件讀取部分

res()

...

rej()

})
.then(result =&> {

// 轉化為圖片部分

resolve()

...

reject()

})
.catch(e =&> {

reject(e)

})

})

}

foo(some arg)
.then(...)
.catch(...)

第二種就是鏈式調用, 如同 @魯小夫 寫的一樣

const foo = (arg) =&> {
return new Promise((resolve, reject) =&> {
// 文件讀取
reject()
...
resolve()
...
})
}

const bar = (arg) =&> {
return new Promise((resolve, reject) =&> {
// 圖片轉化
reject()
...
resolve()
...
})
}

foo(some arg)
.then(bar)
.then(...)
.catch(...)

可讀性來說第二種會更好些


感謝 @魯小夫 關於模塊化的建議

感謝 @Steven Wang 關於可讀性和 promisify 的建議

感謝 @Cat Chen 關於Promise.resolve(new Promise()) 的建議

感謝 匿名用戶 關於 callback hell 的建議

現在的代碼修改如下:

function createPreviewImagePromise(file) {
const fr = new FileReader();
const FileReaderPromise = promisifyEvent(fr);
fr.readAsDataURL(file);
let result = FileReaderPromise.then(() =&> {
return fr.result;
}).catch(() =&> {
throw { type: "FileReader", error: fr.error };
});

const img = new Image();
const ImagePromise = promisifyEvent(img);
result = result
.then(uri =&> {
img.src = uri;
return ImagePromise;
})
.then(() =&> img)
.catch(e =&> {
if (e.type === "FileReader") {
throw e;
}
throw { type: "Image", error: img.error };
});
return result;
}

現在感覺是最後一個 throw 好難看,難道一般是寫一個泛用的 normalize error 最後每次 catch 時調用?


推薦閱讀:

TAG:前端開發 | Promise |