Skip to content

Instantly share code, notes, and snippets.

@JonasGao
Last active February 25, 2017 15:58
Show Gist options
  • Save JonasGao/262bf845cafa04256a7a9a1ddc8e290d to your computer and use it in GitHub Desktop.
Save JonasGao/262bf845cafa04256a7a9a1ddc8e290d to your computer and use it in GitHub Desktop.
angular 1.x $q service 的学习记录,笔记就在 $q 的源代码文件中。后面两个 html 是实验文件
'use strict';
/**
* @ngdoc service
* @name $q
* @requires $rootScope
*
* @description
* A service that helps you run functions asynchronously, and use their return values (or exceptions)
* when they are done processing.
*
* This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred
* objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
* implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
* # $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
* function as the first argument. This is similar to the native Promise implementation from ES6,
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
*
* While the constructor-style use is supported, not all of the supporting methods from ES6 promises are
* available yet.
*
* It can be used like so:
*
* ```js
* // for the purpose of this example let's assume that variables `$q` and `okToGreet`
* // are available in the current lexical scope (they could have been injected or passed in).
*
* function asyncGreet(name) {
* // perform some asynchronous operation, resolve or reject the promise when appropriate.
* return $q(function(resolve, reject) {
* setTimeout(function() {
* if (okToGreet(name)) {
* resolve('Hello, ' + name + '!');
* } else {
* reject('Greeting ' + name + ' is not allowed.');
* }
* }, 1000);
* });
* }
*
* var promise = asyncGreet('Robin Hood');
* promise.then(function(greeting) {
* alert('Success: ' + greeting);
* }, function(reason) {
* alert('Failed: ' + reason);
* });
* ```
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
* Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise.
*
* However, the more traditional CommonJS-style usage is still available, and documented below.
*
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
* interface for interacting with an object that represents the result of an action that is
* performed asynchronously, and may or may not be finished at any given point in time.
*
* From the perspective of dealing with error handling, deferred and promise APIs are to
* asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming.
*
* ```js
* // for the purpose of this example let's assume that variables `$q` and `okToGreet`
* // are available in the current lexical scope (they could have been injected or passed in).
*
* function asyncGreet(name) {
* var deferred = $q.defer();
*
* setTimeout(function() {
* deferred.notify('About to greet ' + name + '.');
*
* if (okToGreet(name)) {
* deferred.resolve('Hello, ' + name + '!');
* } else {
* deferred.reject('Greeting ' + name + ' is not allowed.');
* }
* }, 1000);
*
* return deferred.promise;
* }
*
* var promise = asyncGreet('Robin Hood');
* promise.then(function(greeting) {
* alert('Success: ' + greeting);
* }, function(reason) {
* alert('Failed: ' + reason);
* }, function(update) {
* alert('Got notification: ' + update);
* });
* ```
*
* At first it might not be obvious why this extra complexity is worth the trouble. The payoff
* comes in the way of guarantees that promise and deferred APIs make, see
* https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md.
*
* Additionally the promise api allows for composition that is very hard to do with the
* traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach.
* For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
* section on serial or parallel joining of promises.
*
* # The Deferred API
*
* A new instance of deferred is constructed by calling `$q.defer()`.
*
* The purpose of the deferred object is to expose the associated Promise instance as well as APIs
* that can be used for signaling the successful or unsuccessful completion, as well as the status
* of the task.
*
* **Methods**
*
* - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection
* constructed via `$q.reject`, the promise will be rejected instead.
* - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to
* resolving it with a rejection constructed via `$q.reject`.
* - `notify(value)` - provides updates on the status of the promise's execution. This may be called
* multiple times before the promise is either resolved or rejected.
*
* **Properties**
*
* - promise – `{Promise}` – promise object associated with this deferred.
*
*
* # The Promise API
*
* A new promise instance is created when a deferred instance is created and can be retrieved by
* calling `deferred.promise`.
*
* The purpose of the promise object is to allow for interested parties to get access to the result
* of the deferred task when it completes.
*
* **Methods**
*
* - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or
* will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously
* as soon as the result is available. The callbacks are called with a single argument: the result
* or rejection reason. Additionally, the notify callback may be called zero or more times to
* provide a progress indication, before the promise is resolved or rejected.
*
* This method *returns a new promise* which is resolved or rejected via the return value of the
* `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved
* with the value which is resolved in that promise using
* [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
* It also notifies via the return value of the `notifyCallback` method. The promise cannot be
* resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback
* arguments are optional.
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
* - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise,
* but to do so without modifying the final value. This is useful to release resources or do some
* clean-up that needs to be done whether the promise was rejected or resolved. See the [full
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
* # Chaining promises
*
* Because calling the `then` method of a promise returns a new derived promise, it is easily
* possible to create a chain of promises:
*
* ```js
* promiseB = promiseA.then(function(result) {
* return result + 1;
* });
*
* // promiseB will be resolved immediately after promiseA is resolved and its value
* // will be the result of promiseA incremented by 1
* ```
*
* It is possible to create chains of any length and since a promise can be resolved with another
* promise (which will defer its resolution further), it is possible to pause/defer resolution of
* the promises at any point in the chain. This makes it possible to implement powerful APIs like
* $http's response interceptors.
*
*
* # Differences between Kris Kowal's Q and $q
*
* There are two main differences:
*
* - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation
* mechanism in AngularJS, which means faster propagation of resolution or rejection into your
* models and avoiding unnecessary browser repaints, which would result in flickering UI.
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
* # Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
* var deferred = $q.defer();
* var promise = deferred.promise;
* var resolvedValue;
*
* promise.then(function(value) { resolvedValue = value; });
* expect(resolvedValue).toBeUndefined();
*
* // Simulate resolving of promise
* deferred.resolve(123);
* // Note that the 'then' function does not get called synchronously.
* // This is because we want the promise API to always be async, whether or not
* // it got called synchronously or asynchronously.
* expect(resolvedValue).toBeUndefined();
*
* // Propagate promise resolution to 'then' functions using $apply().
* $rootScope.$apply();
* expect(resolvedValue).toEqual(123);
* }));
* ```
*
* @param {function(function, function)} resolver Function which is responsible for resolving or
* rejecting the newly created promise. The first parameter is a function which resolves the
* promise, the second parameter is a function which rejects the promise.
*
* @returns {Promise} The newly created promise.
*/
/**
* @ngdoc provider
* @name $qProvider
* @this
*
* @description
*/
function $QProvider() {
var errorOnUnhandledRejections = true;
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
return qFactory(function(callback) {
$rootScope.$evalAsync(callback);
}, $exceptionHandler, errorOnUnhandledRejections);
}];
/**
* @ngdoc method
* @name $qProvider#errorOnUnhandledRejections
* @kind function
*
* @description
* Retrieves or overrides whether to generate an error when a rejected promise is not handled.
* This feature is enabled by default.
*
* @param {boolean=} value Whether to generate an error when a rejected promise is not handled.
* @returns {boolean|ng.$qProvider} Current value when called without a new value or self for
* chaining otherwise.
*/
this.errorOnUnhandledRejections = function(value) {
if (isDefined(value)) {
errorOnUnhandledRejections = value;
return this;
} else {
return errorOnUnhandledRejections;
}
};
}
/** @this */
function $$QProvider() {
var errorOnUnhandledRejections = true;
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
return qFactory(function(callback) {
$browser.defer(callback);
}, $exceptionHandler, errorOnUnhandledRejections);
}];
this.errorOnUnhandledRejections = function(value) {
if (isDefined(value)) {
errorOnUnhandledRejections = value;
return this;
} else {
return errorOnUnhandledRejections;
}
};
}
/**
* Constructs a promise manager.
*
* @param {function(function)} nextTick Function for executing functions in the next turn.
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
* debugging purposes.
@ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled
* promises rejections.
* @returns {object} Promise manager.
*/
function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
var $qMinErr = minErr('$q', TypeError);
var queueSize = 0;
var checkQueue = [];
/**
* @ngdoc method
* @name ng.$q#defer
* @kind function
*
* @description
* Creates a `Deferred` object which represents a task which will finish in the future.
*
* @returns {Deferred} Returns a new instance of deferred.
*/
function defer() {
return new Deferred();
}
function Deferred() {
var promise = this.promise = new Promise();
//Non prototype methods necessary to support unbound execution :/
this.resolve = function(val) { resolvePromise(promise, val); };
this.reject = function(reason) { rejectPromise(promise, reason); };
this.notify = function(progress) { notifyPromise(promise, progress); };
}
function Promise() {
this.$$state = { status: 0 };
}
extend(Promise.prototype, {
then: function(onFulfilled, onRejected, progressBack) {
if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
return this;
}
var result = new Promise();
this.$$state.pending = this.$$state.pending || [];
this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
// Promise 的 then 永远返回的都是 Promise
// 参考 $$resolve 中的代码可知,这里虽然每次都返回 Promise
// 但是这里的上下文 this 却始终是最初的 Promise
return result;
},
'catch': function(callback) {
return this.then(null, callback);
},
'finally': function(callback, progressBack) {
return this.then(function(value) {
return handleCallback(value, resolve, callback);
}, function(error) {
return handleCallback(error, reject, callback);
}, progressBack);
}
});
function processQueue(state) {
var fn, promise, pending;
pending = state.pending;
state.processScheduled = false;
state.pending = undefined;
try {
for (var i = 0, ii = pending.length; i < ii; ++i) {
state.pur = true;
promise = pending[i][0];
fn = pending[i][state.status];
try {
if (isFunction(fn)) {
resolvePromise(promise, fn(state.value));
} else if (state.status === 1) {
resolvePromise(promise, state.value);
} else {
rejectPromise(promise, state.value);
}
} catch (e) {
rejectPromise(promise, e);
}
}
} finally {
--queueSize;
if (errorOnUnhandledRejections && queueSize === 0) {
nextTick(processChecks);
}
}
}
function processChecks() {
// eslint-disable-next-line no-unmodified-loop-condition
while (!queueSize && checkQueue.length) {
var toCheck = checkQueue.shift();
if (!toCheck.pur) {
toCheck.pur = true;
var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
if (toCheck.value instanceof Error) {
exceptionHandler(toCheck.value, errorMessage);
} else {
exceptionHandler(errorMessage);
}
}
}
}
function scheduleProcessQueue(state) {
// 这里,是从 $$resolve 回来的,先去看 $$resolve
// 这里检查,errorOnUnhandledRejections 表示是否在未注册 reject 的时候报错
// state.pending 是否有值,因为 pending 都是在 then 中初始化和追加的,也就是 pending 包含了所有用户生成的 then chain
// 如果 pending 不存在,自然也就没必要进行下一步了
// status == 2 用来判断,如果当前状态是 rejected
// state.pur 暂时未知,暂且放放
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) {
if (queueSize === 0 && checkQueue.length === 0) {
nextTick(processChecks);
}
checkQueue.push(state);
}
if (state.processScheduled || !state.pending) return;
state.processScheduled = true;
++queueSize;
nextTick(function() { processQueue(state); });
}
function resolvePromise(promise, val) {
// 检查 promise 的初始化状态
// 初始化状态应该是 0,那么如果不为 0,说明状态已经变更,则不再处理
if (promise.$$state.status) return;
if (val === promise) {
$$reject(promise, $qMinErr(
'qcycle',
'Expected promise to be resolved with value other than itself \'{0}\'',
val));
} else {
// 一切正常的话,会从这里开始
$$resolve(promise, val);
}
}
function $$resolve(promise, val) {
var then;
var done = false;
try {
// 这一步尝试获取返回值的 then
// 如果在上一个 Promise 的 then 中返回自定义的对象,比如 { then: () => console.log('Hi') }
// 那么这里就调用你传递的自定义的 then
// 否则这里的 then 就是 Promise 类型上的 then (跳转到代码 320 - 332 行,内容可能因新追加的注释,导致行数不正确)
if (isObject(val) || isFunction(val)) then = val.then;
if (isFunction(then)) {
// 如果还真是 then, 那就自动调用下一步
// 并且标记当前 Promise 的状态为 -1
promise.$$state.status = -1;
// 并且会传递 doResolve, doReject, doNotify 三个函数
// 如果这里 val 是一个模拟的对象,则可以通过这里改变当前 Promise 的状态
then.call(val, doResolve, doReject, doNotify);
} else {
// 0 可以说是初始化状态
// 上一段代码中的 -1 可以说是不确定状态
// 因为 doResolve 还是 doReject,都传递给了下一级 Promise
// 而当前的 Promise 会通过 call 绑定传递到下一级的 then
// 所以 -1 是不确定的状态,要交给下一级 Promise 来确定
promise.$$state.value = val;
promise.$$state.status = 1;
// 上两句代码确定了值和状态后
// 下一句就是真正开始出发 then chain 的函数
scheduleProcessQueue(promise.$$state);
}
} catch (e) {
doReject(e);
}
// 下面的 done 可以说是用来判断当前 promise 是否已经被处理过状态的 flag
function doResolve(val) {
if (done) return;
done = true;
$$resolve(promise, val);
}
function doReject(val) {
if (done) return;
done = true;
$$reject(promise, val);
}
function doNotify(progress) {
notifyPromise(promise, progress);
}
}
function rejectPromise(promise, reason) {
if (promise.$$state.status) return;
$$reject(promise, reason);
}
function $$reject(promise, reason) {
promise.$$state.value = reason;
promise.$$state.status = 2;
scheduleProcessQueue(promise.$$state);
}
function notifyPromise(promise, progress) {
var callbacks = promise.$$state.pending;
if ((promise.$$state.status <= 0) && callbacks && callbacks.length) {
nextTick(function() {
var callback, result;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
result = callbacks[i][0];
callback = callbacks[i][3];
try {
notifyPromise(result, isFunction(callback) ? callback(progress) : progress);
} catch (e) {
exceptionHandler(e);
}
}
});
}
}
/**
* @ngdoc method
* @name $q#reject
* @kind function
*
* @description
* Creates a promise that is resolved as rejected with the specified `reason`. This api should be
* used to forward rejection in a chain of promises. If you are dealing with the last promise in
* a promise chain, you don't need to worry about it.
*
* When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of
* `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via
* a promise error callback and you want to forward the error to the promise derived from the
* current promise, you have to "rethrow" the error by returning a rejection constructed via
* `reject`.
*
* ```js
* promiseB = promiseA.then(function(result) {
* // success: do something and resolve promiseB
* // with the old or a new result
* return result;
* }, function(reason) {
* // error: handle the error if possible and
* // resolve promiseB with newPromiseOrValue,
* // otherwise forward the rejection to promiseB
* if (canHandle(reason)) {
* // handle the error and recover
* return newPromiseOrValue;
* }
* return $q.reject(reason);
* });
* ```
*
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
function reject(reason) {
var result = new Promise();
rejectPromise(result, reason);
return result;
}
function handleCallback(value, resolver, callback) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
} catch (e) {
return reject(e);
}
if (isPromiseLike(callbackOutput)) {
return callbackOutput.then(function() {
return resolver(value);
}, reject);
} else {
return resolver(value);
}
}
/**
* @ngdoc method
* @name $q#when
* @kind function
*
* @description
* Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise.
* This is useful when you are dealing with an object that might or might not be a promise, or if
* the promise comes from a source that can't be trusted.
*
* @param {*} value Value or a promise
* @param {Function=} successCallback
* @param {Function=} errorCallback
* @param {Function=} progressCallback
* @returns {Promise} Returns a promise of the passed value or promise
*/
function when(value, callback, errback, progressBack) {
var result = new Promise();
resolvePromise(result, value);
return result.then(callback, errback, progressBack);
}
/**
* @ngdoc method
* @name $q#resolve
* @kind function
*
* @description
* Alias of {@link ng.$q#when when} to maintain naming consistency with ES6.
*
* @param {*} value Value or a promise
* @param {Function=} successCallback
* @param {Function=} errorCallback
* @param {Function=} progressCallback
* @returns {Promise} Returns a promise of the passed value or promise
*/
var resolve = when;
/**
* @ngdoc method
* @name $q#all
* @kind function
*
* @description
* Combines multiple promises into a single promise that is resolved when all of the input
* promises are resolved.
*
* @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
* @returns {Promise} Returns a single promise that will be resolved with an array/hash of values,
* each value corresponding to the promise at the same index/key in the `promises` array/hash.
* If any of the promises is resolved with a rejection, this resulting promise will be rejected
* with the same rejection value.
*/
function all(promises) {
var result = new Promise(),
counter = 0,
results = isArray(promises) ? [] : {};
forEach(promises, function(promise, key) {
counter++;
when(promise).then(function(value) {
results[key] = value;
if (!(--counter)) resolvePromise(result, results);
}, function(reason) {
rejectPromise(result, reason);
});
});
if (counter === 0) {
resolvePromise(result, results);
}
return result;
}
/**
* @ngdoc method
* @name $q#race
* @kind function
*
* @description
* Returns a promise that resolves or rejects as soon as one of those promises
* resolves or rejects, with the value or reason from that promise.
*
* @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
* @returns {Promise} a promise that resolves or rejects as soon as one of the `promises`
* resolves or rejects, with the value or reason from that promise.
*/
function race(promises) {
var deferred = defer();
forEach(promises, function(promise) {
when(promise).then(deferred.resolve, deferred.reject);
});
return deferred.promise;
}
function $Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver);
}
var promise = new Promise();
function resolveFn(value) {
resolvePromise(promise, value);
}
function rejectFn(reason) {
rejectPromise(promise, reason);
}
resolver(resolveFn, rejectFn);
return promise;
}
// Let's make the instanceof operator work for promises, so that
// `new $q(fn) instanceof $q` would evaluate to true.
$Q.prototype = Promise.prototype;
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
$Q.resolve = resolve;
$Q.all = all;
$Q.race = race;
return $Q;
}
<html>
<body ng-app="test" ng-controller="testController">
<button ng-click="doTest()">测试</button>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('test', []).controller('testController', function ($q, $scope) {
$scope.$watch(function() {
console.log( "Such $digest! Much triggered." );
});
$scope.doTest = function () {
let promise1 = $q(function(a1, b1) {
setTimeout(function() {
// a1(!console.log('promise 1 resolved') && $q(function(a2, b2) {
// setTimeout(function() {
// a2(!console.log('promise 2 resolved'));
// }, 1000);
// }).then(function () {
// console.log('promise 2 then', arguments);
// }));
a1(!console.log('promise 1 resolved object with then') && {
then: function (promise1Resolve2, promise1Reject) {
console.log(this, ' is custom object with then function. And, parent promise will be rejected');
promise1Reject('yes, you are rejected');
}
});
}, 1000);
});
promise1.then(function (message) {
console.log('Promise 1 resolved, i getted: ', message);
}, function (message) {
console.log('Promise 1 rejected, i getted: ', message);
});
};
});
</script>
</body>
</html>
<html>
<body ng-app="test" ng-controller="testController">
<button ng-click="doTest()">测试</button>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('test', []).controller('testController', function ($q, $scope, $timeout) {
$scope.$watch(function() {
console.log( "Such $digest! Much triggered." );
});
let deferred = $q.defer();
let promise = deferred.promise;
promise.then(function() {
console.log('promise 1 then 1');
return $timeout(function () {
console.log('promise 1 timeout 1');
}, 500);
});
promise.then(function() {
console.log('promise 1 then 2');
return $timeout(function () {
console.log('promise 1 timeout 2');
}, 500);
});
promise.then(function() {
console.log('promise 1 then 3');
return $timeout(function () {
console.log('promise 1 timeout 3');
}, 500);
});
$scope.doTest = function () {
deferred.resolve();
console.log('promise 1 resolved');
};
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment