zoukankan      html  css  js  c++  java
  • 学习笔记—Promise的介绍与实现

    日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue 全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。

    Promise

    参考文献 Promise|MDN

    Promise 出现的原因:处理多个并发请求,利用 链式调用 解决了 回调地狱 的问题。

    Promise 存在三种状态,成功(resolve)失败(reject)等待(pending)

    首先, Promise是一个类 ,需要通过关键字 new 来进行实例化。

    Promise接受一个 executor 函数作为执行器,执行器是立即执行的。同时又会接受两个参数作为 成功失败 的回调。

    image.png

    当我们不去执行 成功失败 的回调,当前这个Promise的状态就会维持在 等待 状态。Promise类会返回一个Promise类,方便下一次调用。

    let promise = new Promise((resolve, reject)=>{})
    console.log(promise) // Promise {<pending>}
    

    Promise 实例的返回值会根据调用的函数,来判断当前返回的是 成功状态失败状态,并且会将传入参数返回。在调用函数时,若不传入参数,则会返回 undefined

    // 什么都不传
    let promise = new Promise((resolve, reject) => {
        resolve()
    })
    console.log(promise); // Promise { undefined }
    
    // 成功状态
    let promise = new Promise((resolve, reject) => {
        resolve('success')
    })
    console.log(promise); // Promise { 'success' }
    
    // 失败状态
    let promise = new Promise((resolve, reject) => {
        reject('failed')
    })
    console.log(promise); // Promise { <rejected> 'failed' }
    

    每一个Promise的实例上,都有一个 .then 方法输出上一个实例传入的结果。当前实例状态被改变后,将无法再进行改变。

    let promise = new Promise((resolve, reject) => {
        resolve('success')
    }).then((result) => {
        console.log(result); // success
    }, (error) => {
        console.log(error);
    })
    

    这样的话,我们就可以总结出来 Promise 的几个特点。

    特点

    1. Promise 是一个类,无需考虑兼容性等问题。
    2. Promise 会传入一个函数(executor)作为执行器,此执行器是立即执行的。
    3. executor 提供了两个函数(resolvereject)用来描述当前 Promise 的状态,而当前实例存在三种状态,成功状态失败状态等待状态 ,当前实例默认为 等待状态。如果调用 resolve 则状态变为 成功状态 ,调用 reject 或 发生异常 则状态变为 失败状态
    4. Promise 一旦状态变化后,则不能再更改。
    5. 每个 Promise 实例都有一个 .then 方法。

    我们可以根据 Promise 的几个特点,手写一套属于自己的 Promise

    手写实现 Promises/A+ 规范

    文档规范 Promises/A+

    注:代码内容为连续内容,请依序观看。谢谢

    Promise的基础功能

    根据上述特点,我们就可以简单实现出 Promise 的效果。

    const PEDDING = 'PEDDING'; // 等待状态
    const FULFILLED = 'FULFILLED'; // 成功状态
    const REJECTED = 'REJECTED'; // 失败状态
    class Promise {
      constructor(executor) { 
        this.status = PEDDING; // 默认状态
        this.result = undefined; // 成功的回调
        this.reason = undefined; // 失败的回调
        const resolve = (result) => { // 成功 resolve 函数
          if (this.status === PEDDING) {
            this.status = FULFILLED; // 修改状态
            this.result = result; // 添加回调
          }
        }
        const reject = (reason) => { // 失败 reject 函数
          if (this.status === PEDDING) {
            this.status = REJECTED; // 修改状态
            this.reason = reason; // 添加回调
          }
        }
        try {
          executor(resolve, reject)
        } catch (error) {
          this.reason = error;
        }
      }
      then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) { // 成功时调用的方法
          onFulfilled(this.result)
        }
        if (this.status === REJECTED) { // 失败时调用的方法
          onRejected(this.reason)
        }
      }
    }
    module.exports = Promise
    

    参考 Promise A+规范,我们可以简单实现出来一版 Promise 类的简易实现版。

    实现Promise的异步功能

    实现 Promise 的异步,我们需要先明确,Promise中只有在触发 .then 方法时(也就是resolvereject ),才是异步的。所以我们利用这样一个思路。

    当用户调用 .then 方法时,Promise 此时可能是 等待状态,我们需要先将其暂存起来。后续调用 resolvereject 时,再去触发对应的 onFulfilledonRejected

    根据上面的描述,我们可以捕捉到 暂存触发 这两个关键词,那么我们就可以使用 发布订阅 的设计模式来实现此功能。

    // ...
    class Promise {
      constructor(executor) {
        // ...
        this.onResolveCallbacks = []; // 用来存储 成功的回调
        this.onRejectCallbacks = []; // 用来存储 失败的回调
        const resolve = (result) => {
          if (this.status === PEDDING) {
            // ...
            this.onResolveCallbacks.forEach(fn => fn())
          }
        }
        const reject = (reason) => {
          if (this.status === PEDDING) {
          // ...
            this.onRejectCallbacks.forEach(fn => fn())
          }
        }
        // ...
      }
      then(onFulfilled, onRejected) {
        if (this.status === PEDDING) {
          this.onResolveCallbacks.push(() => {
            onFulfilled(this.result)
          })
          this.onRejectCallbacks.push(() => {
            onRejected(this.reason)
          })
        }
        // ...
      }
    }
    module.exports = Promise
    

    建立两个 用来存储回调函数的数组,先将需要执行的函数存储进数组 中。当异步执行完后,再依次 执行数组内存储的函数

    Promise链式调用

    首先我们先要清楚,Promise的出现解决了哪些问题?

    • 处理多个并发请求
    • 链式调用解决了回调地狱的问题

    回调地狱是什么? 回调地狱就是我们平时在处理业务代码时,下一个接口的api参数需要用到上一个接口的参数。代码上可能就会出现多级嵌套的情况,导致代码阅读起来十分困难。

    这里我们就需要用到Promise的链式调用,也就是 .then 方法的循环调用,当调用 .then 方法后,会返回一个新的Promise。

    我们先封装一个Promise的异步函数

    let fs = require('fs');
    function readFile(path, encoding) {
      return new Promise((resolve, reject) => {
        fs.readFile(path, encoding, (err, data) => {
          if (err) reject(err)
          resolve(data)
        })
      })
    }
    

    现在我们需要清楚链式调用出现的几种情况。

    1. .then 方法返回的是一个 普通值(不是 Promise) 的情况下,会作为外层下一次.then 方法的 成功结果

      readFile('./a.txt', 'utf8').then((result) => {
        return 1;
      }, (err) => {
        console.log(err);
      }).then((result) => {
        console.log(result); // 1
      }, (err) => {
        console.log(err);
      })
      
    2. .then 方法执行出错,会走到外层下一次.then 方法的 失败结果

      readFile('./a.txt', 'utf8').then((result) => {
        throw new Error('error')
      }, (err) => {
        console.log(err);
      }).then((result) => {
        console.log(result);
      }, (err) => {
        console.log(err); // Error: error
      })
      

      (注:执行错误需要 throw new Error(),如果直接使用 return new Error(),属于返回一个Error对象,会执行下一次的成功结果)

    3. 无论上一次 .then 方法执行结果是 成功 还是 失败,只要返回的是普通值,都会执行下一次.then 方法的 成功结果

      路径填写错误,Promise会默认执行第一层.then方法的错误结果,并返回undefined。则下一层的执行结果是成功结果值为undefined

      // 路径填写错误
      readFile('./a.txt1', 'utf8').then((result) => {
        console.log(result)
      }, (err) => {
        // 相当于在此处 return undefined
        console.log(err); // 错误原因
      }).then((result) => {
        console.log(result); // undefined
      }, (err) => {
        console.log(err);
      })
      
    4. 如果 .then 方法返回的是一个 Promise 对象,此时会根据 Promise 的结果来处理是成功结果还是失败结果(传入的是成功或失败的内容)

      readFile(`${bathPath}a.txt`, 'utf8').then((result) => {
          return readFile(`${bathPath}${result}`, 'utf8')
      }, (err) => {
          console.log('err1', err);
      }).then((result) => {
          console.log('success2', result); // success2 b
      }, (err) => {
          console.log('err2', err); // error
      })
      

    (总结:如果返回的是一个普通值(不是 Promise),就会传递给下一次 .then 方法的成功。如果返回的是一个失败的Promise 或者 抛出异常,就会传递给下一次 .then 方法的失败。)

    手写实现promise链式调用

    根据上述特点和情况,我们每次在 .then 方法调用后都要返回一个新的 Promise 实例。所以我们可以对之前写好的 .then 方法进行相应的修改。

    我们首先来处理 普通值(不是 Promise) 的情况。

    注:在这里我们单独提出来了一个 x ,用来进行后续处理

    // 对 .then() 方法进行改写
    then(onFulfilled, onRejected) {
    	let promise = new Promise((resolve, reject) => { // 返回一个 promise 实例
    		if (this.status === FULFILLED) {
    			try {
            let x = onFulfilled(this.result)
    				resolve(x);
    			} catch (e) {
    				reject(e)
    			}
    		}
    		if (this.status === REJECTED) {
    			try {
            let x = onRejected(this.reason)
    				resolve(x)
    			} catch (e) {
    				reject(e)
    			}
    		}
    		if (this.status === PEDDING) {
    			this.onResolveCallbacks.push(() => {
    				try {
    					let x = onFulfilled(this.result)
    					resolve(x);
    				} catch (e) {
    					reject(e)
    				}
    			})
    			this.onRejectCallbacks.push(() => {
    				try {
    					let x = onRejected(this.reason)
    					resolve(x)
    				} catch (e) {
    					reject(e)
    				}
    			})
    		}
    	})
    	return promise
    }
    

    利用上述思路对之前的方法进行改造,这样我们就可以对 普通值 进行处理。

    上述处理 普通值 的情况,我们可以稍加改动,使其可以处理更多的情况。为此我们需要封装一个 resolvePromise() 函数来进行处理。

    resolvePromise()需要接受四个参数,分别是 当前实例promise结果x成功回调resolve失败回调reject

    为了可以将当前实力promise作为参数传递,我们需要先用异步方法 setTimeout (其他方法也可以) 将其进行封装。

    then(onFulfilled, onRejected) {
    	let promise = new Promise((resolve, reject) => { // 返回一个 promise 实例
    		if (this.status === FULFILLED) {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.result)
              // 在此处进行封装处理
              resolvePromise(promise, x, resolve, reject);
            } catch (e) {
              reject(e)
            }
          }
    		}
        // ... 后面代码进行同样的修改
    	})
    	return promise
    }
    

    这样我们就可以读取到 promise实例 了,下面我们来实现 resolvePromise() 函数。

    function resolvePromise(promise, x, resolve, reject) {
    	if (promise === x) {
    		return reject(new TypeError('错误'))
    	}
      // promise 兼容性
      if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        try {
          let then = x.then // 通过defineProperty实现时,取值时可能会有异常
          if (typeof then === 'function') {
            then.call(x, y => {
            	resolve(y)
          	}, r => {
          		reject(r)
          	})
          } else {
            resolve(x)
          }
        } catch (e) {
        	reject(e)
        }
      } else {
        // 普通值
      	resolve(x)
      }
    }
    

    注:在工作中,我们可能会调用别人封装的Promise,里面可能会有问题。所以我们还需要进行一步处理,也就是在代码里面加个,确保代码的严谨性。

    function resolvePromise(promise, x, resolve, reject) {
    	// ...
      if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        let called = false; // 定义一个参数
        try {
          let then = x.then // 通过defineProperty实现时,取值时可能会有异常
          if (typeof then === 'function') {
            then.call(x, y => {
              // 在这里进行异常判断
              if (called) return
    					called = true
            	resolve(y)
          	}, r => {
              if (called) return
    					called = true
          		reject(r)
          	})
          } else {
            resolve(x)
          }
        } catch (e) {
          if (called) return
          called = true
        	reject(e)
        }
      } else {
        // ...
      }
    }
    

    这样我们就实现了 Promise 的链式调用。

    特殊情况处理

    嵌套Promise

    可能还会出现这种情况,我们在 .then 方法的 resolve 中传入一个 Promise实例 ,这种情况我们要如何处理呢?

    如下情况

    let promise = new Promise((resolve, reject) => {
    	resolve(1)
    }).then(() => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
        	resolve(new Promise((resolve, reject) => {
            setTimeout(() => {
            	resolve(200)
            }, 1000);
          }))
        }, 1000);
      })
    })
    
    promise.then(result => {
    	console.log(result);
    }, err => {
    	console.log(err);
    })
    

    针对上述特殊情况,我们需要继续对之前的resolvePromise() 函数 进行改造。

    function resolvePromise(promise, x, resolve, reject) {
    	// ...
      if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    		// ...
        try {
          // ...
          if (typeof then === 'function') {
            then.call(x, y => {
              // ...
            	// 一直解析,直到不是 Promise 为止
    					resolvePromise(promise, y, resolve, reject)
          	}, r => {
              // ...
          	})
          } else {
            resolve(x)
          }
        } catch (e) {
          // ...
        }
      } else {
        // ...
      }
    }
    
    
    

    关键点就是在于递归调用,直到其值为普通值为止。

    参数穿透

    我们在调用 .then方法 时,还会出现下面这种情况

    new Promise((resolve, reject) => {
    	resolve(100)
      // reject('err')
    }).then().then().then().then(result => {
    	console.log(result); // 100
    }, err => {
    	console.log(err); // 如果传入,则输出 err
    })
    

    不传入参数的情况下,结果会一直进行传递,直到输出为止。

    这种参数穿透的情况,我们也需要在代码上进行改造。

    then(onFulfilled, onRejected) {
      // 对 onFulfilled 进行处理
    	onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
      onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}; // 抛出的情况下才会输出错误结果,所以要用 throw
      // ...
    }
    

    Promise测试

    我们可以对自己封装的Promise进行测试,需要用到测试包 promises-aplus-tests

    在Promise实例目录下执行如下代码

    npm install promises-aplus-tests -g
    promises-aplus-tests ./promise.js
    

    他会自动检测我们封装的Promise是否符合 Promise A+ 规范。

    在我们封装的 Promise 文件下添加 延迟对象

    class Promise {
      // ... 自己封装的 Promise
    }
    
    // 需要进行测试用的代码
    Promise.deferred = function () {
    	let dfd = {};
    	dfd.promise = new Promise((resolve, reject) => {
    		dfd.resolve = resolve
    		dfd.reject = reject
    	})
    	return dfd
    }
    
    module.exports = Promise
    

    注:catchall 等都不属于Promise规范中包含的方法

    检测完后,我们可以看到其输出结果,根据结果我们可以清楚自己封装的 Promise 是否可以正常运行。

    image.png

    至此,我们就封装好了一个Promise

    延迟对象

    用来帮我们减少一次套用,应用并不算广泛。有点类似于代理。

    我们可以对最一开始我们自己的 readFile读取操作 进行封装。

    function readFile(path, encoding) {
      let dfd = Promise.deferred();
    	fs.readFile(path, encoding, (err, data) => {
    		if (err) return dfd.reject(err)
    		dfd.resolve(data)
    	})
      return dfd.promise;
    }
    

    本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
    您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
    最后感谢您的支持!

  • 相关阅读:
    Linux基础命令-cp
    Linux基础命令-mkdir
    Linux基础命令-touch
    Linux基础命令-diff
    Linux基础命令-cut
    Linux基础命令-stat
    System.Web.HttpException: 请求在此上下文中不可用
    数据库日志删除、压缩操作
    如何收缩和删除SQL日志文件
    Excel 常用宏代码大全
  • 原文地址:https://www.cnblogs.com/moxiaoshang/p/15096987.html
Copyright © 2011-2022 走看看