JavaScript进阶之手写Promise
栏目: JavaScript · 发布时间: 5年前
内容简介:Promise是当前ES6语法中的异步解决方案,本文从基本概念开始,层层分析,一步一步手写实现Promise,希望能和大家一起彻底掌握Promise。Promise是异步编程的一种解决方案,跟传统回调函数来实现异步相比,其设计更合理,代码更易懂。Promise是"美颜后"的异步解决方案.在ES6中, 其写进了语言标准提供原生支持。
Promise是当前ES6语法中的异步解决方案,本文从基本概念开始,层层分析,一步一步手写实现Promise,希望能和大家一起彻底掌握Promise。
概述
Promise是异步编程的一种解决方案,跟传统回调函数来实现异步相比,其设计更合理,代码更易懂。Promise是"美颜后"的异步解决方案.
在ES6中, 其写进了语言标准提供原生支持。
Promise有两大特点:
- 对象的状态不受外界的影响,Promise对象代表一个异步操作,有三种状态:
pending
,fulfilled
,rejected
- Promise是一个状态机,一旦状态改变,就不会再变,并且在任何时候都可以得到这个结果。
上图摘自MDN
Promise的具体用法可以参考阮一峰老师的《ECMAScript 6 入门》或 MDN 。
这里有几个点需要注意:
- Promise可以有finally,用于做跟状态无关的业务逻辑操作。
- Promise中定义的函数在resolve之后还是可以执行语句,所以建议在resolve前加上return
new Promise((resolve, reject) => { resolve(1);//为了防止输出2,改成return resolve,日常编码中也要养成return resolve的习惯 console.log(2);// 可以输出2 }).then(r => { console.log(r); }); 复制代码
- then方法中的第二个参数
reject=>{}
可以省略,建议使用catch(error=>{})
,其不仅能够获取reject的内容,还可以捕获到Promise函数和then函数中产生的异常。Promise内部的异常会被吃掉,不会被外部的异常捕获。 - 注意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。
- Promise是一个 构造函数 ,并且传入的参数是一个 函数 ,并且该函数在构造函数中 执行 。
function Promise(executor){ try{ executor() }catch(e){ console.log(e): } } 复制代码
- executor函数的两个参数 resolve 和 reject ,是executor函数中的回调函数。
function Promise(executor){ function resolve(value){ //可以将executor中的数据传入resolve中 } function reject(value){ //可以将executor中的数据传入reject中 } try{ executor(resolve,reject) }catch(e){ console.log(e): } } 复制代码
- 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); }); 复制代码
打印结果如下
- 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); 复制代码
打印结果如下:
我们再来执行如下代码
let p = new Promise((resolve) => { resolve(1); }); p.then((res) => { console.log(res); }); console.log(2); 复制代码
此处我们打印结果为
但是真正的Promise打印结果为:先2后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) } 复制代码
- resolve支持传入Promise对象
我们执行如下代码:
let p = new Promise((resolve) => { var a = new Promise((resolve) => { resolve(1) }); resolve(a); }); p.then((res) => { console.log(res); }); 复制代码
此处resolve传入的是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) } 复制代码
- 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); } }); }); } 复制代码
- 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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。