相信很多人都对 es6 语法中的 promise 比较熟悉,但是大部分人只知道它的用法,却没有真正去了解它的内在实现。下面就让笔者带领大家去一探究竟,并且手写一个类似 promise 的类实现。
文章内容主要分为三个部分:1、高阶函数;2、js 异步编程;3、手写 promise 源码。
这时候就会有人疑惑,前面两部分是干嘛用的?在笔者看来,前面两个章节可以说是对 promise 原理的铺垫以及扩展。
高阶函数
在 js 语言中,函数也可以像普通 object 一样成为其他函数里的参数或是返回值。我们将参数或是返回值为函数的函数称为高阶函数。
下面来看一下它的基本用法:
function f1(a, b) {
return a + b;
}
function f2(a, b, fn) {
let value = fn(a,b);
return function() {
console.log(value);
}
}
let f3 = f2(1, 2, f1);
用法看起来并不困难,那使用它对于我们日常的开发有什么好处呢?下面介绍两种比较常用的使用场景:
1、函数柯里化
把接受多个参数的函数变换成接受部分参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术,就是函数柯里化。
听起来可能有点拗口,那我们先来看看下面这个熟悉的面试题
// 实现一个函数让下面三行代码执行结果相同
currySum(1)(2, 3)
currySum(1)(2)(3)
currySum(1, 2, 3)
其实这道题就是一个函数柯里化的实现。当函数被柯里化以后,一旦传入的参数少于函数定义的参数数量,就会返回一个接受当前传入的部分参数的新函数,所以上面三行代码的执行结果才能一致。下面让我们来看看这是如何实现的:
先定义一个函数:
function getSum(a, b, c) {
let sum = a + b + c;
return sum;
}
紧接着就是将函数柯里化的函数:
const curry = (fn)=> {
return function circle(...args) {
if(args.length < fn.length) {
return (...secondArgs) => {
return circle(...args, ...secondArgs);
}
}
return fn(...args);
}
}
const currySum = curry(getSum);
这里使用了递归、闭包、es6 的展开运算符、高阶函数相关知识,有兴趣的小伙伴可以自行查阅资料,这里笔者就不再一一细讲了
说完函数柯里化的实现以后,也来说一说它能应用在哪些地方:
(1)参数复用
当一个函数接受多个参数时,如果其中一个参数基本保持不变且作为基准值供后续参数使用,函数柯里化就会大派用场,例如一些固定正则表达式的校验,以及一些固定数据源的处理
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying 后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // fal
(2)延迟执行
顾名思义,就是等到所有的参数都传入时才执行,我们在平常使用的 bind 函数实际就是柯里化的一种实现
Function.prototype.bind = function (context) {
var _this = this;
var args = Array.prototype.slice.call(arguments, 1);
return function () {
return _this.apply(context, args);
};
};
2、react 的高阶组件
写过 react 的小伙伴应该都使用过它的高阶组件,它的目的也是为了尽量提取重复的代码进行封装,以达到代码优化的效果,这里就暂时不展开描述,有缘的话我们下次分解。
JS 异步编程
为什么会说到这个模块呢?因为 promise 的执行过程就是在微任务里面执行的。
下面让我们循序渐进的进行介绍:
1、js 异步编程的基本理解
Javascript 语言的执行环境是"单线程",也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。为了避免其中一个任务执行时间过长导致其他任务无法执行的问题,js 将任务分为同步和异步任务。
常见的异步任务有:回调函数(包含事件回调、发布订阅)、setTimeout 和 setInterval 定时器方法、promise
2、EventLoop和消息队列
上述图表清晰的展示了代码的执行过程,下面分别对其中的一些概念进行解释:
eventLoop指的是调用栈(call stack)、事件映射表(event table)以及消息队列(event queue)这三者结合的环路。
消息队列(event queue),也叫回调函数队列,当event table中的事件被触发,对应的回调函数就是被推进消息队列,等待被推进call stack执行。
3、宏任务和微任务
宏任务可以理解为每次调用栈从开始执行到结束执行(调用栈清空)的过程(注意,这里指的并不是每行代码的执行)
微任务指的是当前宏任务执行完毕以后,下一个宏任务执行之前的过渡任务, 它会在当前宏任务执行完毕以后立即执行
promise的执行就是在微任务里面进行的,下面来看一下一个经典的面试题:
console.log('aaaa');
async function edf() {
console.log('bbbb')
}
async function abc() {
console.log('cccc')
let value = await edf();
console.log(value);
console.log('dddd')
}
abc();
console.log('eeee');
setTimeout( ()=> {
console.log('ffff')
}, 0)
console.log('gggg')
// 求以上代码执行的输出结果
这里的输出结果先卖个关子,这里的代码执行涉及到宏任务、微任务以及async、await的原理剖析,笔者在最后会为大家再进行的执行顺序的分析
手写Promise
下面就让我们来手写一个类似的promise的东西。笔者会根据promise的特征一步步进行完善
1、promise基础特征
let a = new Promise( (resolve, reject) => {
})
promise是使用new来进行实例化,并且里面传入一个函数做为实例化的参数,该函数调用类内部的resolve和reject方法,那下面我们来建立一个基础类
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject);
}
resolve = (value) => {
};
reject = (error) => {
};
}
由于 executor为外部传入函数,为了使用实例内部的函数,resolve和reject需要使用箭头函数的形式(跟react里面的函数用法相似)
let a = new Promise( (resolve, reject) => {
resolve(123);
}).then( (value) => {
console.log(value)
}, (err) => {
console.log(err)
})
promise后面可接then进行链式调用,这跟函数柯里化很相似,那大家就可以猜到返回的其实是一个新的promise实例,then里面接收两个函数(成功回调函数和失败回调函数)
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject);
}
resolve = (value) => {
};
reject = (error) => {
};
then(successCallback, failCallback) {
return new Promise( (resove, reject) => {
})
}
}
这个时候promise的基础结构已经完成,我们再来逐步完善
2、promise的状态变化
在promise里面resolve和reject分别代表成功和失败的状态,同时成功和失败也有对应的值。
promise主要根据这些状态值进行下一步的处理
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject);
}
PromiseValue; // promise内置value
PromiseStatus = "pending"; // promise的状态分为'pending','resolved','rejected',默认状态为'pending'
PromiseError; // promise内置error
resolve = (value) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseValue = value;
this.PromiseStatus = "resolved";
};
reject = (error) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseError = error;
this.PromiseStatus = "rejected";
};
then(successCallback, failCallback) {
return new Promise( (resolve, reject) => {
switch (this.PromiseStatus) {
case 'pending':
break;
case 'resolved':
resolve(successCallback(this.PromiseValue));
break;
case 'rejected':
reject(failCallback(this.PromiseError));
break;
}
})
}
上述例子同时说明了一个promise的状态变化逻辑,即只能从pending变成resovled或者rejected,其它状态值变化并不允许
3、promise的异步处理
非异步的情况我们已经实现了,下面来看看异步又有什么办法处理呢?
首先then的异步情况分为两种情况:
1、同一个promise可以不断调用then,这个时候then方法之间没有任何联系,对于同一个PromiseValue进行操作
2、then的链式调用,由于每次then都会返回一个promise,需要使用闭包的方式保存每一个promise
(1)异步情况一
情景如下:
let a = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(134);
}, 1000);
})
a.then((value) => {
console.log(value);
});
a.then((value) => {
console.log(value);
});
我们可以思考一下,异步最终都会走resolve和reject方法,所以我们只要将执行的函数存储起来,等到resolve或者reject方法执行就行,为此,我们需要成功回调函数集合以及失败回调函数集合
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject);
}
PromiseValue; // promise内置value
PromiseStatus = "pending"; // promise的状态分为'pending','resolved','rejected',默认状态为'pending'
PromiseError; // promise内置error
resolveCallbackArray = []; // resolve的回调函数集合
rejectCallbackArray = []; // reject的回调函数集合
resolve = (value) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseValue = value;
this.PromiseStatus = "resolved";
while (this.resolveCallbackArray.length) this.resolveCallbackArray.shift()(this.PromiseValue);
};
reject = (error) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseError = error;
this.PromiseStatus = "rejected";
while (this.rejectCallbackArray.length) this.rejectCallbackArray.shift()(this.PromiseError);
};
then(successCallback, failCallback) {
return new Promise((resolve, reject) => {
switch (this.PromiseStatus) {
case "pending":
this.resolveCallbackArray.push(successCallback);
this.rejectCallbackArray.push(failCallback);
break;
case "resolved":
resolve(successCallback(this.PromiseValue));
break;
case "rejected":
reject(failCallback(this.PromiseError));
break;
}
});
}
(2)异步情况二
情景如下:
let a = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(134);
}, 1000);
})
a.then((value) => {
console.log(value);
return value;
}).then((value) => {
console.log(value);
});
由于每次then都会返回一个promise,需要使用闭包的方式保存每一个promise,代码改造如下:
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject);
}
PromiseValue; // promise内置value
PromiseStatus = "pending"; // promise的状态分为'pending','resolved','rejected',默认状态为'pending'
PromiseError; // promise内置error
resolveCallbackArray = []; // resolve的回调函数集合
rejectCallbackArray = []; // reject的回调函数集合
resolve = (value) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseValue = value;
this.PromiseStatus = "resolved";
while (this.resolveCallbackArray.length)
this.resolveCallbackArray.shift()();
};
reject = (error) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseError = error;
this.PromiseStatus = "rejected";
while (this.rejectCallbackArray.length)
this.rejectCallbackArray.shift()();
};
then(successCallback, failCallback) {
let promise = new Promise((resolve, reject) => {
switch (this.PromiseStatus) {
case "pending":
this.resolveCallbackArray.push(() => {
setTimeout(() => {
resovlePromise(
promise,
successCallback(this.PromiseValue),
resolve,
reject
);
}, 0);
});
this.rejectCallbackArray.push(() => {
setTimeout(() => {
resovlePromise(
promise,
failCallback(this.PromiseValue),
resolve,
reject
);
}, 0);
});
break;
case "resolved":
resolve(successCallback(this.PromiseValue));
break;
case "rejected":
reject(failCallback(this.PromiseError));
break;
}
});
return promise;
}
}
// promise闭包使用的函数
function resovlePromise(promise, value, resolve, reject) {
resolve(value);
}
上面的代码还有一个需要注意的地方,为了能够在promise实例内部拿到该实例自己,需要使用setTimeout函数进行处理,因为promise在微任务执行完毕以后,才会执行宏任务中的setTimeout,这也是我们在实践中经常使用到的方法。
4、promise的错误处理
我们需要对promise内部所有执行的地方进行错误捕捉,由于这部分比较简单,直接上代码
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject);
}
PromiseValue; // promise内置value
PromiseStatus = "pending"; // promise的状态分为'pending','resolved','rejected',默认状态为'pending'
PromiseError; // promise内置error
resolveCallbackArray = []; // resolve的回调函数集合
rejectCallbackArray = []; // reject的回调函数集合
resolve = (value) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseValue = value;
this.PromiseStatus = "resolved";
while (this.resolveCallbackArray.length)
this.resolveCallbackArray.shift()();
};
reject = (error) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseError = error;
this.PromiseStatus = "rejected";
while (this.rejectCallbackArray.length)
this.rejectCallbackArray.shift()();
};
then(successCallback, failCallback) {
// 保证存在成功和失败回调函数
successCallback = successCallback
? successCallback
: (value) => value;
failCallback = failCallback
? failCallback
: (err) => {
throw err;
};
let promise = new Promise((resolve, reject) => {
switch (this.PromiseStatus) {
case "pending":
this.resolveCallbackArray.push(() => {
setTimeout(() => {
try {
resovlePromise(
promise,
successCallback(this.PromiseValue),
resolve,
reject
);
} catch (err) {
reject(err);
}
}, 0);
});
this.rejectCallbackArray.push(() => {
setTimeout(() => {
try {
resovlePromise(
promise,
failCallback(this.PromiseError
),
resolve,
reject
);
} catch (err) {
reject(err);
}
}, 0);
});
break;
case "resolved":
try {
setTimeout(() => {
resovlePromise(
promise,
successCallback(this.PromiseValue),
resolve,
reject
);
}, 0);
} catch (err) {
reject(err);
}
break;
case "rejected":
try {
setTimeout(() => {
resovlePromise(
promise,
failCallback(this.PromiseError
),
resolve,
reject
);
}, 0);
} catch (err) {
reject(err);
}
break;
}
});
return promise;
}
}
function resovlePromise(promise, value, resolve, reject) {
// 不允许返回promise自身
if (promise === value) {
// return的作用是为了中断执行
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
// 如果是一个promise对象,返回该promise内部的值
} else if (value instanceof MyPromise) {
value.then(resolve, reject);
} else {
resolve(value);
}
5、promise的一些api实现
在使用promise的时候,我们有一些内部方法,例如new Promise().catch(),new Promise().finally();还有一些静态方法Promise.all(),Promise.resolve(),下面让我们来一一实现
class MyPromise {
// 等待一个数组内的所有元素执行完毕才返回结果
static all(array) {
return new MyPromise((resolve, reject) => {
let result = new Array(array.length);
let count = 0;
let setResolve = (count, length, result) => {
if (count === length) resolve(result);
};
for (let i = 0; i < array.length; i++) {
// 参数为函数
if (array[i] instanceof Function) {
result[i] = array[i]();
count++;
setResolve(count, array.length, result);
// 参数为MyPromise的对象
} else if (array[i] instanceof MyPromise) {
array[i].then(
(value) => {
result[i] = value;
count++;
setResolve(count, array.length, result);
},
(err) => {
reject(err);
}
);
// 其他类型的参数
} else {
result[i] = array[i];
count++;
setResolve(count, array.length, result);
}
}
});
}
// 快速创建一个promise
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise((resolve, reject) => {
resolve(value);
});
}
constructor(executor) {
executor(this.resolve, this.reject);
}
PromiseValue; // promise内置value
PromiseStatus = "pending"; // promise的状态分为'pending','resolved','rejected',默认状态为'pending'
PromiseError; // promise内置error
resolveCallbackArray = []; // resolve的回调函数集合
rejectCallbackArray = []; // reject的回调函数集合
resolve = (value) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseValue = value;
this.PromiseStatus = "resolved";
while (this.resolveCallbackArray.length)
this.resolveCallbackArray.shift()();
};
reject = (error) => {
// 如果状态不是pending,阻止执行
if (this.PromiseStatus !== "pending") return;
this.PromiseError = error;
this.PromiseStatus = "rejected";
while (this.rejectCallbackArray.length)
this.rejectCallbackArray.shift()();
};
then(successCallback, failCallback) {
// 保证存在成功和失败回调函数
successCallback = successCallback
? successCallback
: (value) => value;
failCallback = failCallback
? failCallback
: (err) => {
throw err;
};
let promise = new Promise((resolve, reject) => {
switch (this.PromiseStatus) {
case "pending":
this.resolveCallbackArray.push(() => {
setTimeout(() => {
try {
resovlePromise(
promise,
successCallback(this.PromiseValue),
resolve,
reject
);
} catch (err) {
reject(err);
}
}, 0);
});
this.rejectCallbackArray.push(() => {
setTimeout(() => {
try {
resovlePromise(
promise,
failCallback(this.PromiseError),
resolve,
reject
);
} catch (err) {
reject(err);
}
}, 0);
});
break;
case "resolved":
try {
setTimeout(() => {
resovlePromise(
promise,
successCallback(this.PromiseValue),
resolve,
reject
);
}, 0);
} catch (err) {
reject(err);
}
break;
case "rejected":
try {
setTimeout(() => {
resovlePromise(
promise,
failCallback(this.PromiseError),
resolve,
reject
);
}, 0);
} catch (err) {
reject(err);
}
break;
}
});
return promise;
}
// catch沿用then方法
catch(failCallback) {
this.then(undefined, failCallback);
}
// finally方法, 不管是成功还是失败,都会执行该函数
finally(finalCallback) {
return this.then(
(value) => {
// 再增加一个then是为了考虑回调函数是一个异步调用的promise的情况
return MyPromise.resolve(finalCallback()).then(() => value);
},
(err) => {
return MyPromise.resolve(finalCallback()).then(() => {
throw err;
});
}
);
}
}
function resovlePromise(promise, value, resolve, reject) {
// 不允许返回promise自身
if (promise === value) {
// return的作用是为了中断执行
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
// 如果是一个promise对象,返回该promise内部的值
} else if (value instanceof MyPromise) {
value.then(resolve, reject);
} else {
resolve(value);
}
至此,一个类似promise的实现已经基本完成
6、async和await的实现
promise在实际应用中经常搭载async和await使用,下面来介绍一下async和await的原理:
实际上它是es6语法中generator的语法糖,那generator的特点如下:
function* gen() {
console.log(123);
let a1 = yield 123;
console.log(456);
let a2 = yield 456;
console.log(789);
return a1;
}
let result = gen();
console.log(result.next());
console.log(result.next());
console.log(result.next());
输出结果如下:
只有调用next方法,generator里面相应的yield到下一个yield之间的代码才会执行,next里面还可以传入值来给generator内部使用,上面改造一下:
function* gen() {
console.log(123);
let a1 = yield 123;
console.log(456);
let a2 = yield 456;
console.log(789);
return a1;
}
let result = gen();
console.log(result.next());
console.log(result.next('abc'));
console.log(result.next());
这个时候的输出为:
紧接着让我们根据下面的代码来重写async和await:
let b = () => {
return new Promise((resolve, reject) => {
console.log("在这里停顿一下喔");
setTimeout(() => {
resolve(456);
}, 1000);
});
};
async function a() {
console.log(123);
let result = await b();
console.log(result);
console.log(789);
}
a();
// 输出结果依次为123,456,78
generator的写法:
function* a() {
console.log(123);
let value = yield new Promise((resolve, reject) => {
console.log("在这里停顿一下喔");
setTimeout(() => {
resolve(456);
}, 1000);
});
let result = value;
console.log(result);
console.log(789);
}
let gen = a();
gen.next().value.then((value) => {
gen.next(value);
})
可以看到这两个输出是完全一样的!
有了这个例子,相信大家对于宏任务和微任务里面的那道面试题也是很清楚了!
总结
promise在实际应用中经常作为异步转化为同步的解决方案,手写一个promise类可以让我们在实际应用中能够更加得心应手。同时在前端面试中也经常会询问到宏任务和微任务执行先后顺序的相关问题,这篇文章也许可以让你更加理解内在一些执行过程。
最后,如果觉得文章写得还可以,就给笔者来个大大的赞!如果发现写得不好或者不对的地方,也欢迎给笔者留言进行更正。