新手也能看懂的Promise原理,超简单!不来看一下吗?(续)想真正明白Promise的原理?那就来看这篇文章吧,新手也
5 修复then回调异步执行
_Promise.prototype.then = function (onResolved, onRejected) {
const _self = this;
setTimeout(() => {
return new _Promise((resolve, reject) => {
// 省略代码
});
});
};
但其实这样是有问题的,我们就用这个加上了setTimeout的_Promise.prototype.then
来测试一下,还是上节的测试代码:
const p0 = new _Promise((resolve) => {
console.log(1);
resolve(3);
});
p0.then((val) => {
console.log(val);
});
console.log(2);
输出结果如下:
唉!这不是输出1 2 3
了吗?没问题呀!!别急,我们换下面这个测试用例:
setTimeout(() => console.log(4));
const p0 = new Promise((resolve) => {
console.log(1);
resolve(3);
});
p0.then((val) => {
console.log(val);
});
console.log(2);
我们在最开始时加入了setTimeout(() => console.log(4))
,原生的Promise输出如下:
然后换我们给_Promise.prototype.then
加了setTimeout()
进行验证,看看输出:
它输出的是1 2 4 3
。
这里就涉及到浏览器的事件循环了,咱们简单提一下,在浏览器的事件循环中,每次执行完一个宏任务后,会清空微任务队列,之后再遍历延时队列按到期时间执行。
这里涉及到三个队列,宏任务队列、微任务队列、延时任务队列。原生的Promise.then
的执行,是放在微任务队列里的,而setTimeout
是放在延时任务队列里的。如果我们的_Promise.prototype.then
里是用setTimeout
包裹的话,那么它就是放在延迟任务队列里去执行了。延时队列是根据延时时间来定的,两个延时时间一样,就按照声明的位置前后来定,setTimeout(() => console.log(4))
的声明要更早,所以它会先执行。
明确了问题,那就好改了,将_Promise.prototype.then
里包裹的return
语句放在微任务队列里就好了,我们可以用queueMicrotask()
方法将then
返回的内容放在微任务队列中,具体改造如下:
_Promise.prototype.then = function (onResolved, onRejected) {
const _self = this;
queueMicrotask(() => {
return new _Promise((resolve, reject) => {
// 其他代码
});
});
};
这样改造之后,输出就是1 2 3 4
了,这里就不截图了,大家可以试试。
6.Promise.prototype.catch
实现了Promise
的then
方法,那么其他方法就比较简单了。我们先看看catch
方法:
老规矩,我们先上原生Promise的catch方法,看看catch的用法:
const p0 = new Promise((resolve, reject) => reject('报错'));
p0.then(
(val) => {
console.log('onResolved', val);
},
(err) => {
console.log('onRejected', err);
throw Error('onRejected 抛出异常');
}
).catch((err) => {
console.log(err);
});
上述代码输出如下:(下面的输出信息我把报错的文件位置等信息去掉了)
onRejected Error: new时报错
Error: onRejected 抛出异常
在上面的示例代码中,我们用new了
一个p0
的Promise
对象,并在new
的时候抛出一个错误,结果我们在p0.then(onResolved, onRejected)
的onRejected
里接受到了这个错误。接着,我们在onRejected
里抛出了另一个错误,结果,我们如期在catch
里面捕捉到了这个错误。
所以,我们发现,catch
方法其实是一个没有第一个入参的then
方法,具体来说,就是Promise.prototype.catch()
方法是.then(null, onRejected)
或.then(undefined, onRejected)
的别名。明白了这点,那我们实现catch
方法的时候完全可以复用then
方法:
_Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
我们来测试一下:
const p = new _Promise((resolve, reject) => {
reject("err");
});
p.catch((res) => {
console.log(res);
});
//
上述代码如期输出err
,这样,我们就又实现了catch
方法。
7.Promise.race
race
方法将多个 Promise
实例,包装成一个新的 Promise
实例。率先改变状态的Promise
实例,其返回值将传回给新生成的Promise
实例,并且新生成的Promise
实例的状态跟率先改变状态的Promise
实例的状态一致。
这么说可能有点饶,我们来看个例子:
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise
实例的返回值,就传递给p
的回调函数。
不知道大家有没有注意到,这里的race
方法我们是直接用Promise
调用的,而不是通过实例化后的对象进行调用。所以,这个race
方法不是实例的方法,我们不能定义在原型链上。
明白race
的作用机制,接下来我们就可以开始实现race
了。
首先,我们根据第一点,race
方法返回一个新的Promise
实例,可以写出如下代码:
_Promise.race = function (promiseArray) {
return new _Promise((resolve, reject) => {});
}
其次,率先改变状态的Promise实例,其返回值将传给新的Promise实例。率先这个词要怎么去理解呢?其实很简单,我们遍历这些传入的Promise,然后在每个Promise的then的回调函数里,就能拿到其返回值了。根据这一点,我们可以写出如下代码:
_Promise.race = function (promiseArray) {
return new _Promise((resolve, reject) => {
promiseArray.forEach(p => {
p.then((val) => {}, (err) => {});
})
});
}
在第5行中,我们通过p.then((val) => {}, (err) => {})
可以拿到传进来的每个Promise
实例的返回值,不仅如此,我们还知道每个Promise
实例的状态是resolved
还是rejected
:在then
的第一个函数参数,该Promise
实例的状态肯定是resolved
,第二个函数参数里,状态肯定是rejected
。
因此,要想做到race
方法新生成的Promise
实例的状态,跟传进来的Promise
数组中、最先改变状态的Promise
实例的状态保持一致,就直接在then
方法里执行resolve
或者reject
就好了:
_Promise.race = function (promiseArray) {
return new _Promise((resolve, reject) => {
promiseArray.forEach(p => {
p.then((val) => resolve(val), (err) => reject(err));
})
});
}
我们在第4行代码p.then((val) => resolve(val), (err) => reject(err))
中,如果p
的状态变成resolved
了,就会进入到第一个函数参数里,然后在这里我们就可以将race
方法新生成的Promise
实例的状态resolve
掉,同理,p
的状态变成rejected
时,就将新生成的Promise
实例rejected
掉。
另外,第四行代码可以简写成:p.then(resolve, reject)
,最终race方法就变成下面这样:
_Promise.race = function (promiseArray) {
return new _Promise((resolve, reject) => {
promiseArray.forEach(p => {
p.then(resolve, reject);
})
});
}
这个简写就有点考验大家对于then
方法里第一个入参onResolved
和第二个入参onRejected
的理解了,如果大家有疑惑的,就再回头看一下then
方法的实现,相信你一定可以明白这个简写的逻辑。
接下来我们测试一下我们实现的race
方法:
const p1 = new _Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 300);
});
const p2 = new _Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 200);
});
const p3 = new _Promise((resolve, reject) => {
setTimeout(() => {
resolve('p3');
}, 100);
});
_Promise.race([p1, p2, p3]).then((res) => console.log(res));
执行上述代码后,成功输出p3
,这就说明了我们的race
方法的实现是正确的。
8.all
最后一个是all方法, all
方法同样将多个 Promise
实例,包装成一个新的 Promise
实例。它有以下两个机制:
- 所有的
Promise
实例的状态都resolved
后,其返回值组成的数组才传回给回调函数,并且返回的Promise
实例的状态也会变成resolved
。 - 或者,传入的多个
Promise
实例中,只要有一个的状态是rejected
,新生成的Promise
实例的状态也会变成rejected
,并且,最先rejected
的Promise
实例,其返回值传给回调函数。
有了上节race
的基础,相信你对于all
的实现应该有自己的想法了,我们可以很轻松的写出下面的代码:
_Promise.all = function (promiseArray) {
const result = [];
return new _Promise((resolve, reject) => {
promiseArray.forEach((p) => {
p.then(
(res) => {},
(err) => {}
);
});
});
};
因为有返回值组成的数组,所以我们在第2行定义了一个保存每个promise
的执行结果的数组result
,然后在all
方法里返回了一个Promise
实例(暂且称为p0
),并且在新建这个Promise
实例的时候遍历传入的Promise
数组。
那么现在,只要在遍历传入的Promise
数组的过程中,遇到resolved
状态的实例,就将其结果存到reuslt
数组中,然后判断数组的长度是否跟传入的Promise
数组的长度相等,相等的话就说明传入的Promise
数组都已经遍历完了,而且也有结果了,那自然就可以执行resolve
函数将p0
的状态改成resolved
了!
而且,如果在遍历的过程中,只要有一个是rejected
的状态,我们就执行reject
,将p0
的状态改成rejected
, 这样就满足all
方法的第2点机制了。 修改后的代码如下:
_Promise.all = function (promiseArray) {
const result = [];
return new _Promise((resolve, reject) => {
promiseArray.forEach((p) => {
p.then(
(res) => {
result.push(res);
if (result.length === promiseArray.length) {
resolve(result);
}
},
(err) => reject(err) // 这个箭头函数也可以直接简写成reject
);
});
});
};
这样写其实有个问题,那就是第7行代码:result.push(res)
,这一行代码,会导致谁先resolved
谁的结果就会先被push
进数组,而all
方法返回的数组,其结果是跟Promise
实例在promiseArray
中的位置一一对应的。所以我们不能用push
,而应该直接对下标赋值,修改代码如下:
_Promise.all = function (promiseArray) {
const result = [];
let resolvedCount = 0;
return new _Promise((resolve, reject) => {
promiseArray.forEach((p, i) => {
p.then(
(res) => {
result[i] = res;
resolvedCount++;
if (resolvedCount === promiseArray.length) {
resolve(result);
}
},
(err) => reject(err)
);
});
});
};
而又因为我们将result.push(res)
改成了result[i] = res
,所以我们不能根据result
的长度来判断当前resolved
状态的promise
实例数量了,因为有可能最后一个promise
实例已经resolved
了,而前面的promise
实例还在pending
中,这时候reuslt
的前面都是undefined
,只有最后一个有值。
因此,我们需要增加一个计数器resolvedCount
,最后也是用计数器跟promiseArray.length
去做比较,来判断当前是不是所以的promise
实例的状态都变成resolved
了。
接下来,我们测试一下all方法,测试用例如下:
const p1 = new _Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 300);
});
const p2 = new _Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 200);
});
const p3 = new _Promise((resolve, reject) => {
setTimeout(() => {
resolve('p3');
}, 100);
});
_Promise.all([p1, p2, p3]).then((res) => console.log(res));
诶!没错,还是这三个Promise实例,只不过我们最后调用的是all方法。其输出结果如下:
非常好,很符合我们的预期。大家也可以用result.push(res)
而不是result[i] = res
来进行测试,以此来加深印象。放个完整代码如下:
function _Promise(executor) {
const _self = this;
_self.status = 'pending';
_self.value = undefined; // 保存resolve函数的入参
_self.reason = undefined; // 保存reject函数的入参
_self.onResolvedCallbacks = []; // 保存pending状态下的onResolved函数
_self.onRejectedCallbacks = []; // 保存pending状态下的onRejected函数
function resolve(res) {
if (_self.status === 'pending') {
_self.status = 'resolved';
_self.value = res;
// 一旦 resolve 执行,遍历执行已收集到的 onResolved 回调函数,并重置 onResolvedCallbacks
_self.onResolvedCallbacks.forEach((fn) => fn());
_self.onResolvedCallbacks = [];
}
}
function reject(err) {
if (_self.status === 'pending') {
_self.status = 'rejected';
_self.reason = err;
// 一旦 reject 执行,遍历执行已收集到的 onRejectedCallbacks 回调函数,并重置 onRejectedCallbacks
_self.onRejectedCallbacks.forEach((fn) => fn());
_self.onRejectedCallbacks = [];
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
_Promise.prototype.then = function (onResolved, onRejected) {
const _self = this;
queueMicrotask(() => {
return new _Promise((resolve, reject) => {
const callback = (fn) => {
try {
const result = fn(_self.value || _self.reason);
if (result instanceof _Promise) {
// prettier-ignore
result.then((res) => resolve(res), (err) => reject(err));
return;
}
resolve(result);
} catch (e) {
reject(e);
}
};
switch (_self.status) {
case 'resolved':
callback(onResolved);
break;
case 'rejected':
callback(onRejected);
case 'pending':
_self.onResolvedCallbacks.push(() => callback(onResolved));
_self.onRejectedCallbacks.push(() => callback(onRejected));
break;
default:
break;
}
});
});
};
_Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
_Promise.race = function (promiseArray) {
return new _Promise((resolve, reject) => {
promiseArray.forEach((p) => {
p.then(resolve, reject);
});
});
};
_Promise.all = function (promiseArray) {
const result = [];
let count = 0;
return new _Promise((resolve, reject) => {
promiseArray.forEach((p, i) => {
p.then(
(res) => {
result[i] = res;
count++;
if (count === promiseArray.length) {
resolve(result);
}
},
(err) => reject(err)
);
});
});
};
刚好100行~我们实现了Promise
构造函数、实现了Promise.prototype.then
、实现了Promise.prototype.catch
,还实现了Promise.race
和Promise.all
方法,平均每个行数也就25行左右,非常简单!!!
以上就是关于Promise
原理的全部内容了,如果对你有帮助的话,欢迎点赞收藏哦 !!如果有问题的话,也可以在评论区留言,我看到会及时回复的~
转载自:https://juejin.cn/post/7413586505354395657