转载:这一次,彻底弄懂 Promise 原理 - 掘金 (juejin.cn)
图解 Promise 实现原理(一)—— 基础实现 - 知乎 (zhihu.com)
Promise
Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。
基本过程
1.初始化 Promise 状态(pending)
2.立即执行 Promise 中传入的 fn 函数,将Promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理
3.执行 then(…) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
4.Promise里的关键是要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行。
链式调用:
关于new关键字;
其伪代码实现为:
new Person('james', 18) = {
var obj = {};//创建空对象
obj.__proto__ = Person.prototype;//绑定对象原型指向其构造函数
var res = Person.call(obj, 'james', 18);//指向构造函数
return typeof res === 'object' ? res : obj;
}
再来分析一段代码的执行顺序:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ test: 1 })
resolve({ test: 2 })
reject({ test: 2 })
}, 1000)
}).then((data) => {
console.log('result1', data)
},(data1)=>{
console.log('result2',data1)
}).then((data) => {
console.log('result3', data)
})
//result1 { test: 1 }
//result3 undefined
//new时会先执行Promise内部的构造函数,目前对于我们是隐式的,传参进去的fn会在构造函数中被调用,并且传递两个能改变Promise状态的函数,这两个函数可以在fn的作用域内向外传递,使得Promise的状态能被异步获取
//在new执行完成后,会调用then方法注册成功或失败后的事件操作
//在resolve,rejected执行以后,Promise内部状态改变,且调用其注册在then中的任务队列
链式调用:then方法的返回值依旧是Promise对象,且每次 then 返回了新的 Promise。then 中返回了新的 Promise,但是then中注册的回调仍然是属于上一个 Promise 的。
源码手写:
function Promise(fn){ //构造函数 内部为传入的fn
let state = 'pending';//初始化状态
let value = null;//携带值
const callbacks = [];//回调队列
this.then = function (onFulfilled){//模拟编写then方法 此处先只有一个传入的回调 然后返回值也需要是一个Promise对象;
return new Promise((resolve, reject)=>{
handle({
onFulfilled, //预期的回调方法
resolve//新Promise的resolve方法
})
})
}
function handle(callback){
if(state === 'pending'){//如果是pending状态先将传入的回调加入到callbacks回调队列中
callbacks.push(callback)
return;
}
if(state === 'fulfilled'){//如果状态改变
if(!callback.onFulfilled){//没有传入预期回调
callback.resolve(value)//将新promise的resolve调用 值为上一个promise的值
return;
}
const ret = callback.onFulfilled(value) //处理预期回调
callback.resolve(ret) //处理下一个 promise 的resolve
}
}
function resolve(newValue){//关于resolve方法
const fn = ()=>{
if(state !== 'pending')return//只能由它转换
state = 'fulfilled';
value = newValue//调整状态和值
handelCb()//调用该方法
}
setTimeout(fn,0) //基于 PromiseA+ 规范
//将其致于宏任务中在本轮微任务执行后进行,以便后续then方法回调函数的组册
}
function handelCb(){
while(callbacks.length) {
const fulfiledFn = callbacks.shift();
handle(fulfiledFn);//排空执行注册的回调
};
}
fn(resolve)//fn调用;
}
关于宏微任务的执行:
一、同步——>异步——>微任务——>宏任务。JS是单线程,碰见同步执行同步直到执行完毕,遇到异步放到执行队列中去,异步包括宏任务和微任务,在异步中微任务是优于宏任务执行的。
二、
了解Promise内部实现后再来分析上一段代码的执行
1.执行同步代码new关键字 new内部初始化了几个函数方法并且最后调用了我们传入的fn 传入了提供给我们的resolve函数。
2.这里还是属于同步代码区段,进入fn的函数执行,执行我们写的方法,调用setto初始化了一个宏任务。
3.退出fn退出promise构造函数,返回promise对象,调用then方法,then方法返回一个新的promiseB对象,且在其中调用了handle方法,属于同步代码区段。
4.handle调用,传入了我们在then中组册的回调和自己的resolve方法,这里又将执行一个构造函数和fn然后进入handle。
5.handle方法判断当前promise状态,用来组册回调事件到对象的回调队列上。然后退出方法,目前仍然是同步。(后序的.then都是一系列同步new Promise执行完成后组册给新Promise的回调方法)
6.当外界调用resolve方法时,会传入一个新的value,组册一个函数并设置其为宏任务;
7.在同步一轮以及微任务执行完后,resolve注册的函数执行,更换了原Promise的状态和值,此时调用handelCb方法,排空callbacks队列,再调用handle方法。
8.这次handle方法进入另一逻辑分支,执行组册的成功回调(或失败),并且resolve下一个promise(接收成功回调的返回值做newvalue)的resolve。
这个模型简单易懂,这里最关键的点就是在 then 中新创建的 Promise,它的状态变为 fulfilled 的节点是在上一个 Promise的回调执行完毕的时候。也就是说当一个 Promise 的状态被 fulfilled 之后,会执行其回调函数,而回调函数返回的结果会被当作 value,返回给下一个 Promise(也就是then 中产生的 Promise),同时下一个 Promise的状态也会被改变(执行 resolve 或 reject),然后再去执行其回调,以此类推下去…链式调用的效应就出来了。
作者:winty
链接:https://juejin.cn/post/6844904063570542599
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
用上面的Promise的resolve方法,执行下面方法后
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ test: 1 })
}, 1000)
}).then((data) => {
console.log('result1', data)
//dosomething
return test()//执行到此其返回值会被传入成新promise的resolve value 但是这里没有区别返回值的类型
}).then((data) => {
console.log('result2', data)
})
function test(id) {
return new Promise(((resolve) => {
setTimeout(() => {
resolve({ test: 2 })
}, 5000)
}))
}
//基于第一个 Promise 模型,执行后的输出
//result1 { test: 1 }
//result2 Promise {then: ƒ}
所以需要拓展我们的resolve函数:
function resolve(newValue){
const fn = ()=>{
if(state !== 'pending')return
if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){
const {then} = newValue
if(typeof then === 'function'){
// newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
//相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
//这样依赖,如果传入promise,会等其执行完成后调用then方法并把上个promise的resolve调用;
then.call(newValue,resolve)
return//特殊情况处理
}
}
state = 'fulfilled';//普通行为处理
value = newValue
handelCb()
}
setTimeout(fn,0)
}
再来写reject方法,并且补全之前代码里的判断逻辑:
function Promise(fn){
let state = 'pending';
let value = null;
const callbacks = [];
this.then = function (onFulfilled,onRejected){
return new Promise((resolve, reject)=>{
handle({
onFulfilled,
onRejected,//新增一种组册回调
resolve,
reject
})
})
}
function handle(callback){
if(state === 'pending'){
callbacks.push(callback)
return;
}
const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;//动态选择回调,因为只能有一类回调被执行
const next = state === 'fulfilled'? callback.resolve:callback.reject;//同上
if(!cb){//判断执行哪一类回调
next(value)
return;
}
const ret = cb(value)
next(ret)
}
function resolve(newValue){***}
function reject(error){
const fn = ()=>{
if(state !== 'pending')return
if(error && (typeof error === 'object' || typeof error === 'function')){//判断是否返回值为promise
const {then} = error
if(typeof then === 'function'){
then.call(error,resolve,reject)
return
}
}
state = 'rejected';
value = error
handelCb()
}
setTimeout(fn,0)
}
function handelCb(){***}
fn(resolve, reject)
}
异常处理代码:
异常通常是指在执行成功/失败回调时代码出错产生的错误,对于这类异常,我们使用 try-catch 来捕获错误,并将 Promise 设为 rejected 状态即可。
可以通过handle代码改造来实现:
function handle(callback){
if(state === 'pending'){
callbacks.push(callback)
return;
}
const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;
const next = state === 'fulfilled'? callback.resolve:callback.reject;
if(!cb){
next(value)
return;
}
try {
const ret = cb(value)
next(ret)
} catch (e) {
callback.reject(e);
}
}
在实际使用时,我们会习惯组册catch方法来处理错误:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ test: 1 })
}, 1000)
}).then((data) => {
console.log('result1', data)
//dosomething
return test()
}).catch((ex) => {
console.log('error', ex)
})
catch方法可以通过then的错误回调来进行处理,通过链式调用传递下来的reject错误态promise。
this.then = function (onFulfilled,onRejected){
return new Promise((resolve, reject)=>{
handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
this.catch = function (onError){
this.then(null,onError)
}
方法拓展
Finally方法:在实际应用的时候,我们很容易会碰到这样的场景,不管Promise最后的状态如何,都要执行一些最后的操作。我们把这些操作放到 finally 中,也就是说 finally 注册的函数是与 Promise 的状态无关的,不依赖 Promise 的执行结果。所以我们可以这样写 finally 的逻辑:
function Promise(fn){
...
this.catch = function (onError){
this.then(null,onError)
}
this.finally = function (onDone){
this.then(onDone,onDone)
}
...//无论结果值的调用
}
resolve 方法和 reject 方法:
这些情况下,Promise.resolve 的入参可能有以下几种情况:
- 无参数 [直接返回一个resolved状态的 Promise 对象]
- 普通数据对象 [直接返回一个resolved状态的 Promise 对象]
- 一个Promise实例 [直接返回当前实例]
- 一个thenable对象(thenable对象指的是具有then方法的对象) [转为 Promise 对象,并立即执行thenable对象的then方法。]
function Promise(fn){
...
this.resolve = function (value){
if (value && value instanceof Promise) {
return value;//如果是promise
} else if (value && typeof value === 'object' && typeof value.then === 'function'){//如果有then方法
let then = value.then;
return new Promise(resolve => {
then(resolve);
});
} else if (value) {
return new Promise(resolve => resolve(value));
} else {
return new Promise(resolve => resolve());
}
}
...
}
作者:winty
链接:https://juejin.cn/post/6844904063570542599
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
//判断上述四点的不同传入
Promise.all:
入参是一个 Promise 的实例数组,然后注册一个 then 方法,然后是数组中的 Promise 实例的状态都转为 fulfilled 之后则执行 then 方法。这里主要就是一个计数逻辑,每当一个 Promise 的状态变为 fulfilled 之后就保存该实例返回的数据,然后将计数减一,当计数器变为 0 时,代表数组中所有 Promise 实例都执行完毕。
function Promise(fn){
...
this.all = function (arr){
var args = Array.prototype.slice.call(arr);//复制一份新数组
return new Promise(function(resolve, reject) {
if(args.length === 0) return resolve([]);//长度为0返回空数组
var remaining = args.length;
function res(i, val) {
try {
if(val && (typeof val === 'object' || typeof val === 'function')) {//判断传入的对象有then方法
var then = val.then;
if(typeof then === 'function') {
then.call(val, function(val) {//then方法调用,并且传入该匿名回调做为成功回调
res(i, val);//成功回调完成后调用该函数
}, reject);
return;//如果是
}
}
args[i] = val;//将args[i]上的promise转换为对应的值
if(--remaining === 0) {
resolve(args);
}//一旦length归0处理完成
} catch(ex) {//捕获回调抛出的异常
reject(ex);
}
}
for(var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
}
...
}
总结:
1.其中最关键的点就是要理解 then 函数是负责注册回调的,真正的执行是在 Promise 的状态被改变之后。
2.而当 resolve 的入参是一个 Promise 时,要想链式调用起来,就必须调用其 then 方法(then.call),将上一个 Promise 的 resolve 方法注入其回调数组中。
3.new和宏微任务的判断和执行也十分重要
补充说明 虽然 then 普遍认为是微任务。但是浏览器没办法模拟微任务,目前要么用 setImmediate
,这个也是宏任务,且不兼容的情况下还是用 setTimeout 打底的。还有,promise 的 polyfill
(es6-promise) 里用的也是 setTimeout。因此这里就直接用 setTimeout,以宏任务来代替微任务了。