promise
创建 XHR 的 promise 对象(Promise 包装 XHR[ajax]处理)
方法一:
创建一个用 Promise 把 XHR 处理包装起来的名为 getURL
的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function getURL(URL) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open("GET", URL, true); req.onload = function() { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { reject(new Error(req.statusText)); }; req.send(); }); }
var URL = "http://httpbin.org/get"; getURL(URL) .then(function onFulfilled(value) { console.log(value); }) .catch(function onRejected(error) { console.error(error); });
|
方法二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function ajaxPromise(url, data, callback) { return new Promise(function(resolve, reject) { $.ajax({ url: url, type: data == null ? "GET" : "POST", dataType: "json", data: data == null ? "" : JSON.stringify(data), async: true, contentType: "application/json", success: function(res) { callback(res); resolve(); }, error: function(XMLHttpRequest, textStatus, errorThrown) { if (XMLHttpRequest.status == "401") { window.parent.location = "/enterprise/enterprise_login.html"; self.location = "/enterprise/enterprise_login.html"; } else { alert(XMLHttpRequest.responseText); } reject(); } }); }); }
|
调用
1 2 3 4 5 6 7 8 9 10 11
| ajaxPromise('/prefix/entity1/action1',null, function(res){ }).then( ajaxPromise('/prefix/entity2/action2', someData, function(res){ } ).then( initVue() ; ).then( )
|
promise 的实现和原理(用 js)
promise 实现原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
| const isFunction = variable => typeof variable === "function";
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"); } this._status = PENDING; this._value = undefined; this._fulfilledQueues = []; this._rejectedQueues = []; try { handle(this._resolve.bind(this), this._reject.bind(this)); } catch (err) { this._reject(err); } } _resolve(val) { const run = () => { if (this._status !== PENDING) return; const runFulfilled = value => { let cb; while ((cb = this._fulfilledQueues.shift())) { cb(value); } }; const runRejected = error => { let cb; while ((cb = this._rejectedQueues.shift())) { cb(error); } };
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); } }; setTimeout(run, 0); } _reject(err) { if (this._status !== PENDING) return; const run = () => { this._status = REJECTED; this._value = err; let cb; while ((cb = this._rejectedQueues.shift())) { cb(err); } }; setTimeout(run, 0); } then(onFulfilled, onRejected) { const { _value, _status } = this; return new MyPromise((onFulfilledNext, onRejectedNext) => { let fulfilled = value => { try { if (!isFunction(onFulfilled)) { onFulfilledNext(value); } else { let res = onFulfilled(value); if (res instanceof MyPromise) { res.then(onFulfilledNext, onRejectedNext); } else { onFulfilledNext(res); } } } catch (err) { onRejectedNext(err); } }; let rejected = error => { try { if (!isFunction(onRejected)) { onRejectedNext(error); } else { let res = onRejected(error); if (res instanceof MyPromise) { res.then(onFulfilledNext, onRejectedNext); } else { onFulfilledNext(res); } } } catch (err) { onRejectedNext(err); } }; switch (_status) { case PENDING: this._fulfilledQueues.push(fulfilled); this._rejectedQueues.push(rejected); break; case FULFILLED: fulfilled(_value); break; case REJECTED: rejected(_value); break; } }); } catch(onRejected) { return this.then(undefined, onRejected); } static resolve(value) { if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); } static reject(value) { return new MyPromise((resolve, reject) => reject(value)); } static all(list) { return new MyPromise((resolve, reject) => {
let values = []; let count = 0; for (let [i, p] of list.entries()) { this.resolve(p).then( res => { values[i] = res; count++; if (count === list.length) resolve(values); }, err => { reject(err); } ); } }); } static race(list) { return new MyPromise((resolve, reject) => { for (let p of list) { 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.race 和超时处理
函数 timeoutPromise(比较对象promise, ms)
接收两个参数,第一个是需要使用超时机制的 promise 对象,第二个参数是超时时间,它返回一个由 Promise.race
创建的相互竞争的 promise 对象。
1 2 3 4 5 6 7 8 9 10 11
| function delayPromise(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function() { throw new Error("Operation timed out after " + ms + " ms"); }); return Promise.race([promise, timeout]); }
|
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function delayPromise(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function() { throw new Error("Operation timed out after " + ms + " ms"); }); return Promise.race([promise, timeout]); }
var taskPromise = new Promise(function(resolve) { var delay = Math.random() * 2000; setTimeout(function() { resolve(delay + "ms"); }, delay); }); timeoutPromise(taskPromise, 1000) .then(function(value) { console.log("taskPromise在规定时间内结束 : " + value); }) .catch(function(error) { console.log("发生超时", error); });
|
虽然在发生超时的时候抛出了异常,但是这样的话我们就不能区分这个异常到底是普通的错误还是超时错误了。
为了能区分这个 Error
对象的类型,我们再来定义一个Error
对象的子类 TimeoutError
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function copyOwnFrom(target, source) { Object.getOwnPropertyNames(source).forEach(function(propName) { Object.defineProperty( target, propName, Object.getOwnPropertyDescriptor(source, propName) ); }); return target; } function TimeoutError() { var superInstance = Error.apply(null, arguments); copyOwnFrom(this, superInstance); } TimeoutError.prototype = Object.create(Error.prototype); TimeoutError.prototype.constructor = TimeoutError;
|
我们定义了 TimeoutError
类和构造函数,这个类继承了 Error 的 prototype。
它的使用方法和普通的 Error
对象一样,使用 throw
语句即可,如下所示。
1 2 3 4 5 6 7
| var promise = new Promise(function() { throw TimeoutError("timeout"); });
promise.catch(function(error) { console.log(error instanceof TimeoutError); });
|
取消 XHR 操作本身的话并不难,只需要调用 XMLHttpRequest
对象的 abort()
方法就可以了。
为了能在外部调用 abort()
方法,我们先对之前本节出现的 getURL
进行简单的扩展,cancelableXHR
方法除了返回一个包装了 XHR 的 promise 对象之外,还返回了一个用于取消该 XHR 请求的abort
方法。
大体的流程就像下面这样。
- 通过
cancelableXHR
方法取得包装了 XHR 的 promise 对象和取消该 XHR 请求的方法
- 在
timeoutPromise
方法中通过 Promise.race
让 XHR 的包装 promise 和超时用 promise 进行竞争。
- XHR 在超时前返回结果的话
- 和正常的 promise 一样,通过
then
返回请求结果
- 发生超时的时候
- 抛出
throw TimeoutError
异常并被 catch
- catch 的错误对象如果是
TimeoutError
类型的话,则调用 abort
方法取消 XHR 请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| function copyOwnFrom(target, source) { Object.getOwnPropertyNames(source).forEach(function(propName) { Object.defineProperty( target, propName, Object.getOwnPropertyDescriptor(source, propName) ); }); return target; } function TimeoutError() { var superInstance = Error.apply(null, arguments); copyOwnFrom(this, superInstance); } TimeoutError.prototype = Object.create(Error.prototype); TimeoutError.prototype.constructor = TimeoutError; function delayPromise(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function() { return Promise.reject( new TimeoutError("Operation timed out after " + ms + " ms") ); }); return Promise.race([promise, timeout]); } function cancelableXHR(URL) { var req = new XMLHttpRequest(); var promise = new Promise(function(resolve, reject) { req.open("GET", URL, true); req.onload = function() { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { reject(new Error(req.statusText)); }; req.onabort = function() { reject(new Error("abort this request")); }; req.send(); }); var abort = function() { if (req.readyState !== XMLHttpRequest.UNSENT) { req.abort(); } }; return { promise: promise, abort: abort }; } var object = cancelableXHR("http://httpbin.org/get");
timeoutPromise(object.promise, 1000) .then(function(contents) { console.log("Contents", contents); }) .catch(function(error) { if (error instanceof TimeoutError) { object.abort(); return console.log(error); } console.log("XHR Error :", error); });
|
promise.all 和顺序处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| function getURL(URL) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open("GET", URL, true); req.onload = function() { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL("http://azu.github.io/promises-book/json/comment.json").then( JSON.parse ); }, people: function getPeople() { return getURL("http://azu.github.io/promises-book/json/people.json").then( JSON.parse ); } }; function main() { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return request .comment() .then(pushValue) .then(request.people) .then(pushValue); }
main() .then(function(value) { console.log(value); }) .catch(function(error) { console.error(error); });
|
使用这种写法的话那么随着 request
中元素数量的增加,我们也需要不断增加对 then
方法的调用
- 将处理内容统一放到数组里,再配合 for 循环进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| function getURL(URL) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open("GET", URL, true); req.onload = function() { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL("http://azu.github.io/promises-book/json/comment.json").then( JSON.parse ); }, people: function getPeople() { return getURL("http://azu.github.io/promises-book/json/people.json").then( JSON.parse ); } }; function main() { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); var tasks = [request.comment, request.people]; var promise = Promise.resolve(); for (var i = 0; i < tasks.length; i++) { var task = tasks[i]; promise = promise.then(task).then(pushValue); } return promise; }
main() .then(function(value) { console.log(value); }) .catch(function(error) { console.error(error); });
|
promise = promise.then(task).then(pushValue);
的代码就是通过不断对 promise 进行处理,不断的覆盖 promise
变量的值,以达到对 promise 对象的累积处理效果。
但是这种方法需要 promise
这个临时变量,从代码质量上来说显得不那么简洁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| function getURL(URL) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open("GET", URL, true); req.onload = function() { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL("http://azu.github.io/promises-book/json/comment.json").then( JSON.parse ); }, people: function getPeople() { return getURL("http://azu.github.io/promises-book/json/people.json").then( JSON.parse ); } }; function main() { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); var tasks = [request.comment, request.people]; return tasks.reduce(function(promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve()); }
main() .then(function(value) { console.log(value); }) .catch(function(error) { console.error(error); });
|
Array.prototype.reduce
的第二个参数用来设置盛放计算结果的初始值。在这个例子中, Promise.resolve()
会赋值给 promise
,此时的 task
为 request.comment
。
在 reduce 中第一个参数中被 return
的值,则会被赋值为下次循环时的 promise
。也就是说,通过返回由 then
创建的新的 promise 对象,就实现了和 for 循环类似的 Promise chain 了。
使用 reduce 和 for 循环不同的地方是 reduce 不再需要临时变量 promise
了,因此也不用编写 promise = promise.then(task).then(pushValue);
这样冗长的代码了,这是非常大的进步。
1 2 3 4 5 6 7 8 9 10
| function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return tasks.reduce(function(promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve()); }
|
需要注意的一点是,和 Promise.all
等不同,这个函数接收的参数是一个函数的数组。为什么传给这个函数的不是一个 promise 对象的数组呢?这是因为 promise 对象创建的时候,XHR 已经开始执行了,因此再对这些 promise 对象进行顺序处理的话就不能正常工作了。
重写上例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return tasks.reduce(function(promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve()); } function getURL(URL) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open("GET", URL, true); req.onload = function() { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL("http://azu.github.io/promises-book/json/comment.json").then( JSON.parse ); }, people: function getPeople() { return getURL("http://azu.github.io/promises-book/json/people.json").then( JSON.parse ); } }; function main() { return sequenceTasks([request.comment, request.people]); }
main() .then(function(value) { console.log(value); }) .catch(function(error) { console.error(error); });
|
如何实现 Promise.all ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| Promise.all = function(promises) { return new Promise((resolve, reject) => { let index = 0; let result = []; if (promises.length === 0) { resolve(result); } else { function processValue(i, data) { result[i] = data; if (++index === promises.length) { resolve(result); } } for (let i = 0; i < promises.length; i++) { Promise.resolve(promises[i]).then( data => { processValue(i, data); }, err => { reject(err); return; } ); } } }); };
|
如何实现 Promise.finally ?
不管成功还是失败,都会走到 finally 中,并且 finally 之后,还可以继续 then。并且会将值原封不动的传递给后面的 then.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Promise.prototype.finally = function(callback) { return this.then( value => { return Promise.resolve(callback()).then(() => { return value; }); }, err => { return Promise.resolve(callback()).then(() => { throw err; }); } ); };
|
常见问题
输出结果:success
解题思路:Promise 状态一旦改变,无法在发生变更。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve("success"); reject("error"); }, 1000); }); promise.then( res => { console.log(res); }, err => { console.log(err); } );
|
输出结果:1
解题思路:Promise 的 then 方法的参数期望是函数,传入非函数则会发生值穿透。
1 2 3 4
| Promise.resolve(1) .then(2) .then(Promise.resolve(3)) .then(console.log);
|
结果是:3 4 6 8 7 5 2 1
优先级关系如下:process.nextTick > promise.then > setTimeout > setImmediate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| setImmediate(function() { console.log(1); }, 0); setTimeout(function() { console.log(2); }, 0); new Promise(function(resolve) { console.log(3); resolve(); console.log(4); }).then(function() { console.log(5); }); console.log(6); process.nextTick(function() { console.log(7); }); console.log(8);
|
V8 实现中,两个队列各包含不同的任务:
macrotasks: script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver
实现一个简单的 Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function Promise(fn) { var status = "pending"; function successNotify() { status = "fulfilled"; toDoThen.apply(undefined, arguments); } function failNotify() { status = "rejected"; toDoThen.apply(undefined, arguments); } function toDoThen() { setTimeout(() => { if (status === "fulfilled") { for (let i = 0; i < successArray.length; i++) { successArray[i].apply(undefined, arguments); } } else if (status === "rejected") { for (let i = 0; i < failArray.length; i++) { failArray[i].apply(undefined, arguments); } } }); } var successArray = []; var failArray = []; fn.call(undefined, successNotify, failNotify); return { then: function(successFn, failFn) { successArray.push(successFn); failArray.push(failFn); return undefined; } }; }
|
解题思路:Promise 中的 resolve 和 reject 用于改变 Promise 的状态和传参,then 中的参数必须是作为回调执行的函数。因此,当 Promise 改变状态之后会调用回调函数,根据状态的不同选择需要执行的回调函数。