摘要:解決異步編程有兩種主要方式事件模型和回調(diào)函數(shù)。將異步操作以同步操作的流程表達(dá)出來,避免了層層嵌套回調(diào)函數(shù)。方法是的別名,相當(dāng)于函數(shù)的第一個參數(shù)傳入,第二個參數(shù)傳入發(fā)生錯誤時的回調(diào)函數(shù)。
JavaScript 解決異步編程有兩種主要方式:事件模型和回調(diào)函數(shù)。但是隨著業(yè)務(wù)越來越復(fù)雜,這兩種方式已經(jīng)不能滿足開發(fā)者的需求了,Promise 可以解決這方面的問題。異步編程的方式 1. 事件模型:為了更好的理解 Promise 是如何工作的,我們先來了解一下傳統(tǒng)的異步編程的方式。
let button = document.getElementId("my-btn"); button.onclick = function() { console.log("Hello"); }
任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個事件是否發(fā)生。上面代碼中,console.log("Hello") 直到 button 被點擊后才會被執(zhí)行。當(dāng) button 被點擊,賦值給 onclick 的函數(shù)就被添加到作業(yè)隊列的尾部,并在隊列前部所有任務(wù)結(jié)束之后再執(zhí)行。
事件模型適用于處理簡單的交互,若將多個獨立的異步調(diào)用連接在一起,必須跟蹤每個事件的事件目標(biāo),會使程序更加復(fù)雜,運行流程會變得很不清晰。
2. 回調(diào)函數(shù)我們來看一下比較經(jīng)典的使用 jsonp 解決跨域問題的示例:
function callback (res) { document.getElementById("d1").innerHTML = res.result.address console.log("Your public IP address is: ", res) } function jsonp (lat, lng) { let src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=yourKey&output=jsonp&callback=callback` let script = document.createElement("script") script.setAttribute("type","text/javascript") script.src = src; document.body.appendChild(script) } jsonp(39.92, 116.43)
初看這種模式運作得相當(dāng)好,簡單、容易理解,但你可能會迅速的發(fā)現(xiàn)這樣的模式不利于代碼的閱讀和維護,各個部分之間耦合度太高,容易陷入回調(diào)地獄。就像這樣:
method1(function(err, result) { if (err) { throw err } method2(function(err, result) { if (err) { throw err } method3(function(err, result) { if (err) { throw err } method4(function(err, result) { if (err) { throw err } method5(result) }) }) }) })
Promise 能大幅度改善這種情況。我們來看下Promise 是如何實現(xiàn)的:
let promise = new Promise((resolve, reject) => { // ... method 1 some code if (/* 異步操作成功 */){ resolve(value); } else { reject(error); } }) promise.then((value) => { // method 2 some code }).then((value) => { // method 3 some code }).then((value) => { // method 4 some code }).then((value) => { // method 5 some code }).catch((err) => { // some err code })
是不是清晰很多?是不是很神奇?接下來一起來學(xué)習(xí)一下 Promise 吧!
Promise 相關(guān)知識 1. Promise 的基礎(chǔ)知識我們先來看下 Promise 的方法有哪些:
Promise.Prototype.then()
Promise.Prototype.catch()
Promise.Prototype.finally()
Promise.all()
Promise.race()
Promise.resolve()
Promise.reject()
Promise.try()
Promise 函數(shù)的執(zhí)行,都是依賴于狀態(tài)的改變,這三種狀態(tài)要記牢哦:
Pending:進行中
Fulfilled:已成功
Rejected:已失敗
Promise 優(yōu)點:
1)對象的狀態(tài)不受外界影響。
2)一旦狀態(tài)改變,就不會再變,任何時候都可以得到這個結(jié)果。
3)將異步操作以同步操作的流程表達(dá)出來,避免了層層嵌套回調(diào)函數(shù)。
4)提供統(tǒng)一的接口,使得控制異步操作更加容易。
Promise 缺點:
1)無法取消 Promise,一旦新建它就會立即執(zhí)行,無法中途取消。
2)如果不設(shè)置回調(diào)函數(shù),Promise 內(nèi)部拋出的錯誤,不會反映到外部。
3)當(dāng)處于 pending 狀態(tài)時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
了解了 Promise 的方法,3種狀態(tài)以及特點和優(yōu)缺點之后,接下來我們來看一下 Promise 是怎么使用的。
2. Promise 的基本用法我們來創(chuàng)造一個讀取文件的 Promise 實例:
const fs = require("fs") const path = require("path") function readFile (filename) { return new Promise (function (resolve, reject) { // reject(new Error("err")) //觸發(fā)異步操作 fs.readFile(filename, {encoding: "utf8"}, function (err, contents) { // 檢查錯誤 if (err) { reject(err) return } //讀取成功 resolve(contents) }) }) } let promise = readFile(path.resolve(__dirname, "../json/a.json"))
上述實例中 resolve 函數(shù)的作用是,將 Promise 對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?pending 變?yōu)?resolved),在異步操作成功時調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去;
reject 函數(shù)的作用是,將 Promise 對象的狀態(tài)從“未完成”變?yōu)椤笆 保磸?pending 變?yōu)?rejected),在異步操作失敗時調(diào)用,并將異步操作報出的錯誤,作為參數(shù)傳遞出去。
2.1 Promise.Prototype.then()Promise 實例生成以后,就可以用 then 方法來分別指定 resolved 狀態(tài)和 rejected 狀態(tài)的回調(diào)函數(shù)了。
promise.then(function (contents) { // 完成 console.log(contents) return(contents) }, function (err) { // 失敗 console.log(err.message) })
我們可以看到,then 方法接受兩個回調(diào)函數(shù)作為參數(shù);
第一個回調(diào)函數(shù)在 Promise 對象的狀態(tài)變?yōu)?resolved 時調(diào)用;
第二個回調(diào)函數(shù)在 Promise 對象的狀態(tài)變?yōu)?rejected 時調(diào)用;
其中,第二個函數(shù)是可選的。這兩個函數(shù)都接受 Promise 對象傳出的值作為參數(shù)。
Promise.then 方法每次調(diào)用,都返回一個新的 Promise 對象,所以支持鏈?zhǔn)綄懛ā?/p>
let taskA = (value) => { console.log("Task A") console.log(value) return value } let taskB = (value) => { console.log("Task B") console.log(value) } promise .then(taskA) .then(taskB) .catch((err) => { console.log(err.message) })2.2 Promise.Prototype.catch()
Promise.prototype.catch 方法是 .then(null, rejection) 的別名,相當(dāng)于 then 函數(shù)的第一個參數(shù)傳入 null,第二個參數(shù)傳入發(fā)生錯誤時的回調(diào)函數(shù)。
promise.then(function(value) { // 成功 console.log(value) }).catch(function (err) { // 失敗 console.log(err.message) })2.3 Promise.Prototype.finally()
finally 方法用于指定不管 Promise 對象最后狀態(tài)如何,都會執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的,目前大部分瀏覽器還不支持,不過可以自己實現(xiàn)。
finally 方法的實現(xiàn):
Promise.prototype.finally = function (callback) { let P = this.constructor return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) ) }
finally 方法的使用:
promise .then((contents) => { console.log(contents) return contents }) .catch((err) => { console.log(err.message) }) .finally(() => { console.log("finally") })2.4 Promise.all()
Promise.all 方法可以將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.all([p1, p2, p3]);
新的 Promise p 的狀態(tài)由 p1、p2、p3 決定,只有當(dāng) p1、p2、p3 的狀態(tài)都變成了 fulfilled,p 的狀態(tài)才會變成 fulfilled;只要 p1、p2、p3 之中有一個被 rejected,p 的狀態(tài)就變成了 rejected。
注意,如果作為參數(shù)的 Promise 實例,自己定義了 catch 方法,那么它一旦被 rejected,并不會觸發(fā)Promise.all()的 catch 方法的。
如果 p2 有自己的 catch 方法,就不會調(diào)用 Promise.all() 的 catch 方法。
const p1 = new Promise((resolve, reject) => { resolve("hello"); }) .then(result => result) .catch(e => e); const p2 = new Promise((resolve, reject) => { throw new Error("報錯了"); }) .then(result => result) .catch(e => e); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // ["hello", Error: 報錯了]
如果 p2 沒有自己的 catch 方法,就會調(diào)用 Promise.all() 的 catch 方法。
const p1 = new Promise((resolve, reject) => { resolve("hello"); }) .then(result => result); const p2 = new Promise((resolve, reject) => { throw new Error("報錯了"); }) .then(result => result); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // Error: 報錯了2.5 Promise.race()
Promise.race 方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
新的Promise p,只要 p1、p2、p3 之中有一個實例率先改變狀態(tài),p 的狀態(tài)就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給 p 的回調(diào)函數(shù)。
function timerPromisefy(delay) { return new Promise(function (resolve, reject) { setTimeout(function () { resolve(delay) }, delay) }) } Promise.race([ timerPromisefy(10), timerPromisefy(20), timerPromisefy(30) ]).then(function (values) { console.log(values) // 10 })2.6 Promise.resolve()
有時需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象,Promise.resolve 方法就起到這個作用,返回一個 fulfilled 狀態(tài)的 Promise 對象。
const promise = Promise.resolve("hello"); promise.then(function(value){ console.log(value); }); // 相當(dāng)于 const promise = new Promise(resolve => { resolve("hello"); }); promise.then((value) => { console.log(value) })2.7 Promise.reject()
Promise.reject(reason) 方法也會返回一個新的 Promise 實例,該實例的狀態(tài)為 rejected。
const p = Promise.reject("出錯了"); p.then(null, (value) => { console.log(value) }) // 等同于 const p = new Promise((resolve, reject) => reject("出錯了")) p.then(null, (value) => { console.log(value) })2.8 Promise.try()
讓同步函數(shù)同步執(zhí)行,異步函數(shù)異步執(zhí)行。
const f = () => console.log("now"); Promise.try(f); console.log("next"); // now // next應(yīng)用 異步加載圖片
實現(xiàn)方法:
function loadImageAsync(url) { return new Promise(function(resolve, reject) { const image = new Image() image.onload = function() { resolve(image) } image.onerror = function() { reject(new Error("Could not load image at " + url)) } image.src = url }) }
調(diào)用:
loadImageAsync("圖片路徑").then((value) => { document.getElementById("d1").appendChild(value) }).catch((err) => { console.log(err.message) })異步加載 js
實現(xiàn)方法:
let loadScript = function () { return function _loadScript(url, callBack) { return new Promise(function (resolve) { let script = document.createElement("script") script.type = "text/javascript" if (script.readyState) { // 兼容IE的script加載事件 script.onreadystatechange = function () { // loaded : 下載完畢 complete: 數(shù)據(jù)準(zhǔn)備完畢。這兩個狀態(tài)ie可能同時出現(xiàn)或者只出現(xiàn)一個 if (script.readyState === "loaded" || script.readyState === "complete") { // 防止加載兩次 script.onreadystatechange = null callBack() // 把函數(shù)傳遞下去,保證能順序加載js resolve(_loadScript) } } } else { script.onload = function () { callBack() resolve(_loadScript) } } script.src = url document.head.appendChild(script) }) } }()
調(diào)用:
loadScript("http://code.jquery.com/jquery-3.2.1.min.js ", () => {}) .then(() => { $("#d1").on("click", () => {alert(1)}) }).catch((err) => { console.log(err) })request 請求的封裝
import axios from "./axios" import qs from "qs" const config = { time: +new Date() + "", timeout: 6000, headers: { "Content-Type": "application/x-www-form-urlencoded", time: new Date().getTime() } } function checkResponse (response, notice) { return new Promise((resolve, reject) => { let code = Number(response.code) if (code === 0 || code === 200 || code === 2000 || code === 1 || code === 2 || code === "0" || code === 109) { resolve(response) } else { if (notice) { // 提示信息 console.log("response-notice", notice) } reject(response) } }) } function fixURL (url, type) { let result = "" switch (type) { case "r": result += `/api/v2${url}` break } return result } /** * Requests a URL, returning a promise. * * @param {object} [options] The options we want to pass to axios * @param {string} [options.url] 請求的url地址(必須) * @param {string} [options.method] 請求方式, get or post,默認(rèn)post * @param {object} [options.data] 請求參數(shù) * @param {number} [options.timeout] 請求超時時間 * @param {boolean} [options.notice] 請求失敗是否顯示提示,默認(rèn)true * @return {object} promise對象 */ function request (options = {}) { let { url, method, data, timeout, headers, type, notice } = options method = method || "post" data = data || {} type = type || "t" timeout = timeout || config.timeout headers = Object.assign({}, config.headers, headers) notice = notice === undefined ? true : notice let result = {} if (method === "get") { result = new Promise((resolve, reject) => { axios({ method: "get", url: fixURL(url, type), params: data, timeout, headers }) .then((res) => { checkResponse(res.data, notice).then((data) => { resolve(data) }) .catch((data) => { reject(data) }) }) .catch((data) => { reject(data) }) }) } else if (method === "post") { result = new Promise((resolve, reject) => { axios({ method: "post", url: fixURL(url, type), data: headers["Content-Type"] === "application/x-www-form-urlencoded" ? qs.stringify(data) : data, timeout, headers }) .then((res) => { checkResponse(res.data, notice).then((data) => { resolve(data) }) .catch((data) => { reject(data) }) }) .catch((data) => { reject(data) }) }) } return result } export default request附 Promise 代碼實現(xiàn)
// 判斷變量否為function const isFunction = variable => typeof variable === "function" // 定義Promise的三種狀態(tài)常量 const PENDING = "PENDING" const FULFILLED = "FULFILLED" const REJECTED = "REJECTED" class MyPromise { constructor (handle) { if (!isFunction(handle)) { throw new Error("MyPromise must accept a function as a parameter") } // 添加狀態(tài) this._status = PENDING // 添加狀態(tài) this._value = undefined // 添加成功回調(diào)函數(shù)隊列 this._fulfilledQueues = [] // 添加失敗回調(diào)函數(shù)隊列 this._rejectedQueues = [] // 執(zhí)行handle try { handle(this._resolve.bind(this), this._reject.bind(this)) } catch (err) { this._reject(err) } } // 添加resovle時執(zhí)行的函數(shù) _resolve (val) { const run = () => { if (this._status !== PENDING) return // 依次執(zhí)行成功隊列中的函數(shù),并清空隊列 const runFulfilled = (value) => { let cb; while (cb = this._fulfilledQueues.shift()) { cb(value) } } // 依次執(zhí)行失敗隊列中的函數(shù),并清空隊列 const runRejected = (error) => { let cb; while (cb = this._rejectedQueues.shift()) { cb(error) } } /* 如果resolve的參數(shù)為Promise對象,則必須等待該Promise對象狀態(tài)改變后, 當(dāng)前Promsie的狀態(tài)才會改變,且狀態(tài)取決于參數(shù)Promsie對象的狀態(tài) */ if (val instanceof MyPromise) { val.then(value => { this._value = value this._status = FULFILLED runFulfilled(value) }, err => { this._value = err this._status = REJECTED runRejected(err) }) } else { this._value = val this._status = FULFILLED runFulfilled(val) } } // 為了支持同步的Promise,這里采用異步調(diào)用 setTimeout(run, 0) } // 添加reject時執(zhí)行的函數(shù) _reject (err) { if (this._status !== PENDING) return // 依次執(zhí)行失敗隊列中的函數(shù),并清空隊列 const run = () => { this._status = REJECTED this._value = err let cb; while (cb = this._rejectedQueues.shift()) { cb(err) } } // 為了支持同步的Promise,這里采用異步調(diào)用 setTimeout(run, 0) } // 添加then方法 then (onFulfilled, onRejected) { const { _value, _status } = this // 返回一個新的Promise對象 return new MyPromise((onFulfilledNext, onRejectedNext) => { // 封裝一個成功時執(zhí)行的函數(shù) let fulfilled = value => { try { if (!isFunction(onFulfilled)) { onFulfilledNext(value) } else { let res = onFulfilled(value); if (res instanceof MyPromise) { // 如果當(dāng)前回調(diào)函數(shù)返回MyPromise對象,必須等待其狀態(tài)改變后在執(zhí)行下一個回調(diào) res.then(onFulfilledNext, onRejectedNext) } else { //否則會將返回結(jié)果直接作為參數(shù),傳入下一個then的回調(diào)函數(shù),并立即執(zhí)行下一個then的回調(diào)函數(shù) onFulfilledNext(res) } } } catch (err) { // 如果函數(shù)執(zhí)行出錯,新的Promise對象的狀態(tài)為失敗 onRejectedNext(err) } } // 封裝一個失敗時執(zhí)行的函數(shù) let rejected = error => { try { if (!isFunction(onRejected)) { onRejectedNext(error) } else { let res = onRejected(error); if (res instanceof MyPromise) { // 如果當(dāng)前回調(diào)函數(shù)返回MyPromise對象,必須等待其狀態(tài)改變后在執(zhí)行下一個回調(diào) res.then(onFulfilledNext, onRejectedNext) } else { //否則會將返回結(jié)果直接作為參數(shù),傳入下一個then的回調(diào)函數(shù),并立即執(zhí)行下一個then的回調(diào)函數(shù) onFulfilledNext(res) } } } catch (err) { // 如果函數(shù)執(zhí)行出錯,新的Promise對象的狀態(tài)為失敗 onRejectedNext(err) } } switch (_status) { // 當(dāng)狀態(tài)為pending時,將then方法回調(diào)函數(shù)加入執(zhí)行隊列等待執(zhí)行 case PENDING: this._fulfilledQueues.push(fulfilled) this._rejectedQueues.push(rejected) break // 當(dāng)狀態(tài)已經(jīng)改變時,立即執(zhí)行對應(yīng)的回調(diào)函數(shù) case FULFILLED: fulfilled(_value) break case REJECTED: rejected(_value) break } }) } // 添加catch方法 catch (onRejected) { return this.then(undefined, onRejected) } // 添加靜態(tài)resolve方法 static resolve (value) { // 如果參數(shù)是MyPromise實例,直接返回這個實例 if (value instanceof MyPromise) return value return new MyPromise(resolve => resolve(value)) } // 添加靜態(tài)reject方法 static reject (value) { return new MyPromise((resolve ,reject) => reject(value)) } // 添加靜態(tài)all方法 static all (list) { return new MyPromise((resolve, reject) => { /** * 返回值的集合 */ let values = [] let count = 0 for (let [i, p] of list.entries()) { // 數(shù)組參數(shù)如果不是MyPromise實例,先調(diào)用MyPromise.resolve this.resolve(p).then(res => { values[i] = res count++ // 所有狀態(tài)都變成fulfilled時返回的MyPromise狀態(tài)就變成fulfilled if (count === list.length) resolve(values) }, err => { // 有一個被rejected時返回的MyPromise狀態(tài)就變成rejected reject(err) }) } }) } // 添加靜態(tài)race方法 static race (list) { return new MyPromise((resolve, reject) => { for (let p of list) { // 只要有一個實例率先改變狀態(tài),新的MyPromise的狀態(tài)就跟著改變 this.resolve(p).then(res => { resolve(res) }, err => { reject(err) }) } }) } finally (cb) { return this.then( value => MyPromise.resolve(cb()).then(() => value), reason => MyPromise.resolve(cb()).then(() => { throw reason }) ); } }
參考文章
Promise 官網(wǎng)
ECMAScript 6 入門
Promise 源碼詳解
Promise 實現(xiàn)原理(附源碼)
es6-promise-try npm
JavaScript Promise:簡介
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/99301.html
摘要:我們可以進行適當(dāng)?shù)母倪M,把回調(diào)函數(shù)寫到外面即使是改寫成這樣,代碼還是不夠直觀,但是如果有了對象,代碼就可以寫得非常清晰,一目了然,請看這樣函數(shù)就不用寫在的回調(diào)中了目前的標(biāo)準(zhǔn)中還未支持對象,那么我們就自己動手,豐衣足食吧。 本文同步自我得博客:http://www.joeray61.com 很多做前端的朋友應(yīng)該都聽說過Promise(或者Deferred)對象,今天我就講一下我對Prom...
摘要:在需要多個操作的時候,會導(dǎo)致多個回調(diào)函數(shù)嵌套,導(dǎo)致代碼不夠直觀,就是常說的回調(diào)地獄,通常通過來解決本意是承諾,在程序中的意思就是承諾我過一段時間后會給你一個結(jié)果。 在需要多個操作的時候,會導(dǎo)致多個回調(diào)函數(shù)嵌套,導(dǎo)致代碼不夠直觀,就是常說的回調(diào)地獄,通常通過promise來解決Promise本意是承諾,在程序中的意思就是承諾我過一段時間后會給你一個結(jié)果。 什么時候會用到過一段時間?答案是...
摘要:如果有錯誤,則到的第二個回調(diào)函數(shù)中,對錯誤進行處理。假設(shè)第一個的第一個回調(diào)沒有返回一個對象,那么第二個的調(diào)用者還是原來的對象,只不過其的值變成了第一個中第一個回調(diào)函數(shù)的返回值。 ES6標(biāo)準(zhǔn)出爐之前,一個幽靈,回調(diào)的幽靈,游蕩在JavaScript世界。 正所謂: 世界本沒有回調(diào),寫的人多了,也就有了})})})})})。 Promise的興起,是因為異步方法調(diào)用中,往往會出現(xiàn)回調(diào)函數(shù)一...
摘要:事件循環(huán)背景是一門單線程非阻塞的腳本語言,單線程意味著,代碼在執(zhí)行的任何時候,都只有一個主線程來處理所有的任務(wù)。在意識到該問題之際,新特性中的可以讓成為一門多線程語言,但實際開發(fā)中使用存在著諸多限制。這個地方被稱為執(zhí)行棧。 事件循環(huán)(Event Loop) 背景 JavaScript是一門單線程非阻塞的腳本語言,單線程意味著,JavaScript代碼在執(zhí)行的任何時候,都只有一個主線程來...
摘要:等到一段時間后,車到了,小紅打電話給小明車到了發(fā)布,小明在接到電話之后,要做一些準(zhǔn)備訂閱時定義的回調(diào)函數(shù)。工作中的異步通過事件實例去做調(diào)控,簡單的代碼示例是一個事件實例,負(fù)責(zé)發(fā)布訂閱者的內(nèi)部實現(xiàn)訂閱發(fā)布另一端程序干一些事情。 淺談異步編程 引子 頁面渲染與setTimeout(); 同步與異步------我的理解 任務(wù)在當(dāng)次事件循環(huán)中阻塞后續(xù)任務(wù)進行的(指的是耗時較多,這個多少,暫時還...
摘要:如果改變已經(jīng)發(fā)生了,你再對對象添加回調(diào)函數(shù),也會立即得到這個結(jié)果。其次,如果不設(shè)置回調(diào)函數(shù),內(nèi)部拋出的錯誤,不會反應(yīng)到外部。作為一個入門級前端,今天是一個非常值得紀(jì)念的日子,因為這是我第一次在論壇上發(fā)表帖子,作為起步。雖然我覺得自己水平還是十分的有限,對一些細(xì)節(jié)的理解還不是很透徹,但是還是要邁出這一步,不管是給別的新手作為學(xué)習(xí)參考,還是自己以后回顧,總覺得需要把自己的成長記錄下來,希望自己以...
閱讀 1193·2021-11-22 13:54
閱讀 2441·2021-09-22 15:36
閱讀 2745·2019-08-30 15:54
閱讀 816·2019-08-30 15:53
閱讀 3178·2019-08-30 15:53
閱讀 522·2019-08-29 15:21
閱讀 2876·2019-08-28 18:28
閱讀 3024·2019-08-26 13:37