动手搞一个Promise

打虎将
• 阅读 1072

动手搞一个Promise

Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

异步模式"编程的4种方法:回调函数、事件监听、发布/订阅、Promises对象
还有generator、async/await.

本文尝试说一下对Promise的理解及如何实现。
1.Promise原理

promise对象有三种状态,pending、fulfilled和rejected。promise对象内部保存一个需要执行一段时间的异步操作,当异步操作执行结束后可以调用resolve或reject方法,来改变promise对象的状态,状态一旦改变就不能再变。new一个promise后可以通过then方法,指定resolved和rejected时的回调函数。下面是我们日常使用Promise的代码逻辑。
let p = new Promise((resolve,reject)=>{

    $.ajax({
    url: "../www/data.txt",
    dataType: "json",
    success(data){
        resolve(data);
     },
    error(err){
        reject(err);
     }
   }); 
});
p.then(function(data){
    alert("success"+data);
},function(err){
    alert("failed");
})

结合Promise A+规范,我们就可以分析一下我们要实现一个什么东西:

实现一个状态机,有三个状态,pending、fulfilled、rejected,状态之间的转化只能是pending->fulfilled、pending->rejected,状态变化不可逆。
实现一个then方法,可以用来设置成功和失败的回调
then方法要能被调用多次,所以then方法需要每次返回一个新的promise对象,这样才能支持链式调用。
构造函数内部要有一个value值,用来保存上次执行的结果值,如果报错,则保存的是异常信息。
2.实现原理

2.1实现状态机

那我们现在就按照上面提到的原理和规范来实现这个Promise构造函数。
class myPromise {

    constructor(executor) {
        this.status = PENDING;
        this.value = '';
        this.Resolve = this.resolve.bind(this);
        this.Reject = this.reject.bind(this);
        this.then= this.then.bind(this);
        executor(this.Resolve, this.Reject);
    }

    resolve(value) {
        if (this.status === PENDING) {
            this.value = value;
            this.status = FULFILLED;
        }
    }

    reject(value) {
        if (this.status === PENDING) {
            this.value = value;
            this.status = REJECTED;
        }
    }

    then(onfulfilled, onrejected) {
        if (this.status === FULFILLED) {
            onfulfilled(this.value);
        }
        if (this.status === REJECTED) {
            onrejected(this.value);
        }
    }
}

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

const test = new myPromise((resolve, reject) => {
    resolve(100);
});
test.then((data) => {
    console.log(data);
}, (data) => {
});

因为Promise是一个构造函数,使用ES6的写法,首先想到的就是有显式constructor声明的class。上面就是我们用class的实现,可以看到这样我们就实现了这个状态机,有status, value两个属性和resolve, reject, then三个函数;同时它有pending, fulfilled和rejected三个状态,其中pending就可以切换为fulfilled或者rejected两种。

运行一下,输出了100,但是现在其实不是一个异步处理方案,代码先运行了resolve(100)然后又运行了then函数,其实对于异步的情况没有处理,不信的话就给resolve加一个setTimeout,好了,代码又没有输出了。

2.2 实现异步设置状态

作为一个异步处理的函数,在使用的时候,我们肯定是会先设置好不同异步返回后的处理逻辑(即then的成功、失败调用函数),然后安心等待异步执行,最后再异步结束后,系统会自动根据我们的逻辑选择调用不同回调函数。换句话说,then函数要对status为pending的状态进行处理。处理的原理是设置两个数组,在pending状态下分别保存成功和失败回调函数,当状态改变后,再根据状态去调用数组中保存的回调函数。

class myPromise {
constructor (executor) {

this.status = PENDING;
this.value = '';
this.onfulfilledArr = [];
this.onrejectedArr = [];
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
this.then = this.then.bind(this);
executor(this.resolve, this.reject);

}

resolve (value) {

if (this.status === PENDING) {
  this.value = value;
  this.onfulfilledArr.forEach(item => {
    item(this.value);
  })
  this.status = FULFILLED; 
}

}

reject (value) {

if (this.status === PENDING) {
  this.value = value;
  this.onrejectedArr.forEach(item => {
    item(this.value);
  })
  this.status = REJECTED;
}

}

then (onfulfilled, onrejected) {

if (this.status === FULFILLED) {
  onfulfilled(this.value);
}
if (this.status === REJECTED) {
  onrejected(this.value);
}
if (this.status === PENDING) {
  this.onfulfilledArr.push(onfulfilled);
  this.onrejectedArr.push(onrejected);
}

}
}

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

const test = new myPromise((resolve, reject) => {
setTimeout(() => {

resolve(100);

}, 2000)
});
test.then((data) => {
console.log(data);
},(data) => {});
可以这样理解

new myPromise 有异步代码

setTimeout(() => {
    resolve(100);
}, 2000)

js是单线程的,这个时候会先把这个任务添加到定时触发器线程中去(计时完毕后添加到事件队列中,等待js引擎空闲后执行),先去执行下面的同步代码
test.then((data) => {

    console.log(data);

},(data) => {});
完成输出及状态改变。

但是Promise的一大特点就是可以链式调用,即test.then(success, fail).then(success, fail)...这就需要then返回一个新的Promise对象,而我们的程序现在明显的是不支持的。那么继续改一下。

2.3 实现链式调用

再观察一下链式调用,如果成功和失败的函数中有返回值,这个值要作为参数传给下个then函数的成功或失败回调。所以我们要在返回的new Promise中调用相应的函数。

class myPromise {
constructor (executor) {

this.status = PENDING;
this.value = '';
this.onfulfilledArr = [];
this.onrejectedArr = [];
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
this.then = this.then.bind(this);
executor(this.resolve, this.reject);

}

resolve (value) {

if (this.status === PENDING) {
  this.value = value;
  this.onfulfilledArr.forEach(item => {
    item(this.value);
  })
  this.status = FULFILLED; 
}

}

reject (value) {

if (this.status === PENDING) {
  this.value = value;
  this.onrejectedArr.forEach(item => {
    item(this.value);
  })
  this.status = REJECTED;
}

}

then (onfulfilled, onrejected) {

if (this.status === FULFILLED) {
  const res = onfulfilled(this.value);
  return new Promise(function(resolve, reject) {
    resolve(res);
  })
}
if (this.status === REJECTED) {
  const res = onrejected(this.value);
  return new Promise(function(resolve, reject) {
    reject(res);
  })
}
if (this.status === PENDING) {
  const self = this;
  return new Promise(function(resolve, reject) {
    self.onfulfilledArr.push(() => {
      const res = onfulfilled(self.value)
      resolve(res);
    });
    self.onrejectedArr.push(() => {
      const res = onrejected(self.value)
      reject(res);
    });
  })
}

}
}

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

const test = new myPromise((resolve, reject) => {
setTimeout(() => {

resolve(100);

}, 2000)
});
test.then((data) => {
console.log(data);
return data + 5;
},(data) => {})
.then((data) => {
console.log(data)
},(data) => {});

再运行一下,输出100,105。好了,一个简单的Promise就实现好了。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Karen110 Karen110
4年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
菜园前端 菜园前端
2年前
什么是 JavaScript 单线程?
原文链接:什么是JavaScript单线程?单线程是指当存在多个任务时候,所有任务都必须排队并且按照顺序执行。这里就会有同学问为什么不是多线程?假设是多线程,同时执行一些任务,某个任务进行了该dom的修改,而某个任务进行了该dom的删除,这样就会出现一些问
Peter20 Peter20
4年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
4年前
Android蓝牙连接汽车OBD设备
//设备连接public class BluetoothConnect implements Runnable {    private static final UUID CONNECT_UUID  UUID.fromString("0000110100001000800000805F9B34FB");
Java服务总在半夜挂,背后的真相竟然是... | 京东云技术团队
最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这