JavaScript进阶之手写Promise

栏目: JavaScript · 发布时间: 5年前

内容简介:Promise是当前ES6语法中的异步解决方案,本文从基本概念开始,层层分析,一步一步手写实现Promise,希望能和大家一起彻底掌握Promise。Promise是异步编程的一种解决方案,跟传统回调函数来实现异步相比,其设计更合理,代码更易懂。Promise是"美颜后"的异步解决方案.在ES6中, 其写进了语言标准提供原生支持。

Promise是当前ES6语法中的异步解决方案,本文从基本概念开始,层层分析,一步一步手写实现Promise,希望能和大家一起彻底掌握Promise。

概述

Promise是异步编程的一种解决方案,跟传统回调函数来实现异步相比,其设计更合理,代码更易懂。Promise是"美颜后"的异步解决方案.

在ES6中, 其写进了语言标准提供原生支持。

Promise有两大特点:

  1. 对象的状态不受外界的影响,Promise对象代表一个异步操作,有三种状态: pending  , fulfilled  , rejected
  2. Promise是一个状态机,一旦状态改变,就不会再变,并且在任何时候都可以得到这个结果。
JavaScript进阶之手写Promise

上图摘自MDN

Promise的具体用法可以参考阮一峰老师的《ECMAScript 6 入门》或 MDN

这里有几个点需要注意:

  1. Promise可以有finally,用于做跟状态无关的业务逻辑操作。
  2. Promise中定义的函数在resolve之后还是可以执行语句,所以建议在resolve前加上return
new Promise((resolve, reject) => {
  resolve(1);//为了防止输出2,改成return resolve,日常编码中也要养成return resolve的习惯
  console.log(2);// 可以输出2
}).then(r => {
  console.log(r);
});
复制代码
  1. then方法中的第二个参数 reject=>{}  可以省略,建议使用 catch(error=>{})  ,其不仅能够获取reject的内容,还可以捕获到Promise函数和then函数中产生的异常。Promise内部的异常会被吃掉,不会被外部的异常捕获。
  2. 注意Promise.all和Promise.race的用法及使用场景。

如果熟悉了Promise的使用,其实我们知道,Promise提供了异步编程的语法糖,使原来异步回调的操作可以用同步的方式来表达。

回调地狱

在传统AJAX异步解决方案中,我们一般使用回调函数来解决数据的接收和处理,如下:

$.get(url, (data) => {
    console.log(data)
)
复制代码

在某些需求场景下,我们需要发送多个异步请求,并且每个请求之间结果之间需要相互依赖,随着回调函数相互嵌套的增加,函数之间的逻辑就会耦合在一起,难以维护,形成 回调地狱 。如下所示:

let country = 'china';
let city = 'shanghai';
let district = 'PD'
$.get(`xxxxx/countries`,countries=>{
  /**
  **这里可以再第一个select控件中,渲染国家列表,
  **/
  countries.forEach((item)=>{
  		if(item===country){
        //查找并选中当前国家
        $.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
             /**
              **这里可以再第二个select控件中,渲染城市列表,
              **/
            cities.forEach((item)=>{
              if(item === city){
               //查找并选中当前城市
               $.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
											  /**
                        **这里可以再第三个select控件中,渲染地区列表,
                        **/
                 			dists.forEach(item=>{
                      	if(item==district){
                        	 //查找并选中地区
                        }
                      })
               })
              }
            })
        })
      }
  });
});
复制代码

上述是一个简单的三级联动功能,使用三个回调函数。它们相互嵌套逻辑复杂,耦合严重。

Promise解决了回调地狱问题,通过Promise将上述代码改写成

let country = 'china';
let city = 'shanghai';
let district = 'PD'
new Promise(() => {
    $.get(`xxxxx/countries`, countries => {
        return countries;
    });
}).then((countries) => {
    countries.forEach((item) => {
        if (item === country) {
            $.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
                return cities;
            })
        }
    })
}).then((cities) => {
    cities.forEach((item) => {
        if (item === city) {
            $.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
                return dists;
            })
        }
    })
}).then((dists) => {
    dists.forEach(item => {
        if (item == district) {
            //查找并选中地区
        }
    })
})
复制代码

此时,将异步执行由原来的回调,改成了 then...then....then... 链式调用的方式。

线性的链式执行(同步)更符合人类的思考习惯(更直白的说,按照顺序一步一步的闭环符合人类思考习惯。Promise就是将原本异步回调的语法形式,改写成同步。所以实现Promise,就是把异步回调这种丑陋的方式改成链式调用。通过手写Promise,我们来理解和消化其设计思想。

开始

有了上述的铺垫,我们了解Promise的概念和特征,也知道了Promise的优势,下面我们一步步来实现Promise。

  1. Promise是一个 构造函数 ,并且传入的参数是一个 函数 ,并且该函数在构造函数中 执行
function Promise(executor){
	try{
    executor()
  }catch(e){
  	console.log(e):
  }
}
复制代码
  1. executor函数的两个参数 resolvereject ,是executor函数中的回调函数。
function Promise(executor){
  function resolve(value){
    //可以将executor中的数据传入resolve中
  }
  function reject(value){
    //可以将executor中的数据传入reject中
  }
	try{
    executor(resolve,reject)
  }catch(e){
  	console.log(e):
  }
}
复制代码
  1. Promise实现了状态机,在执行resolve时,由 PENDING=>FULFILLED  ,在执行reject时,由 PENDING=>REJECTED  。
const pending = 'PENDING';
const rejecting = 'REJECTED';
const fulfilled = 'FULFILLED';
function Promise(executor){
  var that = this;
  that.status = pending;
  that.value = null;
  that.error = null;
  function resolve(val){
    //当且仅当PENDING=》FULFILLED
    if(that.status === pending){
      that.status = fulfilled;
    	that.value = val;
    }
  }
  function reject(val){
    //当且仅当PENDING=》REJECTED
    if(that.status === pending){
      that.status = rejecting;
    	that.error = val;
    }
  }
  try{
    executor(resolve,reject);
  }catch(e){
    //在executor中产生的异常在reject中可以捕获。但是reject的异常,智能catch捕获
  	reject(e);
  }
}

Promise.prototype.then = function(onFulfilled, onRejected){
	var that = this;
  if(that.status === fulfilled){
    //当状态改变后,执行then中的回调
  	onFulfilled(that.value);
  }
  if(that.status === rejecting){
    //同上
    onRejected(that.error)
  }
};
复制代码

执行如下代码

new Promise((resolve)=>{
    resolve(1);
  }).then((res)=>{
    console.log(res);
  });
复制代码

打印结果如下

JavaScript进阶之手写Promise
  1. Promise是异步的

如果executor函数存在异步,则需要等待 resolve 或者 reject 回调执行才会执行 then 中的函数体。

此处使用回调来解决异步:

第一步:定义两个Promise的成员:onResolvedCallBack和onRejectCallBack,存储then函数中的入参。

第二步:当执行then函数时,如果当前状态还是PENDING(此时executor内部有异步操作)那么就将then中的参数传入onResolvedCallBack和onRejectCallBack中。如果此时状态是非PENDING,那么直接执行传入的函数即可。

第三步:Promise中的resolve函数执行时,触发onResolvedCallBack或者onRejectCallBack中的函数。

具体代码如下:

const pending = 'PENDING';
const rejecting = 'REJECTED';
const fulfilled = 'FULFILLED';
function Promise1(executor) {
  var that = this;
  that.status = pending;
  that.value = null;
  that.error = null;
  that.resolvedCallbacks = [];
  that.rejectedCallbacks = [];
  
  function resolve(val) {
    if (that.status === pending) {
      that.status = fulfilled;
      that.value = val;
      that.resolvedCallbacks.map(cb => cb(that.value));
    }
  }
  
  function reject(val) {
    if (that.status === pending) {
      that.status = rejecting;
      that.error = val;
      that.rejectedCallbacks.map(cb => cb(that.value));
    }
  }
  
  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}


Promise1.prototype.then = function (onFulfilled, onRejected) {
  var that = this;
  //为了保证兼容性,then的参数只能是函数,如果不是要防止then的穿透问题
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : r => {
          throw r
        }
  if (that.status === pending) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }
  
  if (that.status === fulfilled) {
    onFulfilled(that.value);
  }
  
  if (that.status === rejecting) {
    onRejected(that.error)
  }

};
复制代码

执行如下一段代码:

let p = new Promise((resolve) => {
   setTimeout(() => {
      resolve(1);
   }, 3000)
});

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

console.log(2);
复制代码

打印结果如下:

JavaScript进阶之手写Promise

我们再来执行如下代码

let p = new Promise((resolve) => {
  resolve(1);
});

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

console.log(2);
复制代码

此处我们打印结果为

JavaScript进阶之手写Promise

但是真正的Promise打印结果为:先2后1

  1. Promise的then属于microtasks

microtask和macrotask是Event Loop的知识点,关于Event Loop可以参考阮一峰老师的 《JavaScript 运行机制详解:再谈Event Loop》

此处我们使用setTimeout来模拟then的microtasks(注:)

function resolve(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
function reject(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
复制代码
  1. resolve支持传入Promise对象

我们执行如下代码:

let p = new Promise((resolve) => {
  var a = new Promise((resolve) => { resolve(1) });
  resolve(a);
});

p.then((res) => {
  console.log(res);
});
复制代码

此处resolve传入的是Promise对象,打印结果为:

JavaScript进阶之手写Promise

所以在resolve函数中需要对value做一次判断

function resolve(value) {
  if (val instanceof Promise) {
    return val.then(resolve, reject);
  }
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
复制代码
  1. then可以链式调用

在Promise中,在then中执行return语句,返回的一定是Promise对象,这也是then能够链式调用的原因。

首先我们将then中的如下片段

if (that.status === pending) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }
复制代码

变形

if (that.status === pending) {
  that.resolvedCallbacks.push(()=>{onFulfilled(that.value)});
  that.rejectedCallbacks.push(()=>{onRejected(that.value)});
}
复制代码

它们之间只是写法的差异,效果相同。

因为我们需要对then里传入的函数onFulfilled, onRejected返回的值进行判断,所以我们需要对then继续改写

if (that.status === pending) {
  that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
  that.rejectedCallbacks.push(()=>{const x = onRejected(that.value)});
}
复制代码

因为then返回的是Promise,所以继续完善

if (that.status === pending) {
  return new Promise(resolve,reject){
  	that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
  	that.rejectedCallbacks.push(()=>{const x = onRejected(that.error)});
  }
}
复制代码

执行onFulfilled和onRejected时,使用try...catch...,所以继续完善

let promise2 = null;
if (that.status === pending) {
  return promise2 = new Promise((resolve,reject)=>{
    that.resolvedCallbacks.push(()=>{
      try{
        const x = onFulfilled(that.value);
      }catch(e){
        reject(e);
      }
    });
    
    that.rejectedCallbacks.push(()=>{
      try{
        const x = onRejected(that.error);
      }catch(e){
        reject(e);
      }
    });
  });
}
复制代码

上述x是onFulfilled(that.value)和onRejected(that.error)的返回值,为了保证then可以链式调用,也就是promise2的resolve能够resolve一个Promise对象,但是x返回的可能是Promise对象,可能是值,也可能是函数,那么此处需要对x进行适配一下。此时引入resolvePromise函数,实现如下:

/**
 * 对resolve 进行改造增强 针对x不同值情况 进行处理
 * @param  {promise} promise2 promise1.then方法返回的新的promise对象
 * @param  {[type]} x         promise1中onFulfilled的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {  
        // 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
        return reject(new TypeError('循环引用'));
    }

    let called = false; // 避免多次调用
    // 如果x是一个promise对象 (该判断和下面 判断是不是thenable对象重复 所以可有可无)
    if (x instanceof Promise) { // 获得它的终值 继续resolve
        if (x.status === PENDING) { // 如果为等待态需等待直至 x 被执行或拒绝 并解析y值
            x.then(y => {
                resolvePromise(promise2, y, resolve, reject);
            }, reason => {
                reject(reason);
            });
        } else { 
            // 如果 x 已经处于执行态/拒绝态(值已经被解析为普通值),用相同的值执行传递下去 
            x.then(resolve, reject);
        }
        // 如果 x 为对象或者函数
    } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
        try { // 是否是thenable对象(具有then方法的对象/函数)
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, reason => {
                    if(called) return;
                    called = true;
                    reject(reason);
                })
            } else { // 说明是一个普通对象/函数
                resolve(x);
            }
        } catch(e) {
            if(called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

复制代码

此时that.status === pending的代码块也要继续修改

if (that.status === pending) {
        return promise = new Promise1((resolve, reject) => {
            that.resolvedCallbacks.push(() => {
                try {
                    const x = onFulfilled(that.value);
                    resolvePromise(promise,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                
                that.rejectedCallbacks.push(() => {
                    try {
                        const x = onRejected(that.error);
                    } catch (e) {
                        reject(e);
                    }
                });
            })
    }	
复制代码

同理that.status===fulfilled和that.status===rejecting的时候代码如下:

if (that.status === FULFILLED) { // 成功态
        return promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try{
                    let x = onFulfilled(that.value);
                    resolvePromise(promise2, x, resolve, reject); 
                } catch(e) {
                    reject(e); 
                }
            });
        })
    }

    if (that.status === REJECTED) {
        return promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }

复制代码
  1. Promise的all和race

Promise.all的用法如下

const p = Promise.all([p1, p2, p3]).then((resolve)=>{},(reject)=>{});
复制代码

Promise.all方法接受一个数组作为参数,只有当数组的所有Promise对象的状态全部fulfilled,才会执行后续的then方法。

根据all的用法和特点,我们推测Promise.all返回一个Promise对象,在Promise对象中去等待Promise数组对象的函数执行完毕,数组中的每个对象执行完毕都+1,当等于数组的长度时,resolve数组对象中所有resolve出来的值。

Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
                //每执行一次都会往values数组中放入promise对象数组成员resolve的值
                //当values的长度等于promise对象数组的长度时,resolve这个数组values
            }, reject)
        })
    })
}

//使用闭包,count和values在函数的声明周期中都存在
function gen(length, resolve) {
    let count = 0;
    let values = [];
    return function(i, value) {
        values[i] = value;
        if (++count === length) {
            console.log(values);
            resolve(values);
        }
    }
}
复制代码

Promise.race的用法和all类似,区别就是promise对象数组其中有一个fulfilled,就执行then方法,实现如下

Promise.race = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
           promise.then(resolve, reject);
        });
    });
}
复制代码

需要注意的是all和race函数中的数组成员不一定是Promise对象,如果不是Promise提供了resolve方法将其转化成Promise对象。resolve的实现很简单如下:

Promise.resolve = function (value) {
    return new Promise(resolve => {
        resolve(value);
    });
}
复制代码

至此一个比较规范的Promise实现了。

参考


以上所述就是小编给大家介绍的《JavaScript进阶之手写Promise》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

枕边算法书

枕边算法书

[韩] 林栢濬 / 崔盛一 / 人民邮电出版社 / 2018-3 / 45.00元

本书第1章重点讲解各种常见算法,第2章主要介绍几种相对少见的算法,第3章和第4章探究其他程序员编写的代码,从中总结优秀算法应具备的特点,以及高级程序员应当持有的态度和必须培养的能力。书中以日常对话般浅显的叙述方式,帮助专业开发人员、刚刚踏入软件开发和编程门槛的初学者体会程序设计的创造性和成就感。一起来看看 《枕边算法书》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具