AboutFE icon indicating copy to clipboard operation
AboutFE copied to clipboard

38、异步、Promise/async-await/Generate

Open CodingMeUp opened this issue 4 years ago • 4 comments

Promise

promise是用来解决这些问题的:

  • 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象
  • promise可以支持多个并发的请求,获取并发请求中的数据
  • 这个promise可以解决异步的问题,本身不能说promise是异步的

Promise是一个构造函数(Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的),自己身上(构造方法)有all、reject、resolve、all、race这几个眼熟的方法,原型上有then、catch、chain等同样很眼熟的方法。 从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多,链式调用

手写Promise

  1. 基础版本(可以创建promise对象实例。 promise实例传入的异步方法执行成功就执行注册的成功回调函数,失败就执行注册的失败回调函数)

代码很短,逻辑也非常清晰,在then中注册了这个promise实例的成功回调和失败回调,当promise reslove时,就把异步执行结果赋值给promise实例的value,并把这个值传入成功回调中执行,失败就把异步执行失败原因赋值给promise实例的error,并把这个值传入失败回调并执行。

function MyPromise(fn) {
    let self = this; // 缓存当前promise实例
    self.value = null; //成功时的值
    self.error = null; //失败时的原因
    self.onFulfilled = null; //成功的回调函数
    self.onRejected = null; //失败的回调函数

    function resolve(value) {
        self.value = value;
        self.onFulfilled(self.value);//resolve时执行成功回调
    }

    function reject(error) {
        self.error = error;
        self.onRejected(self.error)//reject时执行失败回调
    }
    fn(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    //在这里给promise实例注册成功和失败回调
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
}
module.exports = MyPromise
  1. 支持同步任务(上面基础版代码并不支持同步任务,因为是同步任务,所以当我们的promise实例reslove时,它的then方法还没执行到,所以回调函数还没注册上,这时reslove中调用成功回调肯定会报错的)

实现很简单,就是在reslove和reject里面用setTimeout进行包裹,使其到then方法执行之后再去执行,这样我们就让promise支持传入同步方法,另外,关于这一点,Promise/A+规范里也明确要求了这一点。 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

function resolve(value) {
    //利用setTimeout特性(微任务都先搞完,再搞1个宏任务)将具体执行放到then之后
    setTimeout(() => {
        self.value = value;
        self.onFulfilled(self.value)
    })
}

function reject(error) {
    setTimeout(() => {
        self.error = error;
        self.onRejected(self.error)
    })
}

3.支持三种状态(pending(进行中)、fulfilled(已成功)和rejected(已失败))

我们知道在使用promise时,promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。另外,promise一旦状态改变,就不会再变,任何时候都可以得到这个结果promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。

  • 实现promise的三种状态。
  • 实现promise对象的状态改变,改变只有两种可能:从pending变为fulfilled和从pending变为rejected。
  • 实现一旦promise状态改变,再对promise对象添加回调函数,也会立即得到这个结果。

首先,我们建立了三种状态"pending","fulfilled","rejected",然后我们在reslove和reject中做判断,只有状态是pending时,才去改变promise的状态,并执行相应操作,另外,我们在then中判断,如果这个promise已经变为"fulfilled"或"rejected"就立刻执行它的回调,并把结果传入。

//定义三种状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function MyPromise(fn) {
    let self = this;
    self.value = null;
    self.error = null;
    self.status = PENDING;
    self.onFulfilled = null;
    self.onRejected = null;

    function resolve(value) {
        //如果状态是pending才去修改状态为fulfilled并执行成功逻辑
        if (self.status === PENDING) {
            setTimeout(function() {
                self.status = FULFILLED;
                self.value = value;
                self.onFulfilled(self.value);
            })
        }
    }

    function reject(error) {
        //如果状态是pending才去修改状态为rejected并执行失败逻辑
        if (self.status === PENDING) {
            setTimeout(function() {
                self.status = REJECTED;
                self.error = error;
                self.onRejected(self.error);
            })
        }
    }
    fn(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if (this.status === PENDING) {
        this.onFulfilled = onFulfilled;
        this.onRejected = onRejected;
    } else if (this.status === FULFILLED) {
        //如果状态是fulfilled,直接执行成功回调,并将成功值传入
        onFulfilled(this.value)
    } else {
        //如果状态是rejected,直接执行失败回调,并将失败原因传入
        onRejected(this.error)
    }
    return this;
}
module.exports = MyPromise

4.支持链式操作

流程化的操作 promise.then(f1).then(f2).then(f3),但是我们上面的版本最多只能注册一个回调,我们就来实现链式操作。

想支持链式操作,其实很简单,首先存储回调时要改为使用数组

self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];

当然执行回调时,也要改成遍历回调数组执行回调函数

self.onFulfilledCallbacks.forEach((callback) => callback(self.value));

最后,then方法也要改一下,只需要在最后一行加一个return this即可,这其实和jQuery链式操作的原理一致,每次调用完方法都返回自身实例,后面的方法也是实例的方法,所以可以继续执行

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(onFulfilled);
        this.onRejectedCallbacks.push(onRejected);
    } else if (this.status === FULFILLED) {
        onFulfilled(this.value)
    } else {
        onRejected(this.error)
    }
    return this;
}

5.支持串行异步任务

我们上一节实现了链式调用,但是目前then方法里只能传入同步任务,但是我们平常用promise,then方法里一般是异步任务,因为我们用promise主要用来解决一组流程化的异步操作,如下面这样的调取接口获取用户id后,再根据用户id调取接口获取用户余额,获取用户id和获取用户余额都需要调用接口,所以都是异步任务,如何使promise支持串行异步操作呢?

getUserId()
    .then(getUserBalanceById)
    .then(function (balance) {
        // do sth 
    }, function (error) {
        console.log(error);
    });
let p = new Promise((resolve, reject) => {
    fs.readFile('../file/1.txt', "utf8", function(err, data) {
        err ? reject(err) : resolve(data)
    });
});
let f1 = function(data) {
    console.log(data)
    return new Promise((resolve, reject) => {
        fs.readFile('../file/2.txt', "utf8", function(err, data) {
            err ? reject(err) : resolve(data)
        });
    });
}
let f2 = function(data) {
    console.log(data)
    return new Promise((resolve, reject) => {
        fs.readFile('../file/3.txt', "utf8", function(err, data) {
            err ? reject(err) : resolve(data)
        });
    });
}
let f3 = function(data) {
    console.log(data);
}
let errorLog = function(error) {
    console.log(error)
}
p.then(f1).then(f2).then(f3).catch(errorLog)

//会依次输出
//this is 1.txt
//this is 2.txt
//this is 3.txt

所以要想实现异步操作串行,我们不能将回调函数都注册在初始promise的onFulfilledCallbacks里面,而要将每个回调函数注册在对应的异步操作promise的onFulfilledCallbacks里面,用读取文件的场景来举例,f1要在p的onFulfilledCallbacks里面,而f2应该在f1里面return的那个Promise的onFulfilledCallbacks里面,因为只有这样才能实现读取完2.txt后才去打印2.txt的结果。

但是,我们平常写promise一般都是这样写的: promise.then(f1).then(f2).then(f3),一开始所有流程我们就指定好了,而不是在f1里面才去注册f1的回调,f2里面才去注册f2的回调。

如何既能保持这种链式写法的同时又能使异步操作衔接执行呢?我们其实让then方法最后不再返回自身实例,而是返回一个新的promise即可,我们可以叫它bridgePromise,它最大的作用就是衔接后续操作我们看下具体实现代码:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const self = this;
    let bridgePromise;
    //防止使用者不传成功或失败回调函数,所以成功失败回调都给了默认回调函数
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
    onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
    if (self.status === FULFILLED) {
        return bridgePromise = new MyPromise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(bridgePromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        })
    }
    if (self.status === REJECTED) {
        return bridgePromise = new MyPromise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(self.error);
                    resolvePromise(bridgePromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        });
    }
    if (self.status === PENDING) {
        return bridgePromise = new MyPromise((resolve, reject) => {
            self.onFulfilledCallbacks.push((value) => {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(bridgePromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            self.onRejectedCallbacks.push((error) => {
                try {
                    let x = onRejected(error);
                    resolvePromise(bridgePromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        });
    }
}
//catch方法其实是个语法糖,就是只传onRejected不传onFulfilled的then方法
MyPromise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}
//用来解析回调函数的返回值x,x可能是普通值也可能是个promise对象
function resolvePromise(bridgePromise, x, resolve, reject) {
   //如果x是一个promise
    if (x instanceof MyPromise) {
        //如果这个promise是pending状态,就在它的then方法里继续执行resolvePromise解析它的结果,直到返回值不是一个pending状态的promise为止
        if (x.status === PENDING) {
            x.then(y => {
                resolvePromise(bridgePromise, y, resolve, reject);
            }, error => {
                reject(error);
            });
        } else {
            x.then(resolve, reject);
        }
        //如果x是一个普通值,就让bridgePromise的状态fulfilled,并把这个值传递下去
    } else {
        resolve(x);
    }
}

首先,为防止使用者不传成功回调函数或不失败回调函数,我们给了默认回调函数,然后无论当前promise是什么状态,我们都返回一个bridgePromise用来衔接后续操作。

另外执行回调函数时,因为回调函数既可能会返回一个异步的promise也可能会返回一个同步结果,所以我们把直接把回调函数的结果托管给bridgePromise,使用resolvePromise方法来解析回调函数的结果,如果回调函数返回一个promise并且状态还是pending,就在这个promise的then方法中继续解析这个promise reslove传过来的值,如果值还是pending状态的promise就继续解析,直到不是一个异步promise,而是一个正常值就使用bridgePromise的reslove方法将bridgePromise的状态改为fulfilled,并调用onFulfilledCallbacks回调数组中的方法,将该值传入,到此异步操作就衔接上了。

这里很抽象,我们还是以文件顺序读取的场景画一张图解释一下流程:

当执行p.then(f1).then(f2).then(f3)时:

  • 先执行p.then(f1)返回了一个bridgePromise(p2),并在p的onFulfilledCallbacks回调列表中放入一个回调函数,回调函数负责执行f1并且更新p2的状态.
  • 然后.then(f2)时返回了一个bridgePromise(p3),这里注意其实是p2.then(f2),因为p.then(f1)时返回了p2。此时在p2的onFulfilledCallbacks回调列表中放入一个回调函数,回调函数负责执行f2并且更新p3的状态.
  • 然后.then(f3)时返回了一个bridgePromise(p4),并在p3的onFulfilledCallbacks回调列表中放入一个回调函数,回调函数负责执行f3并且更新p4的状态. 到此,回调关系注册完了,如图所示:

avatar

  • 然后过了一段时间,p里面的异步操作执行完了,读取到了1.txt的内容,开始执行p的回调函数,回调函数执行f1,打印出1.txt的内容“this is 1.txt”,并将f1的返回值放到resolvePromise中开始解析。resolvePromise一看传入了一个promise对象,promise是异步的啊,得等着呢,于是就在这个promise对象的then方法中继续resolvePromise这个promise对象resolve的结果,一看不是promise对象了,而是一个具体值“this is 2.txt”,于是调用bridgePromise(p2)的reslove方法将bridgePromise(p2)的状态更新为fulfilled,并将“this is 2.txt”传入p2的回调函数中去执行。
  • p2的回调开始执行,f2拿到传过来的“this is 2.txt”参数开始执行,打印出2.txt的内容,并将f2的返回值放到resolvePromise中开始解析,resolvePromise一看传入了一个promise对象,promise是异步的啊,又得等着呢........后续操作就是不断的重复4,5步直到结束。

到此,reslove这一条线已经我们已经走通,让我们看看reject这一条线,reject其实处理起来很简单:

  • 首先执行fn及执行注册的回调时都用try-catch包裹,无论哪里有异常都会进入reject分支。
  • 一旦代码进入reject分支直接将bridge promise设为rejected状态,于是后续都会走reject这个分支,另外如果不传异常处理的onRejected函数,默认就是使用throw error将错误一直往后抛,达到了错误冒泡的目的。
  • 最后可以实现一个catch函数用来接收错误。
MyPromise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}

到此,我们已经可以愉快的使用promise.then(f1).then(f2).then(f3).catch(errorLog)来顺序读取文件内容了

  1. es6 promise的all,race,resolve,reject方法 以及 promisify方法(它的作用就是将异步回调函数api转换为promise形式) all 如果有一个报错 怎么处理成功的情况
MyPromise.all = function(promises) {
    return new MyPromise(function(resolve, reject) {
        let result = [];
        let count = 0;
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(function(data) {
                result[i] = data;
                if (++count == promises.length) {
                    resolve(result);
                }
            }, function(error) {
                reject(error);
            });
        }
    });
}

MyPromise.race = function(promises) {
    return new MyPromise(function(resolve, reject) {
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(function(data) {
                resolve(data);
            }, function(error) {
                reject(error);
            });
        }
    });
}

MyPromise.resolve = function(value) {
    return new MyPromise(resolve => {
        resolve(value);
    });
}

MyPromise.reject = function(error) {
    return new MyPromise((resolve, reject) => {
        reject(error);
    });
}

MyPromise.promisify = function(fn) {
    return function() {
        var args = Array.from(arguments);
        return new MyPromise(function(resolve, reject) {
            fn.apply(null, args.concat(function(err) {
                err ? reject(err) : resolve(arguments[1])
            }));
        })
    }
}

CodingMeUp avatar May 13 '20 07:05 CodingMeUp

v8 来讲的话,难道不是通过协程实现的么,外加 promise async 和 Generator 都是用到了协程,但是作用不太一样吧 babel 把async转了 generators 和 promises,再用 regenerator-runtime 把 generator 转换为 ES5 代码 文档上的东西,都只是说说而已,真正怎么实现还是引擎说了算

本质是单向链表,async/await 是参照 Generator 封装的一套异步处理方案,可以理解为 Generator 的语法糖,所以了解 async/await 就不得不讲一讲 Generator,而 Generator 又依赖于迭代器Iterator,而 Iterator 的思想呢又来源于单向链表,终于找到源头了:单向链表

async函数的返回值是一个promise对象,

function _async(fn) {
    return (...args) => Promise.resolve(fn(...args));
}

await的作用,它能够将异步代码转变成同步代码的逻辑,在异步代码返回值到达前,程序将会被挂起。所以我们要挑一个能让程序挂起的代码,也就是 while(true)。实现如下

function _await() {
    let result = data.next();
    while (true) {
      console.log('waiting...', result); 
      if (result.done) return result.value;
      result = data.next();
    }
}

用 generator 的调用过程和模拟 async/await 的行为,但这个核心牵扯在一起,有点牵强附会的意思。

async/await 的核心是 promise,因为没有 promise,那 async/await 的实现就要变一变了

promise 的核心是 Promise/A+ 规范,这是一个典型的社区推动语言发展的案例,说白了 promise 就是一个技术实现

规范网站:https://promisesaplus.compromisesaplus.com

说 generator 那就必须要说说 “可迭代协议“ 和 “迭代器协议“,因为这个才是生成器的核心

mdn: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocolsdeveloper.mozilla.org/zh-CN/docs/Web...

promise 其实和 generator 完全没有任何关系,只是因为当时 async/await 没实现,别人用这个模拟了下,发现挺好用的,就推广了,比较经典的例子就是 co 模块和 babel 转换后的代码

generator 和 promise 其实都是一种技术实现,它们各自解决的问题也不一样,generator 更适合 批量 处理数据,有其实 批量操作的时候,非常高效,但是 promise 其实最初是为了解决回调函数的问题

很多语言都有 generator 实现,但是在 js 中,之所以会 和 promise 活着说异步关联,仅仅是因为当时 async/await 不能用,这个又很适合批量动态操作,本质上来说,他们就是没有任何的关联性,因为他们的设计的目标就不一致。

像我使用 babel-plugin-async-to-promises 插件,就将 async/await 转成 promise,没有和 generator 有任何的关联

你用其他的代码其实也可以模拟 async/await 的过程,generator 的强项是数据处理。

我以为你会解释三者的由来,设计理念和代码实现

CodingMeUp avatar May 13 '20 09:05 CodingMeUp

第 8 题:setTimeout、Promise、Async/Await 的区别 #33 附带答案和变种题练习

async function async1(){
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2(){
  console.log('async2')
}
console.log('script start')
setTimeout(function(){
  console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
  console.log('promise1')
  resolve();
  console.log('ss');
}).then(function(){
  console.log('promise2')
})
console.log('script end')

script start
async1 start
async2
promise1
ss
script end
async1 end
promise2
setTimeout

相关解析

CodingMeUp avatar Jun 17 '20 07:06 CodingMeUp

由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask

async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}

等价于

async function async1() {
	console.log('async1 start');
	Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

但是 如果await 后面的方法里 含有 异步函数 此时执行完await并不先把await后面的代码注册到微任务队列中去,而是执行完await之后,直接跳出async1函数,执行其他代码

https://mp.weixin.qq.com/s/QgfE5Km1xiEkQqADMLmj-Q

CodingMeUp avatar Jun 18 '20 12:06 CodingMeUp

JS 异步解决方案的发展历程以及优缺点。 #11

  1. 回调函数(callback) setTimeout(() => { // callback 函数体 }, 1000)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

回调地狱的根本问题在于:

  • 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
  • 嵌套函数过多的多话,很难处理错误

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

  1. Promise Promise就是为了解决callback的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

  1. Generator 特点:可以控制函数的执行,可以配合 co 函数库使用

function *fetch() { yield ajax('XXX1', () => {}) yield ajax('XXX2', () => {}) yield ajax('XXX3', () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()

  1. Async/await async、await 是异步的终极解决方案

await/async 是通过 Generator/function* 来实现的。 所以 async/await 的相关优势也来自于generator。 Generator 是一个可以暂停 function ,感觉推出 generator 的目的包括不仅限于 Callback Hell 和 Inversion of Control。 感觉整个 community 有在往那个方向走。

优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch('XXX1')
  await fetch('XXX2')
  await fetch('XXX3')
}
```js
下面来看一个使用 await 的例子:
let a = 0
let b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

对于以上代码你可能会有疑惑,让我来解释下原因

首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10 上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

CodingMeUp avatar Jun 18 '20 14:06 CodingMeUp