if (typeof Promise === 'undefined') {
return
}

实现 Promise/A+ 规范的库有很多,lie 是一个精简的实现 Promise/A+ 的库,并且通过了 Promise/A+ 专门的测试集,但 lie 的代码写的有点绕,我在 lie 的代码基础上进行了修改,使之更容易阅读和理解,并发布了 appoint 模块供大家参考。

Promise/A+ 规范

Promise 规范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+,有兴趣的可以去了解下,最终 ES6 中采用了 Promise/A+ 规范。在讲解 Promise 实现之前,当然要先了解 Promise/A+ 规范。Promise/A+ 规范参考:

注意:没有特殊说明以下 promise 均指代 Promise 实例。

规范虽然不长,但细节也比较多,我挑出几个要点简单说明下:

1、Promise 本质是一个状态机。每个 promise 只能是 3 种状态中的一种:pending、fulfilled 或 rejected。状态转变只能是 pending -> fulfilled 或者 pending -> rejected。状态转变不可逆。

2、then 方法可以被同一个 promise 调用多次。

3、then 方法必须返回一个 promise。规范里没有明确说明返回一个新的 promise 还是复用老的 promise(即 return this),大多数实现都是返回一个新的 promise,而且复用老的 promise 可能改变内部状态,这与规范也是相违背的。

4、值穿透。下面会细讲。

从头实现 Promise

我们知道 Promise 是一个构造函数,需要用 new 调用,并有以下几个 api:

function Promise(resolver) {}

Promise.prototype.then = function() {}
Promise.prototype.catch = function() {} Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}

下面我们以 appoint 为最终目标,开始一步一步构建完整的 Promise 实现。

'use strict';

var immediate = require('immediate');

function INTERNAL() {}
function isFunction(func) {
return typeof func === 'function';
}
function isObject(obj) {
return typeof obj === 'object';
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
} var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2; module.exports = Promise; function Promise(resolver) {
if (!isFunction(resolver)) {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.value = void 0;
this.queue = [];
if (resolver !== INTERNAL) {
safelyResolveThen(this, resolver);
}
}

immediate 是一个将同步转异步执行的库。INTERNAL 就是一个空函数,类似于一些代码库中的 noop。定义了 3 个辅助函数:isFunction、isObject 和 isArray。定义了 3 种状态:PENDING、FULFILLED 和 REJECTED。safelyResolveThen 后面讲。promise 内部有三个变量:

1、state: 当前 promise 的状态,初始值为 PENDING。状态改变只能是 PENDING -> FULFILLED 或 PENDING -> REJECTED。

2、value: 当 state 是 FULFILLED 时存储返回值,当 state 是 REJECTED 时存储错误。

3、queue: promise 内部的回调队列,这是个什么玩意儿?为什么是一个数组?

Promise 实现基本原理

先看一段代码:

var Promise = require('appoint')
var promise = new Promise((resolve) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
var a = promise.then(function onSuccess() {})
var b = promise.catch(function onError() {})
console.dir(promise, { depth: 10 })
console.log(promise.queue[0].promise === a)
console.log(promise.queue[1].promise === b)

打印出:

Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise: Promise { state: 0, value: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] },
QueueItem {
promise: Promise { state: 0, value: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] } ] }
true
true

可以看出,queue 数组中有两个对象。因为规范中规定:then 方法可以被同一个 promise 调用多次。上例中在调用 .then 和 .catch 时 promise 并没有被 resolve,所以将 .then 和 .catch 生成的新 promise(a 和 b) 和正确时的回调(onSuccess 包装成 callFulfilled)和错误时的回调(onError 包装成 callRejected)生成一个 QueueItem 实例并 push 到 queue 数组里,所以上面两个 console.log 打印 true。当 promise 状态改变时遍历内部 queue 数组,统一执行成功(FULFILLED -> callFulfilled)或失败(REJECTED -> callRejected)的回调(传入 promise 的 value 值),生成的结果分别设置 a 和 b 的 state 和 value,这就是 Promise 实现的基本原理。
再来看另一个例子:

var Promise = require('appoint')
var promise = new Promise((resolve) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
promise
.then(() => {})
.then(() => {})
.then(() => {})
console.dir(promise, { depth: 10 })

打印出:

Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise:
Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise:
Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise: Promise { state: 0, value: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] } ] },
callFulfilled: [Function],
callRejected: [Function] } ] },
callFulfilled: [Function],
callRejected: [Function] } ] }

调用了 3 次 then,每个 then 将它生成的 promise 放到了调用它的 promise 队列里,形成了 3 层调用关系。当最外层的 promise 状态改变时,遍历它的 queue 数组调用对应的回调,设置子 promise 的 state 和 value 并遍历它的 queue 数组调用对应的回调,然后设置孙 promise 的 state 和 value 并遍历它的 queue 数组调用对应的回调......依次类推。

safelyResolveThen

function safelyResolveThen(self, then) {
var called = false;
try {
then(function (value) {
if (called) {
return;
}
called = true;
doResolve(self, value);
}, function (error) {
if (called) {
return;
}
called = true;
doReject(self, error);
});
} catch (error) {
if (called) {
return;
}
called = true;
doReject(self, error);
}
}

safelyResolveThen 顾名思义用来『安全的执行 then 函数』,这里的 then 函数指『第一个参数是 resolve 函数第二个参数是 reject 函数的函数』,如下两种情况:

1、构造函数的参数,即这里的 resolver:

new Promise(function resolver(resolve, reject) {
setTimeout(() => {
resolve('haha')
}, 1000)
})

2、promise 的 then:

promise.then(resolve, reject)

safelyResolveThen 有 3 个作用:

1、try...catch 捕获抛出的异常,如:

new Promise(function resolver(resolve, reject) {
throw new Error('Oops')
})

2、called 控制 resolve 或 reject 只执行一次,多次调用没有任何作用。即:

var Promise = require('appoint')
var promise = new Promise(function resolver(resolve, reject) {
setTimeout(() => {
resolve('haha')
}, 1000)
reject('error')
})
promise.then(console.log)
promise.catch(console.error)

打印 error,不会再打印 haha。

3、没有错误则执行 doResolve,有错误则执行 doReject。

doResolve 和 doReject

function doResolve(self, value) {
try {
var then = getThen(value);
if (then) {
safelyResolveThen(self, then);
} else {
self.state = FULFILLED;
self.value = value;
self.queue.forEach(function (queueItem) {
queueItem.callFulfilled(value);
});
}
return self;
} catch (error) {
return doReject(self, error);
}
} function doReject(self, error) {
self.state = REJECTED;
self.value = error;
self.queue.forEach(function (queueItem) {
queueItem.callRejected(error);
});
return self;
}

doReject 就是设置 promise 的 state 为 REJECTED,value 为 error,callRejected 如前面提到的通知子 promise:『我这里出了点问题呀』然后子 promise 根据传入的错误设置自己的状态和值。doResolve 结合 safelyResolveThen 使用不断地解包 promise,直至返回值是非 promise 对象后,设置 promise 的状态和值,然后通知子 promise:『我这里有值了哟』然后子 promise 根据传入的值设置自己的状态和值。

这里有个辅助函数 getThen:

function getThen(obj) {
var then = obj && obj.then;
if (obj && (isObject(obj) || isFunction(obj)) && isFunction(then)) {
return function appyThen() {
then.apply(obj, arguments);
};
}
}

规范中规定:如果 then 是函数,将 x(这里是 obj) 作为函数的 this 调用。

Promise.prototype.then 和 Promise.prototype.catch

Promise.prototype.then = function (onFulfilled, onRejected) {
if (!isFunction(onFulfilled) && this.state === FULFILLED ||
!isFunction(onRejected) && this.state === REJECTED) {
return this;
}
var promise = new this.constructor(INTERNAL);
if (this.state !== PENDING) {
var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
unwrap(promise, resolver, this.value);
} else {
this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
}
return promise;
}; Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};

上述代码中的 return this 实现了值穿透,后面会讲。可以看出,then 方法中生成了一个新的 promise 然后返回,符合规范要求。如果 promise 的状态改变了,则调用 unwrap,否则将生成的 promise 加入到当前 promise 的回调队列 queue 里,之前讲解了如何消费 queue。有 3 点需要讲解:

1、Promise 构造函数传入了一个 INTERNAL 即空函数,因为这个新产生的 promise 可以认为是内部的 promise,需要根据外部的 promise 的状态和值产生自身的状态和值,不需要传入回调函数,而外部 Promise 需要传入回调函数决定它的状态和值。所以之前 Promise 的构造函数里做了判断区分外部调用还是内部调用:

if (resolver !== INTERNAL) {
safelyResolveThen(this, resolver);
}

2、unwrap 代码如下:

function unwrap(promise, func, value) {
immediate(function () {
var returnValue;
try {
returnValue = func(value);
} catch (error) {
return doReject(promise, error);
}
if (returnValue === promise) {
doReject(promise, new TypeError('Cannot resolve promise with itself'));
} else {
doResolve(promise, returnValue);
}
});
}

从名字也可以理解是用来解包(即执行函数)的,第一个参数是子 promise,第二个参数是父 promise 的 then 的回调(onFulfilled/onRejected),第三个参数是父 promise 的值(正常值/错误)。

有 3 点需要说明:

2.1、使用 immediate 将同步代码变异步。如:

var Promise = require('appoint')
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
promise.then(() => {
promise.then(() => {
console.log('1')
})
console.log('2')
})

打印 2 1,去掉 immediate 则打印 1 2。

2.2、try...catch 用来捕获 then/catch 内抛出的异常,并调用 doReject,如:

promise.then(() => {
throw new Error('haha')
})
promise.catch(() => {
throw new Error('haha')
})

2.3、返回的值不能是 promise 本身,否则会造成死循环,如 node@4.6.0 下运行:

var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
var a = promise.then(() => {
return a
}) a.catch(console.log)// [TypeError: Chaining cycle detected for promise #<Promise>]

3、QueueItem 代码如下:

function QueueItem(promise, onFulfilled, onRejected) {
this.promise = promise;
this.callFulfilled = function (value) {
doResolve(this.promise, value);
};
this.callRejected = function (error) {
doReject(this.promise, error);
};
if (isFunction(onFulfilled)) {
this.callFulfilled = function (value) {
unwrap(this.promise, onFulfilled, value);
};
}
if (isFunction(onRejected)) {
this.callRejected = function (error) {
unwrap(this.promise, onRejected, error);
};
}
}

promise 为 then 生成的新 promise(以下称为『子promise』),onFulfilled 和 onRejected 即是 then 参数中的 onFulfilled 和 onRejected。从上面代码可以看出:比如当 promise 状态变为 FULFILLED 时,之前注册的 then 函数,用 callFulfilled 调用 unwrap 进行解包最终得出子 promise 的状态和值,之前注册的 catch 函数,用 callFulfilled 直接调用 doResolve,设置队列里子 promise 的状态和值。当 promise 状态变为 REJECTED 类似。

注意:promise.catch(onRejected) 就是 promise.then(null, onRejected) 的语法糖。

至此,Promise 的核心实现都完成了。

值穿透

Promise.prototype.then = function (onFulfilled, onRejected) {
if (!isFunction(onFulfilled) && this.state === FULFILLED ||
!isFunction(onRejected) && this.state === REJECTED) {
return this;
}
...
};

上面提到了值穿透问题,值穿透即:

var Promise = require('appoint')
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
promise
.then('hehe')
.then(console.log)

最终打印 haha 而不是 hehe。

通过 return this 只实现了值穿透的一种情况,其实值穿透有两种情况:

1、promise 已经是 FULFILLED/REJECTED 时,通过 return this 实现的值穿透:

var Promise = require('appoint')
var promise = new Promise(function (resolve) {
setTimeout(() => {
resolve('haha')
}, 1000)
})
promise.then(() => {
promise.then().then((res) => {// ①
console.log(res)// haha
})
promise.catch().then((res) => {// ②
console.log(res)// haha
})
console.log(promise.then() === promise.catch())// true
console.log(promise.then(1) === promise.catch({ name: 'nswbmw' }))// true
})

上述代码①②处 promise 已经是 FULFILLED 了符合条件所以执行了 return this。注意:原生的 Promise 实现里并不是这样实现的,所以会打印两个 false。

2、promise 是 PENDING 时,通过生成新的 promise 加入到父 promise 的 queue,父 promise 有值时调用 callFulfilled->doResolve 或 callRejected->doReject(因为 then/catch 传入的参数不是函数)设置子 promise 的状态和值为父 promise 的状态和值。如:

var Promise = require('appoint')
var promise = new Promise((resolve) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
var a = promise.then()
a.then((res) => {
console.log(res)// haha
})
var b = promise.catch()
b.then((res) => {
console.log(res)// haha
})
console.log(a === b)// false

Promise.resolve 和 Promise.reject

Promise.resolve = resolve;
function resolve(value) {
if (value instanceof this) {
return value;
}
return doResolve(new this(INTERNAL), value);
} Promise.reject = reject;
function reject(reason) {
var promise = new this(INTERNAL);
return doReject(promise, reason);
}

当 Promise.resolve 参数是一个 promise 时,直接返回该值。

Promise.all

Promise.all = all;
function all(iterable) {
var self = this;
if (!isArray(iterable)) {
return this.reject(new TypeError('must be an array'));
} var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
} var values = new Array(len);
var resolved = 0;
var i = -1;
var promise = new this(INTERNAL); while (++i < len) {
allResolver(iterable[i], i);
}
return promise;
function allResolver(value, i) {
self.resolve(value).then(resolveFromAll, function (error) {
if (!called) {
called = true;
doReject(promise, error);
}
});
function resolveFromAll(outValue) {
values[i] = outValue;
if (++resolved === len && !called) {
called = true;
doResolve(promise, values);
}
}
}
}

Promise.all 用来并行执行多个 promise/值,当所有 promise/值执行完毕后或有一个发生错误时返回。可以看出:

1、Promise.all 内部生成了一个新的 promise 返回。

2、called 用来控制即使有多个 promise reject 也只有第一个生效。

3、values 用来存储结果。

4、当最后一个 promise 得出结果后,使用 doResolve(promise, values) 设置 promise 的 state 为 FULFILLED,value 为结果数组 values。

Promise.race

Promise.race = race;
function race(iterable) {
var self = this;
if (!isArray(iterable)) {
return this.reject(new TypeError('must be an array'));
} var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
} var i = -1;
var promise = new this(INTERNAL); while (++i < len) {
resolver(iterable[i]);
}
return promise;
function resolver(value) {
self.resolve(value).then(function (response) {
if (!called) {
called = true;
doResolve(promise, response);
}
}, function (error) {
if (!called) {
called = true;
doReject(promise, error);
}
});
}
}
Promise.race 接受一个数组,当数组中有一个 resolve 或 reject 时返回。跟 Promise.all 代码相近,只不过这里用 called 控制只要有任何一个 promise onFulfilled/onRejected 立即去设置 promise 的状态和值。

至此,Promise 的实现全部讲解完毕。

转自:https://zhuanlan.zhihu.com/p/25178630

Google大牛分享的面试秘籍的更多相关文章

  1. Steve Yegge:Google面试秘籍

    我憋了很长时间想写点关于去Google面试的秘籍.不过我总是推迟,因为写出来的东西会让你抓狂.很可能是这样.如果按统计规律来定义"你"的话,这文章很可能让你不爽. 为啥呢?因为啊- ...

  2. 中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂(上)

    前言 当下,正面临着近几年来的最严重的互联网寒冬,听得最多的一句话便是:相见于江湖~.缩减HC.裁员不绝于耳,大家都是人心惶惶,年前如此,年后想必肯定又是一场更为惨烈的江湖厮杀.但博主始终相信,寒冬之 ...

  3. 51Testing丛书新作《软件测试工程师面试秘籍》

    51Testing又有好消息告诉小伙伴了!51Testing软件测试网作品系列重磅推出全新丛书<软件测试工程师面试秘籍> 此次我们邀请到知名互联网企业测试专家李江(G.li),整理并撰写软 ...

  4. 转:google测试分享-GTA

    原文: http://blog.sina.com.cn/s/blog_6cf812be0102viuh.html 上一次分享了google测试分享-分层测试,有很多自动化测试的策略和实施都要有一个重点 ...

  5. 如何进BAT,有了这个篇面试秘籍,成功率高达80%!!(附资料)

    多年前自己刚来北京找工作的时候,面了一个星期 面了七八家公司才拿到一个offer.而上次跳槽面了不到10家公司基本全过而且都给到了期望的薪资,本来自己在面试前没想到能够这么顺利,回想起来还是自己准备的 ...

  6. Google测试分享-测试经理

    首先大家可以思考下,google的测试经理角色必须具备什么样的能力.据不完全统计,google的测试经理,超过一半之前都做过TE的角色(大家可以想想为啥):对于与被测产品相关的任何使用问题,测试经理都 ...

  7. 想成为一个高效的Web开发者吗?来看看大牛分享的经验吧~ #精选JAVASCRIPT前端开发

    想成为一个高效的Web开发者吗?来看看大牛分享的经验吧~ 作为一个软(ku)件(bi)工(de)程(ma)师(nong),你有没有觉得做什么事都没时间?没时间学习新东西,没时间去回顾.整理原来写的烂代 ...

  8. 转:google测试分享-问题和挑战

    原文: http://blog.sina.com.cn/s/blog_6cf812be0102vxer.html 前言:这个系列分享的内容大部分都是出自于<google是如何测试的>的书, ...

  9. 转:google测试分享-测试经理

    原文: http://blog.sina.com.cn/s/blog_6cf812be0102vode.html 前言:这个系列分享的内容大部分都是出自于<google是如何测试的>的书, ...

随机推荐

  1. Discuz上传错误

    换了服务器后,上传图片的时候,显示上传100%,然后报错:upload error: 500.怎么回事那? [解决方法] 原来是php上传文件的时候,会首先上传到一个临时目录.如果临时目录没有权限,就 ...

  2. Discuz 学习笔记一 :getgdc 和get_client_ip

    Getgdc函数 discuz有一个超级变量的自定义函数:   function getgpc($k, $type='GP') {     $type = strtoupper($type);     ...

  3. 【PHP 】 伪静态 - 3. 伪静态的基本使用

    原理图: 原先浏览器输入的网址会发送到apache服务器,然后apache会调用php模块来处理,最后找到你所想访问的页面; 如果在apahce, httpd.conf文件中开启rewrite机制,则 ...

  4. Appium Python 四:怎样获取APP的Package以及Activity

    看到一篇很好的博客:[Android测试][随笔]获得App的包名和启动页Activity 除了博客上的方法,我还找到两种方法: 方法一:aapt 前提需要使用SDK Manager.exe 下载 A ...

  5. 带"叉叉"的GridView

    由于需要用到“删除图片”的功能,需要写这样一个小demo: 对之前博文的修改 发现imageView监听点击事件 效果实在不敢恭维,因此换个方式:设置Touch的监听函数, 下面的Demo没有改过来哈 ...

  6. php之快速入门学习-4(数据类型)

    PHP 5 数据类型 String(字符串), Integer(整型), Float(浮点型), Boolean(布尔型), Array(数组), Object(对象), NULL(空值). PHP ...

  7. VB中如何修改treeview的背景色

    改变 TreeView 的背景    Private Declare Function SendMessage Lib "User32" Alias "SendMessa ...

  8. POSIX 线程详解

    一种支持内存共享的简捷工具 POSIX(可移植操作系统接口)线程是提高代码响应和性能的有力手段.在本系列中,Daniel Robbins 向您精确地展示在编程中如何使用线程.其中还涉及大量幕后细节,读 ...

  9. JVM:垃圾回收机制和调优手段

    转载请注明出处: jiq•钦's technical Blog - 季义钦 引言: 我们都知道JVM内存由几个部分组成:堆.方法区.栈.程序计数器.本地方法栈 JVM垃圾回收仅针对公共内存区域即:堆和 ...

  10. 纯jascript解决手机端拍照、选图后图片被旋转问题

    需要的js1 需要的js2 这里主要用到Orientation属性. Orientation属性说明如下: 旋转角度 参数 0° 1 顺时针90° 6 逆时针90° 8 180° 3 <!DOC ...