为什么要使用异步编程
在(javascript)单线程的世界里,如果有多个任务,就必须排队,前面一个完成,再继续后面的任务。就像一个ATM排队取钱似的,前面一个不取完,就不让后面的取。为了这个问题,javascript提供了2种方式: 同步和异步。
回调地狱 (Callbacks Hell)
举个例子:通过api拿到数据,数据里面有图片,图片加载成功渲染,那么代码如下:
request(url, (data) => {
if(data){
loadImg(data.src, () => {
render();
})
}
})
如果有在业务逻辑比较复杂,就成了下面这个样子:
doSth1((...args, callback) => {
doSth2((...args, callback) => {
doSth3((...args, callback) => {
doSth4((...args, callback) => {
doSth5((...args, callback) => {
})
})
})
})
})
海康初始化的代码
// 初始化
WebControl = new WebControl({
...,
cbConnectSuccess: cbConnectSuccess
})
function cbConnectSuccess(){
// 设置接受控件返回信息
oWebControl.JS_SetWindowControlCallback({
cbIntegrationCallBack: cbIntegrationCallBack
})
}
function cbIntegrationCallBack(data){
// 接收控件信息
console.log(data)
}
使用方法回调来接收一个异步任务的结果会造成很多不便
-
异常处理不方便
当某个方法执行失败后,不利于进行重试,不宜与指定重试的时机
-
流程控制不方便
不宜与对某些还没执行的异步任务进行清除
实际场景
假设现在有一个方法A
需要执行,但是他需要等WebControl
初始化完毕后才能执行,有以下几种情况
- 使用
setTimeout
延迟指定时间,等待控件加载完成后,在执行某些操作。
弊端:会浪费多余的时间,如果因为某些因素控件加载过长,就会导致后续逻辑无法进行。
- 在
WebControl
初始化完毕后将一个全局变量设置为True
,方法A
通过这个变量来判断是否可执行
弊端:如果这个方法是不可舍弃的,那会丢掉这一次函数执行,例如项目中有个场景,在自动监控打开的时候,刚好推过来一条新的进店消息,是有概率丢失这一条消息的
最好的解决方法:使用异步队列来管理这些任务
解决初始化完成时机
Promise严格来说不是一种新技术,它只是一种机制,一种代码结构和流程,用于管理异步回调。
promise
状态由内部控制,外部不可变- 状态只能从
pending
到resovled
,rejected
,一旦进行完成不可逆 - 每次
then/catch
操作返回一个promise实例,可以进行链式操作
对比上下两段初始化代码:
// 旧
WebControl = new WebControl({
...,
cbConnectSuccess: cbConnectSuccess
})
function cbConnectSuccess(){
oWebControl.JS_SetWindowControlCallback({
cbIntegrationCallBack: cbIntegrationCallBack
})
}
function cbIntegrationCallBack(data){
console.log(data)
}
// 新
function init(){
if(window.initWebControl){
await window.initWebControl
}
window.initWebControl = new Promise((resolve,reject)=>{
// 将用于改变promise状态的resolve存储起来,方便后续去通知完成
window.initWebControlResolve = resolve
WebControl = new WebControl({
...,
cbConnectSuccess: cbConnectSuccess
})
})
}
function cbConnectSuccess(){
oWebControl.JS_SetWindowControlCallback({
cbIntegrationCallBack: cbIntegrationCallBack
})
}
function cbIntegrationCallBack(data){
window.initWebControlResolve() //改变promise
}
队列管理
过期清除
缓存一定数量的异步队列,可以将过期的、重复的异步任务删除
-为什么要使用?
- 例如海康的api都是基于回调的方式,那么很有可能某些api调用并没有响应,这个promise就一直在运行中,导致后续的任务没有机会运行
- 方便在插入异步任务时,对以往重复的,不需要的进行舍弃
class LruCache {
constructor(max) {
this.max = max//最大缓存长度
this.cache = new Map()//用一个map来存储缓存
}
get(key) {
if (this.cache.has(key)) {//如果该值存在 则将该值从队列拿出来放到最后一位
const val = this.cache.get(key)//1.取出来
this.cache.delete(key)//2.从现在的位置删除
this.cache.set(key, val)//3.放在队尾
return this.cache.get(key)
}else return undefined
}
set(key, value) {
//如果当前值存在 就把当前值删除
if (this.cache.has(key)) {
this.cache.delete(key)
}
//然后将当前值放在队尾
this.cache.set(key, value)
//如果在进行set操作后 超过了预设长度 则把队首的删除
//这也就是为什么会把重复get的值放在队尾
//原则上来说 被删除掉的都是在一定时间范围内使用频率不高的缓存值
if (this.cache.size > this.max) {
this.cache.delete(this.cache.keys().next().value)
}
}
}
接下来我们再基于上述例子,实现一个定时过期的功能。
我的思路是 在数据set的时候同时进行一个setTimeout
操作, 并且在删除数据的时候进行一次clearTimeout
,直接上代码!
class LruCache {
constructor({ max, time }) {
this.max = max
this.time = time
this.cache = new Map()
}
has(key) {
return this.cache.has(key)
}
get(key) {
if (this.cache.has(key)) {
//valu是一个数组,该数组的第0个元素是set的时候的数据,
//第一个元素是settimeout返回的事件句柄,清除定时器时能用到
const val = this.cache.get(key)
clearTimeout(val[1])
this.cache.delete(key)
const timer = setTimeout(() => {
this.cache.delete(key)
}, this.time);
this.cache.set(key, [val[0], timer])
return this.cache.get(key)[0]
} else return undefined
}
set(key, value) {
if (this.cache.has(key)) {
const val = this.cache.get(key)
clearTimeout(val[1])
this.cache.delete(key)
}
const timer = setTimeout(() => {
this.cache.delete(key)
}, this.time)
//在set操作的时候一定要将settimeout返回的事件句柄保存起来
//用于在适当的时机进行清除定时器操作
this.cache.set(key, [value, timer])
if (this.cache.size > this.max) {
const dk = this.cache.keys().next().value
clearTimeout(this.cache.get(dk)[1])
this.cache.delete(dk)
}
}
}
这个类就实现了一个接口缓存的功能,且支持过期接口数据清理的功能
改造结果
基于以上思路,写了一份符合业务的类
需求分析:
- new一个这个队列,然后可以指定这个队列的启动和停止,会依次执行队列中的所有异步任务,
- 后续每次新增一个队列的时候,检测是否有在运行的异步任务,没有就运行
- 支持超时异步队列移除
- 支持异步任务错误处理
任务队列和执行
class AsyncQueue {
static _$uuid_count = 1 //用于生成任务的唯一标识符
_queues = null //存储待执行任务的队列。
_runningAsyncTask = null //当前正在执行的任务
_enable = false //启用队列
errHandle = null //发生错误会调用
constructor({errHandle}) {
this._queue = []
this.errHandle = errHandle
}
/**
* 往队列末位插入一个任务
* @param callback 异步方法
* @param params 传递给异步方法的参数
* @return {number} 任务的唯一标识符
*/
push(callback, params) {
const uuid = AsyncQueue._$uuid_count++
this._queues.push({
uuid,
callback,
params
});
return uuid;
}
shift() {
this._queues.shift()
}
play() {
// 判断是否可以继续
if (!this._runningAsyncTask && this._enable && this._queues.length > 0) {
return
}
const actionData = this._queues[0]
// 记录当前任务
this._runningAsyncTask = actionData
// 执行当前任务
await actionData.callback(...actionData.params)
this.next()
}
next() {
this._runningAsyncTask = null;
this._queues.shift()
}
}
实现异常捕获
play() {
// 判断是否可以继续
if (!this._runningAsyncTask && this._enable && this._queues.length > 0) {
return
}
const actionData = this._queues[0]
// 记录当前任务
this._runningAsyncTask = actionData
// 执行当前任务
try{
await actionData.callback(...actionData.params)
}catch(e){
// 执行相关操作,通知外部允许发生错误,例如可以通过传递一个异常处理函数,在这里调用
this.errHandle(actionData)
}finally{
this.next()
}
}
实现超时任务处理
// 放工具函数
function timeoutPromise(promise, ms = 1000) {
// 超时fn
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('执行超时')), ms)
)
return Promise.race([promise, timeout])
}
play() {
...
try{
// 具有超时记录的方法
await timeoutPromise(actionData.callback(...actionData.params))
}catch(e){
// 执行相关操作,通知外部允许发生错误,例如可以通过传递一个异常处理函数,在这里调用
this.errHandle(actionData)
}finally{
this.next()
}
}
插队
当某个任务失败时,往往需要重新执行,而不是插入到队尾排队等待
jumpingInLine(callback, params = [], uuid) {
let index = this._queues.findIndex(item => item.uuid === uuid)
if (index === -1) {
this._queues.push({
uuid,
callback,
params
})
} else {
this._queues.splice(index, 0, {
uuid,
callback,
params
})
}
return uuid
}
重试
通过将失败的任务,通过插队的方式,放到下一次调用
let re_count = 5 //重试次数
let re_execute_count = 0 //当前重试次数
let re_execute_uuid = null //当前重试的任务id
function handleReExecute(actionData) {
if(re_execute_uuid!==actionData.uuid){
re_execute_uuid = actionData.uuid
re_execute_count = 0
}
re_execute_uuid = actionData.uuid
re_execute_count++
if (re_execute_count > re_count) {
return
}
queue.jumpingInLine(actionData.callback, actionData.params, actionData.uuid)
console.log(`失败的任务重复执行,次数${re_execute_count},`, actionData)
}
保证每次队列操作时自动执行任务
目前实现的只有手动,如果当前任务已经执行完毕,这时候再push一个任务,不会自动执行
解决思路:劫持数组
// 重写数组push和shift,重新监事队列是否有空余
class QueueArray extends Array {
constructor(queueClass) {
super();
this.queueClass = queueClass
}
enableQueue() {
if (this.queueClass) {
this.queueClass.play()
}
}
push(item) {
let result = super.push(item)
this.enableQueue()
return result
}
shift() {
let result = super.shift()
if (this.length > 0) {
this.enableQueue()
}
return result
}
splice() {
let result = super.splice(...arguments)
if (this.length > 0) {
this.enableQueue()
}
return result
}
}
改造普通数组
class AsyncQueue {
...
constructor(){
// this._queues = []
this._queues = new QueueArray(this)
}
}
测试数据
let nowPlayList = []
async function multerPlay(playList) {
nowPlayList.value = playList
return new Promise((resolve, reject) => {
// 90%概率成功
setTimeout(() => {
let isSuccess = Math.random() > 0.1
if (isSuccess) {
resolve('批量播放成功')
} else {
reject('批量播放失败')
}
}, 200)
})
}
async function multerCloseVideo(videoList) {
return Promise.all([...videoList.map(item => closeVideo(item))])
}
async function closeVideo(videoInfo) {
return new Promise((resolve, reject) => {
// 90%概率成功
setTimeout(() => {
let isSuccess = Math.random() > 0.6
if (isSuccess) {
let index = nowPlayList.value.findIndex(item => item.id === videoInfo.id)
if (index !== -1) {
nowPlayList.value.splice(index, 1) //删除视频
}
resolve('关闭视频成功')
} else {
reject('关闭视频失败')
}
}, 200)
})
}
// 异常处理
function handleError(actionData) {
// 可以通过对比函数地址来判断是哪个方法执行失败
let {params, callback, uuid} = actionData
if (callback === multerPlay) {
handleReExecute(actionData)
}
if (callback === closeVideo) {
handleReExecute(actionData)
}
if (callback === multerCloseVideo) {
console.log('批量关闭失败')
}
}