彻底理解Promise对象——用es5语法实现一个自己的Promise(上篇)
本文同步自我的个人博客: http://mly-zju.github.io/
众所周知javascript语言的一大特色就是异步,这既是它的优点,同时在某些情况下也带来了一些的问题。最大的问题之一,就是异步操作过多的时候,代码内会充斥着众多回调函数,乃至形成回调金字塔。为了解决回调函数带来的问题,Promise作为一种更优雅的异步解决方案被提出,最初只是一种实现接口规范,而到了es6,则是在语言层面就原生支持了Promise对象。
最初接触Promise的时候,我觉得它是比较抽象并且令人困惑的,相信很多人也有同样的感觉。但是在后来的熟悉过程中,我慢慢体会到了它的优雅,并开始思考Promise对象实现的原理,最终用es5语法实现了一个具备基本功能的自己的Promise对象。在这篇文章中,会把自己实现的过程和思路循序渐进的记录一下,相信大家看完之后,也能够彻底理解Promise对象运行的原理,并在以后的开发中,能更熟练的使用它。
github源码地址: https://github.com/mly-zju/Js-practice
1. 回到过去: resolve, reject和then
首先来看一个Promise的使用实例:
var fn=function(resolve, reject){
console.log('begin to execute!');
var number=Math.random();
if(number<=0.5){
resolve('less than 0.5');
}else{
reject('greater than 0.5');
}
}
var p=new Promise(fn);
p.then(function(data){
console.log('resolve: ', data);
}, function(data){
console.log('reject: ', data);
})
这个例子当中,在fn当中产生一个0~1的随机数,如果小于等于0.5, 则调用resolve函数,大于0.5,则调用reject函数。函数定义好之后,用Promise包裹这个函数,返回一个Promise对象,然后调用对象的then方法,分别定义resolve和reject函数。这里resolve和reject比较简单,就是把传来的参数加一个前缀然后打印输出。
这里我们需要注意,当运行 p=new Promise(fn)这条语句的时候,fn函数就已经在执行了,然而,p.then这个方法是在后面才定义了resolve和reject,那么为何fn函数能够知道resolve和reject函数是什么呢?
换句话说,resolve和reject函数是如何回到过去,出现在先执行的fn函数当中的呢?这是Promise当中最重要的一个概念之一。
其实想要实现这个“黑科技”,方法也非常简单,主要运用的就是setTimeout这个方法,来延迟fn当中resolve和reject的执行。利用这个思路,我们可以初步写出一个自己的初级版Promise,这里我们命名为MyPromise:
function MyPromise(fn) {
this.value;
this.resolveFunc = function() {};
this.rejectFunc = function() {};
fn(this.resolve.bind(this), this.reject.bind(this));
}
MyPromise.prototype.resolve = function(val) {
var self = this;
self.value=val;
setTimeout(function() {
self.resolveFunc(self.value);
}, 0);
}
MyPromise.prototype.reject = function(val) {
var self=this;
self.value=val;
setTimeout(function() {
self.rejectFunc(self.value);
}, 0);
}
MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
this.resolveFunc = resolveFunc;
this.rejectFunc = rejectFunc;
}
var fn=function(resolve, reject){
console.log('begin to execute!');
var number=Math.random();
if(number<=0.5){
resolve('less than 0.5');
}else{
reject('greater than 0.5');
}
}
var p = new MyPromise(fn);
p.then(function(data) {
console.log('resolve: ', data);
}, function(data) {
console.log('reject: ', data);
});
可以看出, MyPromise接收fn函数,并将自己的this.resolve和this.reject方法作为fn的resolve和reject参数传给fn并执行。而我们观察MyPromise的resolve方法,便可以发现,其主要操作,就是使用setTimeout,延迟0秒执行resolveFunc。
而再来观察then方法,可以看到,这里比较简单,就是接受两个函数,并分别赋给自身的this.resolveFunc和this.rejectFunc。
这里逻辑就很清楚了,虽然fn函数首先执行,但是由于在调用resolve和reject的时候,使用了setTimeout。虽然是延迟0秒执行,但是我们知道js是单线程+消息队列,必须等主线程代码执行完毕才能开始执行消息队列当中的代码。因此,会首先执行then这个方法,给resolveFunc和rejectFunc赋值。then执行完毕后,再执行setTimeout里面的方法,这个时候,resolveFunc和rejectFunc已经被赋值了,所以就可以顺利执行。这就是“回到过去”的奥秘所在。
2. 加入状态: pending, resolved, rejected
上一节,初步实现了看起来似乎能够运行的MyPromise,但是问题很多。我们看一下下面代码:
var fn=function(resolve, reject){
resolve('hello');
reject('hello again');
}
var p1=new Promise(fn);
p1.then(function(data){
console.log('resolve: ',data)
}, function(data){
console.log('reject: ',data)
});
//'resolve: hello'
var p2=new MyPromise(fn);
p2.then(function(data){
console.log('resolve: ',data)
}, function(data){
console.log('reject: ',data)
});
//'resolve: hello '
//'reject: hello again'
p1是原生Promise,p2是我们自己写的,可以看出,当调用resolve之后再调用reject,p1只会执行resolve,我们的则是两个都执行。事实上在Promise规范当中,规定Promise只能从初始pending状态变到resolved或者rejected状态,是单向变化的,也就是说执行了resolve就不会再执行reject,反之亦然。
为此,我们需要在MyPromise中加入状态,并在必要的地方进行判断,防止重复执行:
function MyPromise(fn) {
this.value;
this.status = 'pending';
this.resolveFunc = function() {};
this.rejectFunc = function() {};
fn(this.resolve.bind(this), this.reject.bind(this));
}
MyPromise.prototype.resolve = function(val) {
var self = this;
if (this.status == 'pending') {
this.status = 'resolved';
this.value=val;
setTimeout(function() {
self.resolveFunc(self.value);
}, 0);
}
}
MyPromise.prototype.reject = function(val) {
var self = this;
if (this.status == 'pending') {
this.status = 'rejected';
this.value=val;
setTimeout(function() {
self.rejectFunc(self.value);
}, 0);
}
}
MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
this.resolveFunc = resolveFunc;
this.rejectFunc = rejectFunc;
}
这样,再次运行上面的实例,就不会出现resolve和reject都执行的情况了。
3. 链式调用
在Promise的使用中,我们一定注意到,是可以链式调用的:
var fn=function(resolve, reject){
resolve('hello');
}
var p1=new Promise(fn);
p1.then(function(data){
console.log(data);
return 'hello again';
}).then(function(data){
console.log(data);
});
//'hello'
//'hello again'
很显然,要实现链式调用,then方法的返回值也必须是一个Promise对象,这样才能再次在后面调用then。因此我们修改MyPromise的then方法:
MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
var self = this;
return new MyPromise(function(resolve_next, reject_next) {
function resolveFuncWrap() {
var result = resolveFunc(self.value);
resolve_next(result);
}
function rejectFuncWrap() {
var result = rejectFunc(self.value);
resolve_next(result);
}
self.resolveFunc = resolveFuncWrap;
self.rejectFunc = rejectFuncWrap;
})
}
这里可以看出,then返回了一个MyPromise对象。在这个MyPromise当中,包裹了一个函数,这个函数会立即执行,主要做的事情,就是对resolveFunc和rejectFunc进行封装,然后再赋值给前一个MyPromise的resolveFunc和rejectFunc。这里难点是看懂封装的目的。
这里以上面一个例子来说明。在上面的链式调用例子中,出现了两个Promise,第一个是我们通过new Promise显式定义的,我们叫它Promise 1,而第二个Promise,是Promise 1的then方法返回的一个新的,我们叫它Promise 2 。在Promise 1的resolve方法执行之后,resolve的返回值,会传递给Promise 2的resolve作为参数,这也是为什么上面第二个then中打印出了第一个then返回的字符串。
而我们封装的目的,就是为了让Promise 1的resolve或者reject在执行后,将其返回值传递给Promise 2的resolve。在我们自己的实现中,Promise 2的resolve我们命名为resolve_next,在Promise 1的resolveFunc执行之后,我们拿到返回值result,然后调用resolve_next(result),传递参数给Promise 2的resolve。这里值得注意的是,无论Promise 1执行的是resolveFunc还是rejectFunc,其之后调用的,都是Promise 2的resolve,至于Promise 2的reject用来干嘛,在下面的章节里面我们会详细描述。
至此,我们的MyPromise看起来就可以使用链式调用了。
然而我们再回去观察Promise规范,会发现链式调用的情况也分两种。一种情况下,前一个Promise的resolve或者reject的返回值是普通的对象,这种情况下我们目前的MyPromise可以正确处理。但还有一种情况,就是前一个Promise的resolve或者reject执行后,返回的值本身又是一个Promise对象,举个例子:
var fn=function(resolve, reject){
resolve('hello');
}
var p1=new Promise(fn);
p1.then(function(data){
console.log(data);
return 'hello again';
}).then(function(data){
console.log(data);
return new Promise(function(resolve){
var innerData='hello third time!';
resolve(innerData);
})
}).then(function(data){
console.log(data);
});
//'hello'
//'hello again'
//'hello third time!'
在这个例子当中出现了两次链式调用,第一个then返回的是一个'hello again'字符串,在第二个then的resolve中会打印处理。然后我们注意第二个then当中,返回的是一个Promise对象,调用了resolve。那么问题来了,这个resolve哪里来呢?答案就是在第三个then当中定义!这个例子中第三个then定义的resolve也比较简单,就是直接打印传给resolve的参数。
因此,这里我们的MyPromise也需要修改,针对前一个resolve或者reject的返回值做判断,看是不是Promise对象,如果是,就做不同的处理,修改的代码如下:
MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
var self = this;
return new MyPromise(function(resolve_next, reject_next) {
function resolveFuncWrap() {
var result = resolveFunc(self.value);
if (result && typeof result.then === 'function') {
//如果result是MyPromise对象,则通过then将resolve_next和reject_next传给它
result.then(resolve_next, reject_next);
} else {
//如果result是其他对象,则作为参数传给resolve_next
resolve_next(result);
}
}
function rejectFuncWrap() {
var result = rejectFunc(self.value);
if (result && typeof result.then === 'function') {
//如果result是MyPromise对象,则通过then将resolve_next和reject_next传给它
result.then(resolve_next, reject_next);
} else {
//如果result是其他对象,则作为参数传给resolve_next
resolve_next(result);
}
}
self.resolveFunc = resolveFuncWrap;
self.rejectFunc = rejectFuncWrap;
})
}
可以看到在代码中,对于resolveFunc或者rejectFunc的返回值,我们会判断是否含有.then方法,如果含有,就认为是一个MyPromise对象,从而调用该MyPromise的then方法,将resolve_next和reject_next传给它。否则,正常对象,result就作为参数传给resolve_next。
这样修改之后,我们的MyPromise就可以在链式调用中正确的处理普通对象和MyPromise对象了。
如此,在这篇文章中,我们就首先实现了Promise的常用基本功能,主要是then的调用,状态的控制,以及链式调用。而在后面的文章中,还会进一步讲解如何实现Promise的错误捕获处理等等(比如Promise当中的.catch方法原理),从而让我们的MyPromise真正健壮和可用!
彻底理解Promise对象——用es5语法实现一个自己的Promise(上篇)的更多相关文章
- promise对象里resolve和reject状态讲解及Promise.all()的使用
首先来说下同步异步与阻塞非阻塞的概念,同步异步与阻塞非阻塞并没有关系.同步异步主要是事情做完以后,如何进行处理.或者说关注的是一种消息通信机制. 同步的情况下,是由处理消息者自己去等待消息是否被触发: ...
- ES6之Promise对象学习——8个例子学会Promise
目录 Promise 立即执行 Promise 三种状态 Promise 不可逆性 链式调用 Promise.then()回调异步性 Promise中的异常 Promise.resolve() res ...
- 大白话理解promise对象
Promise 代表了未来某个将要发生的事件(通常是一个异步操作) Promise 是异步编程的解决方案,能够简化多层回调嵌套,代表了未来某个将要发生的事件.Promise是一个构造函数,本身有a ...
- 前端笔记之ES678&Webpack&Babel(下)AMD|CMD规范&模块&webpack&Promise对象&Generator函数
一.AMD和CMD规范(了解) 1.1传统的前端开发多个js文件的关系 yuan.js中定义了一个函数 function mianji(r){ return 3.14 * r * r } main.j ...
- ES6 - 基础学习(8): Promise 对象
概述 Promise是异步编程的一种解决方案,比传统的解决方案(多层嵌套回调.回调函数和事件)更强大也更合理.从语法上说,Promise是一个对象,从它可以获取异步操作的消息,Promise 还提供了 ...
- Promise对象 异步编程
Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大.所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是 ...
- JavaScript异步编程(1)- ECMAScript 6的Promise对象
JavaScript的Callback机制深入人心.而ECMAScript的世界同样充斥的各种异步操作(异步IO.setTimeout等).异步和Callback的搭载很容易就衍生"回调金字 ...
- Promise对象
1.Promise思想:每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程.这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用. ...
- 通过一道笔试题浅谈javascript中的promise对象
因为前几天做了一个promise对象捕获错误的面试题目,所以这几天又重温了一下promise对象.现在借这道题来分享下一些很基础的知识点. 下面是一个面试题目,三个promise对象捕获错误的例子,返 ...
随机推荐
- Ubuntu 安装wireshark
参考:ubuntu下安装wireshark 依赖及相关包的安装 1.编译工具 apt-get install build-essential 2.GTK+的开发文件和GLib库(libraries) ...
- C# 安装包制作
VS制作安装包的一般步骤. 一·新建项目 (1)新建 (2)界面跳转 二·添加引用 (1)添加卸载程序 1.在'C:WINDOWSsystem32'路径下,找到msiexec.exe . 2.将msi ...
- HTML <head>
HTML <head> 元素 <head> 元素包含了所有的头部标签元素.在 <head>元素中你可以插入脚本(scripts), 样式文件(CSS),及各种met ...
- 排名前10的H5、Js 3D游戏引擎和框架
由于很多人都在用JavaScript.HTML5和WebGL技术创建基于浏览器的3D游戏,所有JavaScript 3D游戏引擎是一个人们主题.基于浏览器的游戏最棒的地方是平台独立,它们能在iOS.A ...
- .Net多线程编程—误用点分析
1 共享变量问题 错误写法: 所有的任务可能会共享同一个变量,所以输出结果可能会一样. public static void Error() { ;i<;i++) { Task.Run(() = ...
- HTML5行业现状与未来 - 2016年终大盘点
* { margin: 0; padding: 0 } .con { width: 802px; margin: 0 auto; text-align: center; position: inher ...
- Java虚拟机(JVM)默认字符集详解
Java中对字符串等进行转换字节数组时, 需要根据字符集编码来进行转换, 当不显示的指定字符集编码时(如: "测试".getBytes()), 会使用Charset.default ...
- SVN-TortoiseSVN安装和常用操作步骤
安装VisualSVN-Server-2.0.5 服务端: 运行VisualSVN-Server-2.0.5.msi程序,点击Next,下面的截图顺序即为安装步骤: 2 图2: 注意:Server P ...
- iOS真机测试友盟碰到错误linker command failed with exit code 1 (use -v to see invocation) 百度地图的检索失败 sqlite 错误码
因为友盟不支持bitcode 在模拟器上运行正常,但是在模拟器上就会报错,这是因为xocde7之后增加了一个bitcode,bitcode是被编译程序的一种中间形式的代 码.包含bitcode配置的程 ...
- 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数
关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...