一步步深入了解JavaScript异步编程

JavaScript异步编程

附有图的有道笔记

异步编程 单线程javascript异步方案

javascript 采用单线程模式执行工作的原因?

与最早的设计初衷有关,JavaScript最早是运行在浏览器上的脚本语言,目的是实现页面上的动态交互,而实现页面交互的核心就是DOM操作,这也就决定了它必须实现单线程模型,否则会出现多线程同步的问题,假设DOM操作是多线程工作,这个修改了DOM,同时那个删除了DOM,这样浏览器就不能决定以哪个结果为准,所以为了避免线程同步的问题,JavaScript被设计成单线程模式工作

JavaScript单线程是指在js执行代码环境当中,负责执行代码的线程只有一个,这种模式的优点是更安全,更简单,缺点是遇到耗时的任务需要等待排队去实现,这会导致整个程序会被拖延出现假死的情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjPJYcrI-1597674758657)(BC607CCB725C408BAC089FEF39F76C56)]

同步模式

同步模式指我们代码中的任务必须依次执行,代码中的任务必须等待上一个任务执行完才能执行,程序的执行顺序和代码的编写顺序是完全一致的,这种方式会比较简单,在单线程情况下我们大多数任务都会以同步模式去执行,这里的同步指排队执行

js在执行过程中维护了一个正在执行的工作表,在这里我们会记录他当前做的事情,当工作表中的任务全部清空以后,这轮工作算是结束了

// 同步执行顺序 
//顺序0:首先会加载整体代码,在调用栈中压入一个匿名函数调用 
console.log('start');//顺序1
function bar(){
	console.log('bar');//顺序3
}
function foo(){
	console.log('foo');//顺序2
	bar()
}
foo()
console.log('end');//顺序4

// start
// foo
// bar
// end

这种排队执行的机制存在一个很严重的问题:如果其中某一行代码执行时间过长,后面的任务就会被延迟,那我们把这种延迟称之为阻塞,这种阻塞对于用户而言会有卡顿或卡死,所以需要异步模式来解决编程当中的耗时操作,如浏览器端的ajax或nodejs中的大文件读写

异步模式

异步模式不会等待这个任务的结束才开始执行下一个任务,对于耗时操作,它都是开启过后就立即执行下一个任务,耗时任务的后继逻辑都是通过回调函数的方式定义,在内部呢,耗时任务完成过后就会自动执行传入的回调函数

异步模式的重要性:没有异步模式,JavaScript无法处理大规模的耗时任务

相比于同步模式,异步的执行顺序是你不能确定的,跳跃的

// 异步执行顺序
//顺序0:首先会加载整体代码,在调用栈中压入一个匿名函数调用 
console.log('start');//顺序1
// 遇到异步调用,放在web apis中执行
setTimeout(function timer1 (){//注意倒计时器是单独工作的并不受我们的js线程影响
	console.log('timer1');//顺序4
},1800)
setTimeout(function timer1 (){
	console.log('timer2');//顺序3
	setTimeout(function timer1 (){
		console.log('timer3');//顺序5
	},1000)
	
},1000)
console.log('end');//顺序2
// start
// end
// timer2
// timer1
// timer3

  • 事件循环机制负责监听调用栈和消息队列,当调用栈中多有任务都结束后,事件循环机制会从消息队列取出第一个回调函数,然后压入到调用栈,此时的消息队列是空的,程序也就暂停了,等到计时器的倒计时结束后,先结束的timer2会被放在消息队列的第一位,time1放在消息队列的第二位,一但消息队列发生变化,时间循环机制就会监听到,然后把消息队列中的第一个time2压入调用栈,继续执行time2,此时对于调用栈相当于开启了新的执行,执行顺序是一致的,遇到异步调用就放在web api中执行,如此不断重复,直到调用栈和消息队列都没有需要执行的任务了,整体的代码就结束了
  • 如果说调用栈是一个正在执行的工作表,那么消息队列就是一个代办的工作表,而事件循环就是先执行调用栈中的任务,然后再去执行消息队列中的任务,以此类推,这一过程,可以随时往消息队列里放置任务,这些任务会排队等待事件循环,然后在消息队列里按顺序压入到调用栈执行
  • 以上就是异步调用在JavaScript的实现过程以及它的基本原理,整个过程都是通过内部的消息队列和事件循环去实现的
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VWppBH29-1597674758657)(A66498E444824F739E55D67B4543C8FB)]

这里我们需要注意的是JavaScript是单线程的而我们的浏览器不是单线程的,具体就是通过JavaScript调用的某些内部的api并不是单线程的,例如计时器,内部有一个单独的线程在倒数,在时间到了过后会将我们的回调函数放入消息队列,也就是说这样一件事情是有一个人单独去做的

同步模式的api是代码执行完才会继续往下走比如console.log;
异步模式的api就是下达这个任务开启任务的指令后就会继续执行,不会等待代码执行结束,如setTimeout

回调函数 (所有异步编程方案的根基)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jdaGflEM-1597674758660)(495827D1A8BC4AF49B007B807AEEE514)]
由调用者定义,交给执行者执行的函数被称为回调函数,具体用法就是把函数作为参数传递

function foo (callback){
	setTimeout(function(){
		callback()
	})
}
foo(function(){
	console.log('这是回调函数')
	console.log('调用者定义这个函数,执行者执行这个函数')
	console.log('其实就是调用者告诉执行者异步任务结束时应该做什么')
})

除了这种传递参数的回调函数外还有几种常见的异步方式如事件机制和发订阅,个人认为这些都是基于回调函数的变体

Promise

直接使用传统回调的方式去完成复杂的异步流程,无法避免大量的回调函数嵌套,这也就导致常说的回调地狱的问题,为了避免回调地狱的问题,commonjs社区率先提出来Promise规范,目的是为异步编程提供一个更合理更强大的统一解决方案,后来在ES2015当中被标准化,成为语言规范

所谓Promise就是一个异步对象,用来表示一个异步任务结束之后最终结束过后究竟是成功还是失败,就像是内部对外界做出来一个承诺(promise),一开始这个承诺是待定状态(Pending),最终有可能成功(fulfilled),也有可能失败(rejected)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHYg1TaY-1597674758661)(21E89E13433244CEB1B3F6E58E86B5F0)]
不管这个承诺是成功还是失败都会有相应的反应,也就是说承诺状态最终明确过后,都会有相对应的任务自动执行,而且这种承诺会有一个很明显的特点就是一但明确了结果过后就不可能发生改变了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-btoFkqKZ-1597674758661)(6C65F260EBE54C6A97BB96488D6C0614)]

// Promise 基本示例
const promise = new Promise(function(resolve,reject){
	// 这里用于兑现承诺
	// resolve('200');//承诺达成
	reject(new Error('Promise rejected'));//承诺失败
})
promise.then(function(value){
	console.log('resolved',value)
},function(error){
	console.log('rejected',error)
})
console.log('end')
// 需要注意的是,即便Promise当中没有任何的异步操作,
// then方法指定的回调函数仍然会在回调队列中排队,
// 也就是说这里需要等同步代码执行完了才回去执行

Promise 使用案例

用Promise 封装ajax请求

// Promise 封装ajax
function ajax(url){
	return new Promise((resolve,reject)=>{
		const xhr = new XMLHttpRequest();
		xhr.open('GET',url);
		xhr.responseType = 'json'
		xhr.onload = function(){
			if(this.state === 200){
				resolve(this.response)
			}else{
				reject(new Error(this.statusText))
			}
		}
		xhr.send()
	})
}
ajax('apis/users.json').then((resolve)=>{
	console.log(resolve)
},(reject)=>{
	console.log(reject)
})

Promise的误区

Promise的本质是定义异步任务结束后的所需要执行的任务

嵌套使用的方式是Promise最常见的使用误区

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
// 回调地狱
ajax('/api/users.json').then(function (res) {
  ajax('/api/users.json').then(function (res) {
	ajax('/api/users.json').then(function (res) {
	  ajax('/api/users.json').then(function (res) {
	    ajax('/api/users.json').then(function (res) {
	      
	    })
	  })
	})	
  })
})

以上这种情况嵌套函数没有意义,而且增加了Promise的复杂度

正确做法是通过Promise的then方法链式调用,尽可能使异步任务扁平化

Promise链式调用

链式调用能最大限度的避免嵌套

Promise内部会返回一个Promise对象,并且返回的对象是一个新的Promise对象,其目的是实现一个链条,表示一个承诺过后会返回一个新的承诺,每个承诺可以负责一个异步任务,并且相互之间没有任何影响

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
ajax('/api/users.json').then(function (res) {
  console.log(111111)
}) .then(function (res) {
  console.log(222222)
}) .then(function (res) {
  console.log(333333)
}) .then(function (res) {
  console.log(444444)
}) .then(function (res) {
  console.log(555555)
}) .then(function (res) {
  console.log(666666)
}) 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jxnl7Ft1-1597674779089)(FE383F245F504016918A39AFFEE53819)]
每个Promise对象的第一个then方法都可以return一个Promise对象,用于链式调用下一个then方法当中的回调,这样可以避免不必要的回调嵌套

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
ajax('/api/users.json').then(function (res) {
  console.log(111111)
  console.log(res)
  return ajax('/api/users01.json')
}) .then(function (res) {
  console.log(222222)
  console.log(res)
  return ajax('/api/users02.json')
}) .then(function (res) {
  console.log(333333)
  console.log(res)
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zGczVE0U-1597674779090)(FDA938A2657F4D1FB549908375D9E3A6)]

需要注意的是前面then方法回调的返回值会作为后面then方法回调的参数,如果回调中返回的是Promise对象,后面的then方法的回调函数会等待他的结果

总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-atqTWyqc-1597674779090)(731400F69EF7491484BF8B734748BC9A)]

Promise异常处理

1.rejected回调

只能捕获到当前Promise对象的异常

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
ajax('/api/usersc.json').then(function (resolve) {
  console.log(resolve)
 
},function(reject){
	console.log('异常结果',reject)
	return ajax('/error-url')
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ENKBzRQ-1597674779092)(86361CE890494D66B756D60CDE05132E)]

2.catch方法

catch方法是then方法的别名相当于

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
ajax('/api/usersc.json').then(function (resolve) {
  console.log(resolve)
 
}).catch(undefined,function(error){
		console.log(error)
	})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8fCIAEkz-1597674779092)(D902968F492D433299FD7CCCEB061DCE)]

catch更适合链式调用,它能够捕获Promise链式调用时前面多有Promise的异常信息

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
ajax('/api/users.json').then(function (resolve) {
  console.log(resolve)
	return ajax('/error-url')
}).catch(undefined,function(error){
		console.log(error)
	})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XA6u2Yw-1597674779093)(0C03D5375D73435091B1C1F979F5904C)]

unhandledrejection

unhandledrejection 用于处理一些没有被手动捕获的异常
在浏览器注册window上面,在node,注册在process上面

window.addEventListener("unhandledrejection", event => {
  const {reason,promise} = event;
  console.log(reason,promise)
  // reason失败的原因--一般是错误的对象
  // promise:错误的promise对象
});

process.on("unhandleRrejection", (reason,promise) => {
 
  console.log(reason,promise)
  // reason失败的原因--一般是错误的对象
  // promise:错误的promise对象
});

更多内容

Promise 静态方法

Promise.resolve()

快速把一个值转换成Promise对象

Promise.resolve('fpp').then(res=>console.log(res));//fpp

等同于

new Promise((resolve,reject)=>resolve('fpp')).then(res=>console.log(res));//fpp

通过Promise.resolve包装一个Promise对象,得到的是原本的Promise对象

const promise01 = ajax('/api/users.json');
const promise02 = Promise.resolve(promise01);
console.log(promise01 === promise02);//true

特殊情况:如果传入一个对象,这个对象也有then方法,包含resolve和reject两个参数,那么这个对象也可以被当作Promise对象去执行

Promise.resolve({
	then(onFullfilled,onRejected){
		onFullfilled('foo')
	}
}).then(function (resolve) {
  console.log(resolve);//foo
})

这种特殊的情况可以把第三方的then方法转成promise对象

Promise.reject()

快速创建失败的Promise对象,传入的数据是失败的理由

Promise并行执行

如果 多个接口相互之间没有依赖,我们可以一起请求,可以减少消耗的时间

Promise.all()

Promise.all会等待所有的Promise都结束才会执行

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
Promise.all([
	ajax('/api/users.json'),
	ajax('/api/users01.json')
])//,两个任务都成功了才会返回到then方法,成功返回包含两个Promise对象结果的数组
.then(resolve=>console.log(resolve))
.catch(error=>console.log(error))//如果两个任务有一个失败了,另外一个也以失败结束

串行和并行相结合

ajax('/api/url.json').then((res)=>{
	const urls = Object.values(res)
	const arr = [];
	urls.map((v,i,a)=>{
		arr.push(ajax(v))
	}) 
	return Promise.all(arr)
})
.then(resolve=>console.log(resolve))
.catch(error=>console.log(error))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hbcAHk1V-1597674779095)(8169B7A4A89F41FC8B47FFCEF0E3604E)]

Promise.race()

跟着所有完成的任务中第一个完成任务的一起结束

const a = ajax('/api/users.json');
const b = new Promise(function(resolve,reject){
	setTimeout(function(){
		reject(new Error('异常'))
	},500)
})

Promise.race([
	a,
	b
])
.then(resolve=>console.log(resolve))
.catch(error=>console.log(error))
//注意:通过network的online调成show 3G查看

Promise 执行时序 宏任务/微任务

// 微任务
console.log('start')
setTimeout(function(){
	console.log('settimeout')
},0)
Promise.resolve()
.then(res=>console.log('promise1'))
.then(res=>console.log('promise2'))
console.log('end')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DvbRwggi-1597674779095)(680A9D02DA884228948D38352023066C)]

回调队列中的任务被称之为宏任务,而宏任务执行过程中有可能会临时加上一些额外的需求,这些额外的需求可以选择作为一个新的宏任务进入任务队列中排队,也可以作为当前任务中的微任务,之间在当前任务结束后立即去执行,而不是回到队伍的末尾重新排队,这就是宏任务和微任务的差异,而Promise的回调是作为微任务执行的,他就会在本轮结束的末尾继续执行,这也就是为啥先打印promise再打印settimeout的原因,因为settimeout是以宏任务的形式进入队列的末尾再执行的

微任务是后来引入的,提高了整体的响应能力,目前绝大部分异步调用都是作为宏任务执行,而Promise&&MutationObserver以及node的process.nextTick都会作为微任务之间在本轮调用末尾执行

Generator异步方案(上)

generator执行流程

function * foo () {
	try {
		console.log('foo')
		const res = yield 'foo';
		console.log(res);//res能通过next方法传递参数的方式传递进来值
	}catch(e){
		console.log(e)
	}
}
const generator = foo();
const result = generator.next();
console.log(result)
const result2 = generator.next('bar');
console.log(result2)
generator.throw(new Error('异常'))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5PuHfjc-1597674779096)(1EAEDBE4FC5A43AE9EBA9CBB096A88A1)]

Generator异步方案(中)

可以用yield暂停生成器函数的一个特点来实现更优的异步编程体验

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
function * main (){
	const users = yield ajax('/api/users.json');
	console.log('users',users)
	const users01 = yield ajax('/api/users01.json');
	console.log('user01',users01)
}
const generator = main();
const result = generator.next();//拿到ajax请求结果
console.log(result);//{value: Promise, done: false}
const { value,done } = result;//获取请求ajax返回的promise对象


value.then((data)=>{
	// 将ajax得到的值传递给main方法的users
	const {value,done} = generator.next(data);//以此类推
	console.log('done2',done)
	if(done) return;
	value.then((data)=>{
		// 将ajax得到的值传递给main方法的users
		const {value,done} = generator.next(data);
		console.log('done3',done)
		if(done) return
		value.then((data)=>{
			// 将ajax得到的值传递给main方法的users
			const {value,done} = generator.next(data);
			console.log('done4',done)
			
		})
		
		})
	
})


这种异步编程体验,对于promise内部彻底消灭了promise的回调,有一种近乎于同步代码的体验

generator异步方案(下)

递归执行generator函数

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
function * main (){
	try {
		const users = yield ajax('/api/users.json');
		console.log('users',users)
		const users01 = yield ajax('/api/users01.json');
		console.log('user01',users01)
	}catch(e){
		console.log(e)
	}
}
const generator = main();
// 递归函数执行Generator
function handleResult(result){
	const { value,done } = result;//获取请求ajax返回的promise对象
	value.then((data)=>{
		// 将ajax得到的值传递给main方法的users
		const result = generator.next(data);//以此类推
		const { value,done } = result;
		if(done) return;
		handleResult(result)
	},error =>{
		generator.throw(error)
	})
}
handleResult(generator.next())

更进一步

// 封装co函数,参数传递generator函数
function co(generatorFunc){
	const generator = generatorFunc();
	// 递归函数执行Generator
	function handleResult(result){
		const { value,done } = result;//获取请求ajax返回的promise对象
		value.then((data)=>{
			// 将ajax得到的值传递给main方法的users
			const result = generator.next(data);//以此类推
			const { value,done } = result;
			if(done) return;
			handleResult(result)
		},error =>{
			generator.throw(error)
		})
	}
	handleResult(generator.next())
}
co(main)

generator异步方案更有利于异步编程扁平化

异步执行解决方案co(2015年实现)

async/await 语法糖

语言层面的异步编程标准

function ajax (url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
async function main (){
	try {
		const users = await ajax('/api/users.json');
		console.log('users',users)
		const users01 = await ajax('/api/users01.json');
		console.log('user01',users01)
	}catch(e){
		console.log(e)
	}
}
const promise = main()
console.log(promise)
promise.then(res=>{
	console.log('all finfish')
})

async/await相比generator不需要类似co这样的一个执行器,是语言层面的标准异步编程语法,其次async函数会返回一个promise对象,更有利于代码的整体控制

值得注意的是await只能在async的内部去使用,不能在外层单独使用

你可能感兴趣的:(javascript基础,javascript,js)