大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说promise的all方法_promise实例的用法,希望您对编程的造诣更进一步.
引子
本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,建议先了解Promise的使用
本篇文章整体架构的大图如下,接下来会一步一步去实现一个 Promise
。
Promise 类
首先呢,promise肯定是一个类,同时还定义了 resolve
和 reject
方法。
function Promise(executor) {
// 初始化state为等待态
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
// 存放 fn1 的回调
this.fn1Callbacks = [];
// 存放 fn2 的回调
this.fn2Callbacks = [];
// 成功
let resolve = () => { };
// 失败
let reject = () => { };
// 立即执行
executor(resolve, reject);
}
上面的代码实现了Promise
构造函数的主体,但有两个问题:
-
executor
有可能会出错,对吧,毕竟是用户传进来的方法,类似下面这样。如果executor出错,报错我们需要用 try catch 捕获一下,Promise应该被其throw出的值reject:new Promise(function(resolve, reject) { console.log(a) // a 没有被定义 })
-
resolve
、reject
还是空函数,我们需要在里面补上逻辑。
接下来继续完善:
function Promise(executor){
// 初始化state为等待态
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
let resolve = value => {
// state改变,resolve调用就会失败
if (this.state === 'pending') {
// resolve调用后,state转化为成功态
this.state = 'fulfilled';
// 储存成功的值
this.value = value;
}
};
let reject = reason => {
// state改变,reject调用就会失败
if (this.state === 'pending') {
// reject调用后,state转化为失败态
this.state = 'rejected';
// 储存失败的原因
this.reason = reason;
}
};
// 如果executor执行报错,直接执行reject
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
用一张图小结一下:
上面的代码不算特别复杂,下面的then
方法有点复杂。
实现 then 方法
Promise
对象有一个then
方法,用来注册在这个Promise状态确定后的回调。当Promise
的状态发生了改变,不论是成功或是失败都会调用then
方法
then
方法使用方法如下:
// then 方法传入两个方法作为参数,一个是fn1方法,一个是 fn2 方法
p1.then(function fn1(data){
// fn1 方法的参数,用于获取promise对象的值
}, function fn2(err){
// fn1 方法的参数,用于获取失败的原因
})
从上面的例子,很明显,我们得出结论:
-
then
方法可以在p1
实例上调用。因此then
方法的实现是在Promise
的prototype
上。 -
then
方法会返回一个Promise
,而且是返回一个新的Promise(详情)对象。 -
可以多次调用
then
方法,也就是链式调用,并且每次会返回一个新Promise
对象,Promise
状态是不确定的,可能是fullfilled
, 也可能是resolve
, 取决于那一次调用then
时,fn1
的返回值。
所以,then
方法的实现也很简单,根据Promise
状态来调用不同的回调函数即可
下面是 then 方法的思路图:
下面我们来实现 then
方法:
// then方法接收两个参数,fn1,fn2,分别为Promise成功或失败后的回调
Promise.prototype.then = function(fn1, fn2) {
var self = this
var promise2
// 首先对入参 fn1, fn2做判断
fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
//todo
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
//todo
})
}
if (self.status === 'pending') {
return promise2 = new Promise(function(resolve, reject) {
// todo
})
}
}
首先,对入参 fn1
, fn2
做判断。规范中说,fn1
和 fn2
都是可选参数。
也就是说可以传也可以不传。传入的回调函数也不是一个函数类型,那怎么办?规范中说忽略它就好了。因此需要判断一下回调函数的类型,如果明确是个函数再执行它。
其次,Promise
总共有三种可能的状态,我们分三个if
块来处理,在里面分别都返回一个new Promise。
所以,接下来的逻辑是:
-
如果
promise
状态是resolved
,需要执行fn1
; -
如果
promise
状态是rejected
, 需要执行fn2
; -
如果
promise
状态是pending
, 我们并不能确定调用fn1
还是fn2
,只能先把方法都保存在fn1Callback
,fn2Callback
数组中。等到Promise的状态确定后再处理。
根据上面的逻辑,填充下面代码:
Promise.prototype.then = function(fn1, fn2) {
var self = this
var promise2
fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
// 把 fn1、fn2 放在 try catch 里面,毕竟 fn1、fn2 是用户传入的,报错嘛,很常见
try {
var x = fn1(self.data)
// fn1 执行后,会有返回值,通过 resolve 注入到 then 返回的 promise 中
resolve(x)
} catch (e) {
reject(e)
}
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
try {
var x = fn2(self.data)
reject(x)
} catch (e) {
reject(e)
}
})
}
if (self.status === 'pending') {
return promise2 = new Promise(function(resolve, reject) {
this.fn1Callback.push(function(value){
try {
var x = fn1(self.data);
resolve(x)
} catch (e) {
reject(e)
}
})
this.fn2Callback.push(function(value) {
try {
var x = fn2(self.data);
reject(x)
} catch (e) {
reject(e)
}
})
})
}
}
-
fn1
,fn2
都是用户传入的,有可能报错唉,所以要放在 try catch 里面 -
fn1
,fn2
的返回值,我们记为x
, 规范中的命名也是x
, 保持一致。x
值将在下文中频繁使用。
then
函数本质是把fn1
的返回值,包装成一个 promise
返回出去。问题是,fn1
的返回值是开发者写的,可能千奇百怪。上面代码中,假定 x
是一个普通值。其实实际上,x
有不同的情况,我们得去分别处理:
-
如果
x
是一个普通值,如同上面的代码,直接使用resolve
方法,then
就可以返回一个正常的promisereturn new Promise((resolve) => { var x = fn1(self.data); resolve(x) })
-
如果
x
是一个 promise ,需要等待这个promise
状态变化, 拿到fullfilled
的值。然后我们代码再改一改,增加一个判断return new Promise((resolve) => { var x = fn1(self.data); if (x instanceof Promise) { x.then((data) => {resolve(data)}, (e) => {reject(e)}) } else { resolve(x) } })
-
根据规定,我们需要兼容各种百花齐放的写法,比如说,如果
x
是一个对象,并且对象有then
方法,也就是所谓的thenable
对象,则我们得这样处理:return new Promise((resolve) => { var x = fn1(self.data); if (x instanceof Promise) { x.then((data) => {resolve(data)}, (e) => {reject(e)}) } else if (typeof x.then === 'function'){ x.then(function(y){ resolve(y) }, function(e){ reject(e) }) } else { resolve(x) } })
上面,我们新增了一些逻辑,为了处理 x 返回值 的各种情况。我们需要把这些逻辑,挪到一个 resolvePromise
方法中,resolvePromise
负责把 各种稀奇古怪的 x
包装成一个正常的 promise
。
resolvePromise
resolvePromise
方法,就是为了把 x
包裹成一个正常的promise
function resolvePromise(promise2, x, resolve, reject) {
// 为了防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'));
}
// 如果 x 是 promise
if (x instanceof Promise) {
x.then(function (data) {
resolve(data)
}, function (e) {
reject(e)
});
return;
}
// 如果 x 是 object 类型或者是 function
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
// 拿x.then可能会报错
try {
// 先拿到 x.then
var then = x.then;
var called
if (typeof then === 'function') {
// 这里的写法,是 then.call(this, fn1, fn2)
then.call(x, (y) => {
// called 是干什么用的呢?
// 有一些 promise 实现的不是很规范,瞎搞的,比如说,fn1, fn2 本应执行一个,
// 但是有些then实现里面,fn1, fn2都会执行
// 为了 fn1 和 fn2 只能调用一个, 设置一个 called 标志位
if (called) {
return;
}
called = true;
return resolvePromise(promise2, y, resolve, reject);
}, (r) => {
if (called) {
return;
}
called = true;
return reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) {
return;
}
return reject(e);
}
} else {
resolve(x);
}
}
上面代码,需要注意的地方:
-
var then = x.then
这一行代码可能会报错,需要用 try catch 包一下。为什么取对象上的属性有报错的可能?Promise
有很多实现(bluebird
,Q等),Promises/A+只是一个规范,大家都按此规范来实现Promise才有可能通用,因此所有出错的可能都要考虑到,假设另一个人实现的Promise对象使用Object.defineProperty()
恶意的在取值时抛错,我们可以防止代码出现Bug。 -
如果对象中有then,且then是函数类型,就可以认为是一个Promise对象,之后,使用
x
作为this来调用then方法。 -
如果
x === promise2
,则是会造成循环引用,自己等待自己完成,则报“循环引用”错误。 x 和 promise2 是同一个是什么情况呢?let p2 = p1.then(function(data){ console.log(data) return p2; })
上面的例子中,
p1.then()
的返回值是p2
,fn1
的返回值也是p2
。这会存在什么问题呢?promise
如果不手动的调用resolve
方法,是没有办法修改状态的。p2
的状态没法改变,没法自己改动自己的状态,永远不会被fullfilled
、rejected
-
我们需要不同的Promise实现能够相互交互,即我们要把
fn1
/fn2
的返回值,x,当成一个可能是Promise的对象,也即标准里所说的thenable
,并以最保险的方式调用x上的then方法。如果大家都按照标准实现,那么不同的Promise之间就可以交互了。而标准为了保险起见,即使x返回了一个带有then属性但并不遵循Promise标准的对象(比如说这个x把它then里的两个参数都调用了,同步或者异步调用(PS,原则上then的两个参数需要异步调用,下文会讲到),或者是出错后又调用了它们,或者then根本不是一个函数),也能尽可能正确处理。
then 里面的函数需要异步执行
最后,我们刚刚说到,原则上,promise.then(onResolved, onRejected)
里的这两相函数需要异步调用,关于这一点,标准里也有说明:
In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.
那么如何将同步代码变成异步执行呢?可以使用setTimeout函数来模拟一下:
setTimeout(()=>{
//此入的代码会异步执行
},0);
利用此技巧,将代码then执行处的所有地方使用setTimeout变为异步即可,举个栗子:
setTimeout(() => {
try {
let x = fn1(value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
},0);
promise 主体结构
// 1. 定义 status 状态
// 2. fn1, fn2 的数组
// 3. 定义 resolve reject 方法
// 4. executor 执行
function Promise(executor) {
let self = this;
self.status = 'pending';
self.fn1Callback = [];
self.fn2Callback = [];
// resolve 做到事情
// 1. 修改this 实例的状态
// 2. 修改this 这里的data
// 3. 遍历执行 this fn1Callback 上挂载的方法
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject);
}
setTimeout(() => { // 异步执行所有的回调函数
if (self.status === 'pending') {
self.status = 'resolved';
self.data = value;
for (let i = 0; i < self.fn1Callback.length; i++) {
self.fn1Callback[i](value);
}
}
});
}
function reject(reason) {
setTimeout(() => { // 异步执行所有的回调函数
if (self.status === 'pending') {
self.status = 'rejected';
self.data = reason;
for (let i = 0; i < self.fn2Callback.length; i++) {
self.fn2Callback[i](reason);
}
}
});
}
try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}
// 1. 参数校验
// 2. 根据 statue, 执行 fn1, fn2 或者把 执行fn1, fn2的行为保存在数组
// 3. 把 fn1,fn2 的返回值, 使用 resolvePromise 包裹成 promise
Promise.prototype.then = function (fn1, fn2) {
let self = this;
let promise2;
fn1 = typeof fn1 === 'function' ? fn1 : function (v) {
return v;
};
fn2 = typeof fn2 === 'function' ? fn2 : function (r) {
throw r;
};
// 执行到 then, 并不确定 promise 状态已经是 resolved
if (self.status === 'resolved') {
// then() 执行后,返回一个promise, promise 的值
return promise2 = new Promise(((resolve, reject) => {
setTimeout(() => { // 异步执行onResolved
try {
// 执行 fn1(),拿到结果 x
// fn1是用户传入的,那fn1返回值, 可能性可就多了
let x = fn1(self.data);
// 如果 x 是简单值,直接 resolve(x);
// resolve(x);
// 需要使用 resolvePromise 方法封装
resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.status === 'rejected') {
return promise2 = new Promise(((resolve, reject) => {
setTimeout(() => { // 异步执行onRejected
try {
let x = fn2(self.data);
resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.status === 'pending') {
// 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义
return promise2 = new Promise(((resolve, reject) => {
// 先定义一个方法,把方法 挂载到 onResolvedCallback 数组上
// 方法里面 就是 调用传入的 fn1
self.onResolvedCallback.push((value) => {
try {
let x = fn1(value);
resolvePromise(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
self.onRejectedCallback.push((reason) => {
try {
let x = fn2(reason);
resolvePromise(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 1. 普通值
// 2. promise 值
// 3. thenable 的值,执行 then
function resolvePromise(promise2, x, resolve, reject) {
// 为了防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'));
}
// 如果 x 是 promise
if (x instanceof Promise) {
x.then(function (data) {
resolve(data)
}, function (e) {
reject(e)
});
return;
}
// 如果 x 是 object 类型或者是 function
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
// 拿x.then可能会报错
try {
// 先拿到 x.then
var then = x.then;
var called
if (typeof then === 'function') {
// 这里的写法,是 then.call(this, fn1, fn2)
then.call(x, (y) => {
// called 是干什么用的呢?
// 有一些 promise 实现的不是很规范,瞎搞的,比如说,fn1, fn2 本应执行一个,
// 但是有些then实现里面,fn1, fn2都会执行
// 为了 fn1 和 fn2 只能调用一个, 设置一个 called 标志位
if (called) {
return;
}
called = true;
return resolvePromise(promise2, y, resolve, reject);
}, (r) => {
if (called) {
return;
}
called = true;
return reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) {
return;
}
return reject(e);
}
} else {
resolve(x);
}
}
手写 Promise.all
Promise.all
需要等到所有的 promise
的状态都变成 fulfilled
之后才 resolve
, 但只要有一个 promise
失败即返回失败的结果。
Promise.all = function (arr) {
return new Promise((resolve, reject) => {
if (!Array.isArray(arr)) {
throw new Error(`argument must be a array`)
}
let dataArr = [];
let num = 0;
for (let i = 0; i < arr.length; i++) {
let p = arr[i];
p.then((data) => {
dataArr.push(data);
num ++;
if (num === arr.length) {
return resolve(data)
}
}).catch((e) => {
return reject(e)
})
}
})
}
手写 Promise.retry
retry 是报错会尝试,尝试超过一定次数才真正的 reject
Promise.retry = function(getData, times, delay) {
return new Promise((resolve, reject) => {
function attemp() {
getData().then((data) => {
resolve(data)
}).catch((err) => {
if (times === 0) {
reject(err)
} else {
times--
setTimeout(attemp, delay)
}
})
}
attemp()
})
}
满分的测试
好不容易写好的Promise源码,最终是否真的符合Promises/A+规范,开源社区提供了一个包用于测试我们的代码:promises-aplus-tests
此包可以一项项的检查我们写的代码是否合规,如果有任一项不符就会给我们报出来,如果检查你的代码一路都是绿色,那恭喜,你的Proimse已经合法了,可以上线提供给别人使用了。
本文发布于 github 博客链接:github.com/dujuncheng/… ,未来会长期更新,欢迎关注 star
另外
字节跳动(杭州|北京|上海)大量招人,福利超级棒,薪资水平秒杀 BAT,上班不打卡、每天下午茶、免费零食无限供应、免费三餐(我念下菜单,大闸蟹鲍鱼扇贝海鲜烤鱼片黑椒牛柳咖喱牛肉麻辣小龙虾)、免费健身房、入职配touch bar15寸顶配全新mbp、每月还有租房房补。 这次真的机会多多,年后研发人数要扩招n倍,技术氛围好,大牛多,加班少,还犹豫什么?快发简历到下方邮箱,就现在!
仅仅是一小部的jd, 更多的欢迎加微信~
前端jd: job.toutiao.com/s/bJM4Anjob…
后端jd: job.toutiao.com/s/bJjjTsjob…
测试jd: job.toutiao.com/s/bJFv9bjob…
产品jd: job.toutiao.com/s/bJBgV8job…
前端实习生: job.toutiao.com/s/bJ6NjAjob…
后端实习生: job.toutiao.com/s/bJrjrkjob…
持续招聘大量前端、服务端、客户端、测试、产品,实习社招都阔以
简历发 dujuncheng@bytedance.com,建议加微信 dujuncheng1,可以聊天聊地聊人生,请注明来自掘金以及要投递哪里的岗位
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13203.html