AboutFE
AboutFE copied to clipboard
38、异步、Promise/async-await/Generate
Promise
promise是用来解决这些问题的:
- 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象
- promise可以支持多个并发的请求,获取并发请求中的数据
- 这个promise可以解决异步的问题,本身不能说promise是异步的
Promise是一个构造函数(Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的),自己身上(构造方法)有all、reject、resolve、all、race这几个眼熟的方法,原型上有then、catch、chain等同样很眼熟的方法。 从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多,链式调用
手写Promise
- 基础版本(可以创建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
- 支持同步任务(上面基础版代码并不支持同步任务,因为是同步任务,所以当我们的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的状态. 到此,回调关系注册完了,如图所示:
- 然后过了一段时间,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)来顺序读取文件内容了
- 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])
}));
})
}
}
v8 来讲的话,难道不是通过协程实现的么,外加 promise async 和 Generator 都是用到了协程,但是作用不太一样吧 babel 把async转了 generators 和 promises,再用 regenerator-runtime 把 generator 转换为 ES5 代码 文档上的东西,都只是说说而已,真正怎么实现还是引擎说了算
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 的强项是数据处理。
我以为你会解释三者的由来,设计理念和代码实现
第 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
由于因为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
- 回调函数(callback) setTimeout(() => { // callback 函数体 }, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
- 嵌套函数过多的多话,很难处理错误
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
- Promise Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
- 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()
- 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 的话,其实自己就可以实现这样的语法糖。