异步编程
回调函数、事件监听(变成事件驱动,执行流程不清晰)、事件监听的优化版‘发布/订阅方式’
回调函数实现异步,错误不能被异步操作发起者获取。
ES6新增promise,通过期约链,标准化异步错误处理。
ES2017 async与await,将异步代码写成同步形式
ES2018异步迭代器和for/await循环
从OS角度理解同步异步、阻塞与非阻塞
异步与同步是对应的,它们描述的是线程之间的关系。
两个线程要么是同步,要么是异步。
阻塞与非阻塞是对于同一个线程来说,在某一时刻,线程要么处于阻塞,要么处于非阻塞。
关系
阻塞是同步机制的结果,非阻塞式使用异步机制的结果
基础
JavaScript是单线程的,意思是JavaScript的执行时只有一个主线程,他的宿主环境(node、浏览器)都是多线程的。主线程是单线程,所有同步任务都在主线程上执行,形成一个执行栈(execution context stack).所有阻塞的部分交给一个线程池处理,然后主线程通过事务队列跟线程池协作。
非阻塞是指需要执行异步任务时候,主线程会pending这个任务,当异步任务执行完毕会被放到事务队列中。事务队列在当前执行栈中的所有任务执行完毕后执行。
基于事件循环:主线程的执行过程就是一个 tick(同步、微任务、宏任务),不断循环
异步任务队列其实有两种,微任务和宏任务。先执行所有的微任务再宏
setTimeout实现动画的实质是间断地改变图像的位置属性,而requestAnimation是由系统来决定回调函数的执行时机,其回调频率取决于系统的屏幕刷新率(刷新一次即一帧)。保证回调函数在屏幕每一次的刷新间隔中只且一定被执行一次。即CPU节流又不丢帧。setTimeout回调时间可能是任何时候,一帧里面可能执行了很多次也可能根本没有执行,requestAnimation始终在DOM节点渲染前执行
window.requestAnimationFrame(callback);
补充:
微任务:
MutaionObserverJavaScript红宝书之DOM基础、MutationObserver接口(H5)
Object.observe(已废弃;Proxy 对象替代)
process.nextTick()
是Node.js提供的一个异步执行函数,它不是setTimeout(fn, 0)的别名,它的效率更高,它的执行顺序要早于setTimeout和setInterval,它是在主逻辑的末尾任务队列调用之前执行。
宏任务:
I/O(Node.js)
UI rendering/UI事件
postMessage,MessageChannel
script?
例子
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
ajax进入Event Table,注册回调函数success。
执行console.log(‘代码执行结束’)。
ajax事件完成,回调函数success进入Event Queue。
主线程从Event Queue读取回调函数success并执行。(js引擎存在monitoring process进程,会持续检查主线程执行栈是否为空,一旦为空,就调用Event Queue)
回顾
JavaScript是单线程事件循环模型。早期只支持定义回调函数来表明异步函数完成,串联多个异步操作就需要深度嵌套的回调函数(回调地狱)。
在 js 中,函数也是对象,可以赋值给变量,可以作为参数放在函数的参数列表中。回调函数,就是放在另外一个函数(如 parent)的参数列表中,作为参数传递给这个 parent,然后在 parent 函数体的某个位置执行。
ES6新增promise、ES8新增异步函数
promise主要是为异步代码提供了清晰的抽象,串行异步代码。可以用其表示异步执行的代码块,也可以用其表示异步计算的值。
异步函数是将promise应用于JavaScript函数的结果,可以暂停执行而不阻塞主线程,可以编写基于promise的代码,组织串行/平行执行的异步代码。
XMLHttpRequest fetch()返回一个期约
Promise
期约链===线性
缺点:
无法取消,一旦开始执行,中途无法取消;
不设置回调函数,promise内部抛出的错误,无法返回到外部
处于pendding状态时,无法得知进展到哪一个阶段。
语法上讲,Promise是一个有状态的对象,可以获取异步操作的消息,满足JavaScript异步编程的需求
pendding //正在请求,当把一件事情交给promise后
fulfilled===resolved //成功兑现
rejected //失败
状态转换不可撤销
期约的状态是私有的,不能直接通过JavaScript检测到,主要为避免根据读取到的期约状态,以同步方式处理期约对象。期约故意将异步行为封装起来,从而隔离外部的同步代码。
e.g:期约只要状态切换成兑现,就会有一个私有的内部值(接收异步操作会实际生成的值),切换为拒绝,就会有一个私有的内部理由。值、理由都是包含原始值或对象的不可修改的引用。在期约到达某个落定状态时执行的异步代码始终会受到这个值或理由。
var myFirstPromise = new Promise(function(resolve, reject){
//当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
//在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
setTimeout(function(){
resolve("成功!"); //代码正常执行!
}, 250);
});
myFirstPromise.then(function(successMessage){
//successMessage的值是上面调用resolve(...)方法传入的值.
//successMessage参数不一定非要是字符串类型,这里只是举个例子
document.write("Yay! " + successMessage);
});
执行器函数 new Promise()
里的代码是原本同步执行的,只是在实际的应用中会夹杂着异步代码。
通过执行函数控制期约状态:
- 初始化promise的异步操作
- 控制状态的最终转换:通过调用resolve()、reject()
调用Promise.resolve()静态方法,实际上可以把任何值都转换为一个期约,传给Promise.resolve()的第一个参数对应解决的期约的值。
相应,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过try/catch捕获,而只能通过onRejected拒绝处理程序捕获)。
无法取消Promise,一旦新建它就会立即执行,无法中途取消。
当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
非重入期约方法:当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行,跟在添加这个处理程序的代码之后的同步代码一定会在处理之前先执行
同步、异步执行的二元性
try{
throw new Error('foo');
}catch(e){
console.log(e);//Error:foo成功抛出并捕获错误
}
try{
Promise.reject(new Error('bar'));
}catch(e){
console.log(e);
}
//Uncaught (in promise) Error:bar抛出错误却没捕获到
如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的,因此try/catch块并不能捕获该错误,代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体的说就是期约的方法。
期约的实例方法们
是链接外部同步代码与异步代码之间的桥梁,可以访问异步操作的返回数据、处理期约成功和失败的结果。连续对期约求值、添加状态转变的代码…
实现Thenable接口即可以调用then()
const then={
then(callback){callback('baz');}
};
在ECMAScript暴露的异步结构中,任何对象都有一个then()方法。
Promise.prototype.then()
实质:把处理程序推进消息队列,当前同步代码执行完成前不会执行。(所有的处理程序都只能异步执行)
非函数处理程序会被静默忽略
为期约实例添加处理程序的主要方法,这个then()方法接收最多的两个参数:onResolved处理程序onRejected处理程序。这两个参数都是可选的,如果提供的话分别会在期约进入兑现或拒绝状态时执行:
function onResolved(id){//
setTimeout(console.log,id,'resolved');
}
function onRejected(id){//
setTimeout(console.log,0,id,'rejected');
}
let p1 = new Promise((resolve,reject)=>setTimeout(resolve(3),3000));
let p2 = new Promise((resolve,reject)=>setTimeout(reject,3000));
//监听期约状态
p1.then((mess)=>onResolved(mess),
(mess)=>onRejected('p1'));
//不传解决监听器的规范写法
p2.then(null,//避免在内存上创建多余
()=>onRejected('f'));
//3秒后
//3 resolved
//p2 rejected
promise的小例子
var p = new Promise((resolve, reject) => {resolve(1);}).then(
()=>{console.log(1);return false;},
()=>{console.log(2);reject(1);}).then(
(x)=>{console.log(3)},()=>{console.log(4)})
//1 3
reject,与resolve分别决定调用then当中的哪一个函数。当有返回值却没有落定的时候,无论返回值是什么,执行第一个resolve的回调函数
Promise.prototype.catch()
用于给期约添加处理程序,只接收一个参数:onRejected处理程序:
Promise.prototype.finally()
用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免onResolved和onRejected处理程序中出现冗余代码,但onFinally处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码:
let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function(){
setTimeout(console.log,0,'Finally!')
}
p1.finally(onFinally);//Finally
p2.finally(onFinally);//Finally
与catch、then一样也会返回一个期约,但是通常被忽略。
finally的期约值一般取决于 调用finally的期约,但是如果finally回调抛出错误,就会用此错误拒绝返回的期约(then、catch同样)
拒绝期约与拒绝错误处理
拒绝期约类似于throw表达式,在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。
let p1=new Promise((resolve,reject)=>reject(Error('foo')));
let p2=new Promise((resolve,reject)=>{throw Error('foo');});
let p3=Promise.resolve().then(()=>{throw Error('foo');});
let p4=Promise.reject(Error('foo'));
期约连锁与期约合成
期约连锁
即链式调用
每个期约实例的方法then、catch、finally都会返回一个新的期约对象。
后续期约等待之前的期约,串行化异步任务。
let p1=new Promise((resolve,reject)=>{
console.log('p1');
setTimeout(resolve,1000);
});
p1.then(()=>new Promise((resolve,reject)=>{
console.log('p2');
setTimeout(resolve,1000);
}))
.then(()=>new Promise((resolve,reject)=>{
console.log('p3');
setTimeout(resolve,1000);
}));
//p1 p2 p3
期约图
Promise.resolve(data)
如果 Promise.resolve 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 落定的Promise 对象,解决的期约的值对应第一个传参。
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
console.log("h")
//h Hello
具有幂等性
p === Promise.resolve(p);
Promise.reject(error)
不具有幂等性
Promise.reject(p); // 期约对象成为拒绝理由
异步操作的error,try/catch不能获取,只有通过异步回调拦截
Promise.all()
该静态方法接收一个可迭代对象,返回一个新期约
获取多个数据。
getLatestJob(context){
const result1=api.getJobJsonFromShield(context)
.then(response => {
return response.json();
});
const result2=api.getJobJson(context)
.then(response => {
return response.json();
});
Promise.all([result1, result2])
.then(([shieldData, nbuData])=>{
context.commit('mergeList',{"shield":shieldData,"nbuData":nbuData})
});
}
关联性极强,有一个失败就不会继续走下去了 会影响到其他接口操作
可以在promise.all队列中,使用map每一个过滤每一个promise任务,其中任意一个报错后,return一个返回值,确保promise能正常执行走到.then中
Promise.all([p1, p2, p3].map(p => p.catch(e => return '出错后返回的值' )))
.then(values => {
console.log(values);
}).catch(err => {
console.log(err);
})
Promise.race()
返回一个包装期约,式一组集合中最先解决或者拒绝的期约的镜像
手写一个promise
promise可以处理异步问题,其实本质上还是发布订阅模式。
首先是一个类,构造函数里有一个接收两个回调函数的执行函数,实现了then方法获取resolve或者reject的传递的结果
雏形
const PENDING = 'PENDING'; // 等待态
const FULFILLED = 'FULFILLED'; // 成功态
const REJECTED = 'REJECTED'; // 失败态
class Promise1 {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
};
try {//抛出错误的情况
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
}
}
支持异步
怎样在resolve或者reject方法执行的时候自动触发then方法中的回调?发布订阅模式
const PENDING = 'PENDING'; // 等待态
const FULFILLED = 'FULFILLED'; // 成功态
const REJECTED = 'REJECTED'; // 失败态
class Promise1 {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];// 实现异步,发布订阅模式
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn());
}
}
try {//抛出错误的情况
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
// 实现异步
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
});
}
}
}
then返回一个promise支持链式调用
x为resolve或者reject的返回值
如果x是一个普通值,调用resolve处理,如果是一个promise,调用then方法。
then(onFulfilled, onRejected) {
// 每次调用then方法 都必须返回一个全新的promise
let promise2 = new Promise((resolve, reject) => {
// x 就是上一个then成功或者失败的返回值,这个x决定promise2 走成功还是走失败
if (this.status == FULFILLED) {
try {
let x = onFulfilled(this.value);
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 == PENDING) {
this.onResolvedCallbacks.push(() => {
try {
let x = onFulfilled(this.value);
resolve(x);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(() => {
try {
let x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
});
}
});
return promise2;
}
防止循环调用
在promise2中获取promise2变量,使用定时器,当promise执行完时在获取
// 替换所有对x的处理 (这里只写一处例子)
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(x, promise2, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
function resolvePromise(x, promise2, resolve, reject) {
// If promise and x refer to the same object, reject promise with a TypeError as the reason
if (x === promise2) {
return reject(new TypeError('循环引用'));
}
if ((typeof x === 'object' && x !== null) || typeof x == 'function') {
//If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
let called = false;
try {
let then = x.then;
if (typeof then == 'function') {
then.call(x,
(y) => {
// y有可能还是一个promise ,所以要再次进行解析流程
// 我需要不停的解析成功的promise中返回的成功值,直到这个值是一个普通值
if (called) return;
called = true;
resolvePromise(y, promise2, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
异步函数
javaScript的迭代模式:迭代器与生成器
yield命令后面可以跟一个promise对象,来模拟异步函数
同步方式编写的代码可以异步执行
异步函数是将promise应用于JavaScript函数的结果,可以暂停执行而不阻塞主线程,可以编写基于promise的代码,组织串行/平行执行的异步代码。
async
async 函数的:是将 Generator 函数和自动执行器,包装在一个函数里。
声明异步函数
可以用于函数声明、函数表达式、箭头函数和方法
async function foo(){}
let foo=async ()=>{};
class P{
//类方法
async p(){}
}
async可以让函数具有异步特征,但总体上其代码也是同步求值的,但是异步函数使用return返回值,这个值会被Promise.resolve()包装成一个期约对象,返回值就被当做已经解决的期约。
async function foo(){
console.log("asy1");
return "asy_re";
//等同于 return Promise.resolve("asy_re");
}
foo().then(console.log);//给返回添加一个解决处理程序
console.log(2);
//asy1、2、asy_re
异步函数很希望你能返回一个实现了thenable接口的对象,对象可以由提供给then程序的处理程序”解包“。
await
异步函数主要针对不会马上完成的任务,所以需要一种暂停和恢复执行的能力,await
只能在异步函数中使用。
JavaScript运行在碰到await关键字,会记录暂停位置,等到await右边的值可用了,JavaScript会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
async function awaitPro(){
console.log('awaitPro1');
console.log(await Promise.resolve(8));
console.log('awaitPro2');
}
async function awaitIn(){
console.log('awaitIn1');
console.log(await 6);
console.log('waitIn2');
}
console.log(1);
awaitPro();
console.log(2);
awaitIn();
console.log(3);
//1 awaitPro1 2 awaitIn1 3 6 awaitIn2 8 awaitPro2
- 打印1
- 调用异步函数awaitPro();
- (awaitPro中)打印
- (awaitPro中)await暂停执行,向消息队列添加一个期约在落定之后执行的任务
- 期约立即落定,把await后的任务添加到消息队列
- awaitPro退出
- 打印2
- 调用异步函数awaitIn();
- (awaitIn中)打印waitIn1
- (awaitIn中)await暂停执行,向消息队列添加一个立即可用的值6的任务
- awaitIn退出
- 打印3
- 顶级线程执行完毕
- JavaScript运行时,从队列中取出一个解决期约的处理程序,并将解决值8提供给它
- JavaScript运行时,向队列添加一个恢复执行awaitPro函数的任务
- JavaScript运行时,从队列中取出恢复执行awaitIn的任务和值6
- (awaitIn中)打印6
- (awaitIn中)打印waitIn2
- waitIn()返回
- 异步任务完成,JavaScript运行时,从队列中取出恢复执行awaitPro的任务和值8
- (awaitPro中)打印8
- (awaitPro中)打印waitPro2
消息队列上一次添加了promise.resolve(8)、6、
执行之后添加了8
异步函数中加入promise相当于在异步中开启了另一个异步操作
期约取消
借助定时器
new Promise( (resolve, reject) =>{
cancelFn( () => {
setTimeout( console.log, 0, 'delay cancelled');
resolve();
});
});
期约进度通知
异步函数中处理错误语句
try-catch包围
对可能出错的异步操作添加catch回调函数
串联异步操作的方法
注意函数式编程与promise结合,并引入响应式编程,可写出优美的回调代码
promise嵌套
then方法返回期约对象
Promise.resolve将promise串连成一个任务队列
原生generator函数
Node
Node服务器底层就是异步的,定义了很多使用回调和事件的API,例如读取文件内容的默认API fs.readFile()就是异步的。