[JS]带你深入与手写Promise

439 阅读10分钟

最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。

前言

Promise是ES6带来的重量级新特性。就算很多开发者在日常编码中,不习惯使用es6的某些新语法,但相信大部分都会请不自禁地使用Promise。因为他不仅是优化了我们的代码复杂度,甚至改变了我们的编码风格。

概念

Promise是es6的一个新的内置类,用来管理异步任务(当然不一定是异步)。它是异步编程风格的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大,解决了如回调地狱等问题。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。

用法

new Promise(【executor】)。函数参数不能为空,否则报错。

let p1 = new Promise();  
// Uncaught TypeError: Promise resolver undefined is not a function

// executor函数应该有2个参数resolve, reject
let p = new Promise(function(resolve, reject){
	// do something...
});

Promise实例

执行new之后我们会得到一个promise实例。

属性

他有2个内置属性,一个promise的状态,一个是promise的result。

  • [PromiseState]]: 实例状态,pending准备状态 fulfilled/resolved成功态 rejected失败态。通过resolve, reject方法修改。注意:状态一旦设置了就不能再修改了。
  • [[PromiseResult]] : 实例的值,executor执行后的结果。通过resolve, reject方法修改。

公共属性方法 Promise.prototype

  • then
  • catch
  • finally
  • Symbol(Symbol.toStringTag)

then

then中填2个函数参数,onfulfilledCallback和onrejectedCallback。

 p.then(onfulfilledCallback,onrejectedCallback);

onfulfilledCallback和onrejectedCallback分别是Promise executor的执行结果的后续处理。如果Promise执行了resolve就会触发onfulfilledCallback并把结果传递,同理onrejectedCallback就是reject的后续。

p1.then(result => {
  	//Promise执行了resolve,result是resolve的结果
    console.log('成功-->', result);
}, reason => {
  	//Promise执行了reject,reason是reject的结果
    console.log('失败-->', reason);
});

值得注意的是,p.then有返回值的。他会返回一个新的Promise实例。而这个实例的状态和结构遵循以下:

  • 无执行的是基于p1.then存放的onfulfilledCallback/onrejectedCallback两个方法中的哪一个。只要方法执行不报错且没有返回值,就会触发resolve。返回值是undefined。
var p1 = new Promise((resolve,reject)=>{
  	resolve('p ins')
});
// 执行onfulfilledCallback
var p2 = p1.then((res)=>{
  	console.log(res);
})

console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: undefined

//执行onrejectedCallback
var p3 = new Promise((res,rej)=>{
	rej('wrong!');
}).then((res)=>{},reason=>{
	console.log(reason);
})
console.log(p3);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: undefined
  • 如果方法执行报错,p2的 [[PromiseState]]设为rejected 。 [[PromiseResult]]就是报错原因。
var p2 = p1.then((res)=>{
  	throw '出错了!';
})

console.log(p2);
//[[PromiseState]]: "rejected"
//[[PromiseResult]]: "出错了!"
  • 如果方法中返回一个全新的Promise实例,则“全新的Promise实例”的成功和失败决定then实例的成功和失败。
var p2 = p1.then((res)=>{
  	return new Promise((resolve,reject)=>{
      	resolve(10);
    });
})

console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: 10

var p2 = p1.then((res)=>{
  	return new Promise((resolve,reject)=>{
      	reject(-10);
    });
})

console.log(p2);
//[[PromiseState]]: "rejected"
//[[PromiseResult]]: -10
  • 如果主动返回非promise,则 [[PromiseState]]设为fulfiled。 [[PromiseResult]]是主动返回的值。
var p2 = p1.then((res)=>{
  	return 100
})

console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: 100
  • 如果传入的不是函数,会直接返回p1的结果
var p2 = p1.then(111)

console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: 'p ins'

顺延/穿透

既然then会返回一个新的promise实例,因此我们就可以利用这个机制,不断地then下去。实现同步逻辑写法。这种现象就是【顺延】或者【穿透】。

new Promise((resolve, reject) => {
        // resolve('OK');
        reject('NO');
    }).then(result=>resul ,reason=>Promise.reject(reason))
    .then(result => {
        console.log('成功-->', result);
    }, reason => {
        console.log('失败-->', reason);
    }).then(result => {
        console.log('成功-->', result);
    }, reason => {
        console.log('失败-->', reason);
    });

catch

正常情况下,executor的执行,如果触发的是reject。promise是会往程序抛错的。

new Promise((resolve,reject)=>{
	reject(100)
});
// Uncaught (in promise) 100

因此,promise实例很贴心的为我们提供了catch方法。它是promise用来捕获错误的公共方法,我们不需要自己另外try,catch。

new Promise((resolve,reject)=>{
reject(100)
}).catch(err=>{
    console.log(100);
})
// 100

当然它还可以配合穿透机制使用,在then的队列中只要其中一个抛错了,就会触发catch。并不是按顺序执行。

new Promise((resolve, reject) => {
    resolve('OK');
}).then(result => {
  	// 抛错直接执行catch
  	return Promise.reject('xx');
}).then(result => {
    // 由于前面已经抛错,这里不会再执行了
    console.log('成功-->', result);
}).catch(reason => {
    // 执行输出xx
    console.log('失败-->', reason);
});

Promise.all

Promise.all是一个挂在Promise上的方法。入参是一个promise实例数组。返回值是一个新的promise实例A,这个实例A是成功还是失败,取决于数组中的每一个promise实例是成功还是失败,只要有一个是失败,A就是失败的,只有都成功A才是成功的。

function fn(interval) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(interval);
        }, interval);
    });
}
let p1 = fn(3000);
let p2 = fn(1000);
let p3 = fn(2000);

Promise.all([p1, p2, p3]).then(results => {
    // 不论谁先知道状态,最后结果的顺序和传递数组的顺序要保持一致
    console.log(results);
    // 输出[3000,1000,2000]
}).catch(reason => {
    console.log(reason);
});
let p1 = fn(3000);
let p2 = fn(1000);
let p3 =  Promise.reject(0);

Promise.all([p1, p2, p3]).then(results => {
    console.log(results);
}).catch(reason => {
  	// 处理过程中,遇到一个失败,则All立即为失败,结果就是当前实例失败的原因
    console.log(reason);
    // 输出0
});

Promise.race

Promise.race同样是挂在Promise上的方法,同样传入promise实例数组。返回一个新的promise实例R。实例R的结果取决于,最先数组中最先完成的promise实例的结果。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  // p2先完成,直接作为结果
  console.log(value);
  // 输出 'two'
});

重点:promise的同步与异步

现在我们已经了解了Promise的基础使用了,下面我们来看看Promise的难点——在Promise中的同步与异步。这部分的知识点常见于面试题的笔记部分。

那些是异步?

我们先来区分promise中究竟哪些是同步执行的,哪些是异步执行的。很多人也许不知道,exector本身是同步执行的。只是他的结果实例的then和catch等结果回调才是异步的。

举个例子:

console.log(1);

var p1 = new Promise((resolve,reject)=>{
	console.log(2);
  resolve('hi');
  console.log(3);
})

p1.then((res)=>{
	console.log(res);
})

console.log(4);

因为executor的内容是同步执行的,所以2,3会在4之前输出,但then的内容是异步的,会最后输出。

// 1
// 2
// 3
// 4
// hi

回调方法必须在实例的状态改变之后才会执行

then的内容是否执行,得看实例本身的状态是否已经改变了。只要改变了之后,函数才会进入执行队列。

new Promise(resolve => {
    console.log(1);
    resolve();
}).then(() => {
    console.log(2);
    new Promise(resolve => {
        console.log(3);
        resolve();
    }).then(() => {
        console.log(4);
    }).then(() => {
        console.log(5);
    });
}).then(() => {
    console.log(6);
}); 

在上述的代码中,执行流程应该是:

  • 第一次程序执行时,同步内容输出1,并且修改实例为成功状态。
  • 后面接的2个then会进入待执行的状态。但执行时机取决于实例的状态。
  • 这里因为resolve已经修改了状态。所以会执行第一个then。
  • 输出2,同步执行第二个executor,输出3,修改成功状态。并把4,5进入待执行状态。
  • 注意这是我们待执行的队列中有6,4,5。
  • 由于4的状态已经被3改变,所以输出4。修改了5的状态为成功。
  • 同时由于2中的代码已经执行完了,并肯定没有抛错,所以6的状态转为成功。
  • 此时待执行队列中6和5都是成功状态了,可是因为6是先进入队列的,所以先输出6,再输出5
// 答案是
// 1
// 2
// 3
// 4
// 6
// 5

难点,这里6其实是先进入待执行队列的,但根据我们上面学习的内容知道,then返回的promise实例的结果假如没有返回,就得看then内代码有没有报错。所以6会等then内容的promise全部执行完,确保没抛错了之后,修改状态为成功。又由于6比5先进入队列,所以即使两者状态同为成功,6会比5先输出。

手写Promise

手写Promise在近年来也成为了一道面试题出现,既然我们已经掌握了promise的核心机制。接下来我们就试着手写Promise吧。

Promise类

class MyPromise{
  
	constructor(executor){
    // 设置实例初始状态
    this.PromiseState = 'pedding';
    this.PromiseResult = undefined;
    this.onFulfilled = [];//成功的回调
    this.onRejected = []; //失败的回调
    // 定义resolve,reject
    const resolve = (result)=>{
      if(this.PromiseState==='padding'){
				this.PromiseState = 'fulfilled';
    		this.PromiseResult = result;
        this.onFulfilled.forEach(item=>item());
      }
		};
    const reject = (reason)=>{
			 if(this.PromiseState==='padding'){
				this.PromiseState = 'rejected';
    		this.PromiseResult = result;
        this.onRejected.forEach(item=>item());
      }
		};
    // 如果exector执行抛错,直接当reject处理
    try{
      executor(resolve,reject);
    }catch(error){
    	reject(error);
    }
  }
  
  // 定义then方法
  then(onfulfilledCallback,onrejectedCallback){
    // 处理入参,防止不是函数或者undefined
    const fulfilledCallback = typeof onfulfilledCallback === 'function'? onfulfilledCallback: (result)=>result;
    const rejectedCallback = typeof onrejectedCallback === 'function'? onrejectedCallback: (reason)=>{ throw reason};
    
    // 返回一个新的promise实例
    const subPromiseInst =  new MyPromise((resolve,reject)=>{
       	// 1. 因为要有异步的效果,这里有setTimeout制作异步,但大家要留意setTimeout是宏任务,promise应该是微任务。
        // 2. 要try catch是因为如果then的回调本身执行抛错,统一当reject处理
      	// 3. resolvePromise核心函数负责处理then结果和新promise实例的关系
    		
  		if(this.PromiseState = 'fulfilled'){
       		// 状态已经成功可以直接执行
        	setTimeout(()=>{
            try{
              const thenResult = fulfilledCallback(this.PromiseResult);
        			resolvePromise(subPromiseInst,thenResult,resolve,reject);
            }catch(error){
            	reject(error)
            }
          },0)
        
    	}else if(this.PromiseState = 'rejected'){
      		// 状态已经失败可以直接执行
        	setTimeout(()=>{
            try{
              const thenResult = rejectedCallback(this.PromiseResult);
        			resolvePromise(subPromiseInst,thenResult,resolve,reject);
            }catch(error){
            	reject(error)
            }
          },0)
    
    	}else if(this.PromiseState = 'padding'){
      		// 状态还没设置,先加到队列中
      		this.onFulfilled.push(()=>{
          	setTimeout(()=>{
            	try{
              	const thenResult = fulfilledCallback(this.PromiseResult);
        				resolvePromise(subPromiseInst,thenResult,resolve,reject);
            	}catch(error){
            		reject(error)
            	}
          	},0)
          });
      		this.onRejected.push(()=>{
          	setTimeout(()=>{
            	try{
              	const thenResult = rejectedCallback(this.PromiseResult);
        				resolvePromise(subPromiseInst,thenResult,resolve,reject);
            	}catch(error){
            		reject(error)
            	}
          	},0)
          });
    	}
    });
    return subPromiseInst;
  }
}

核心

resolvePromise核心函数是处理then和返回的新promise实例的关系的。每次执行then都会返回一个新的promise实例,而返回的内容是根据then的内容执行决定的。thenResult的可能性有:

  • 返回原始数据类型或者undefined
  • 返回普通引用数据类型
  • 返回一个新的promise实例

同时我们需要一个标识(used),来确保成功回调和失败回调只会执行其中一个。
所谓then结果和新实例的关系,其实就是我们要知道究竟要执行新实例的resolve方法还是reject方法!

// 处理核心
function resolvePromise(promiseIns, thenResult, resolve, reject) {
    let self = this;
    // 避免死循环
    if (promiseIns === thenResult) {
        reject(new TypeError('死循环'));
    }
    // thenResult返回的是引用类型
    if (thenResult && typeof thenResult === 'object' || typeof thenResult === 'function') {
        let used; 
        try {
            let then = thenResult.then;
            // 判断then中返回是是不是又是promise实例
            if (typeof then === 'function') {
              	// 如果返回的是新的promise实例,得根据实例的结果来决定返回的内容
                then.call(thenResult, (newPromiseInsThenResult) => {
                  	// 如果新实例then的成功回调被执行了,说明新实例的状态是成功
                    if (used) return;
                    used = true;
                  	// 递归新promise实例
                  	// 虽然我们知道新promise实例是成功的,但不知道要resolve传递什么内容,所以还得再进行一次。
                    resolvePromise(promiseIns, newPromiseInsThenResult, resolve, reject);
                }, (newPromiseInsThenReason) => {
                  	// 如果新实例的then的失败回调被执行了,说明新实例的状态是失败
                    if (used) return;
                    used = true;
                    reject(newPromiseInsThenReason);
                });
            }else{
              // 如果then中返回是普通数据,可以直接执行resolve并传递值
                if (used) return;
                used = true;
                resolve(thenResult);
            }
        } catch (error) {
          	// 确保resolve和reject只执行一个
            if (used) return;
            used = true;
            reject(error);
        }
    //如果then返回的是原始数据类型或者undefined,直接执行resolve,并传递结果
    } else {
        resolve(thenResult);
    }
}

总结

今天我们了解了promise的核心规律,这部分的内容可以帮助我们解决几乎所有的promise相关的笔试。同时我们也深入尝试了手写promise的实现,需要注意的是虽然我们的异步实现用的是setTimeout, 但promise的异步事实上是微任务。这个要记得。

参考

juejin.cn/post/684490…

zhuanlan.zhihu.com/p/183801144