1.前言
小程序开发中经常遇到后一个操作依赖前一个操作异步执行结果的情形。虽然JavaScript是单线程语言,但是主线程中的耗时操作通常都被放入任务队列中异步执行,避免阻塞主线程,例如:
let f1 = function (sequence) {
console.log("f1开始执行");
setTimeout(function () {
console.log("f1执行完成");
},10)
}
let f2 = function (sequence) {
console.log("f2开始执行");
setTimeout(function () {
console.log("f2执行完成");
},30)
}
let f3 = function (sequence) {
console.log("f3开始执行");
setTimeout(function () {
console.log("f3执行完成");
},1)
}
let test=function(){
f1();
f2();
f3();
}
运行test函数发现执行结果为:
可以看到函数f1,f2和f3中的console部分按照test中的调用顺序依次执行,这是因为这部分代码是在主线程中执行的,是同步执行。而setTimeout的回调函数则未按照调用顺序依次执行,这是因为setTimeout被放入任务队列中异步执行,因为执行耗时f3<f1<f2,所以f3中的setTimeout最先从任务队列中返回到主线程中执行回调函数,而f2最后返回。
关于JavaScript执行顺序更多知识,参见:前端干货:JS的执行顺序 - 简书JS的运行机制 先来一个今日头条的面试题 1. 单线程的JavaScript js是单线程的,基于事件循环,非阻塞IO的。特点: 处理I/O型的应用,不适合CPU运算密集型...https://www.jianshu.com/p/62c7d633a879
如果f2的执行依赖f1中setTimeout回调函数的执行结果,而f3又依赖f2中的执行结果,则要保证三个函数中的异步操作顺序执行,例如:一个操作需要依次发出多个网络请求才能完成。这里提出一种非阻塞(主线程不用等待)方法,确保三个函数中的异步操作顺序执行。
2.使用轮询的方式确保异步操作顺序执行
为了避免小程序UI界面卡顿,不能采用阻塞主线程的方法,因此这里采用setInterval定时器实现异步轮询操作,确保异步操作顺序执行。首先定义顺序执行类:
let SequentialExec=class SequentialExec {
constructor(func_list) {
//顺序执行函数列表
this.func_list = func_list;
// 计数器
this.count = 0;
// 函数执行标志
this.running = false;
//函数执行结果
this.res = null;
//定时器序号
this.timer=null;
}
/**
* 启动定时器轮询
*/
wait(){
if(!this.timer){
// 启动定时器轮询next方法
this.timer=setInterval(this.next,5,this)
}
}
/**
* 切换运行函数
* @param {顺序保持类对象} sequence
*/
next(sequence) {
//执行完毕,关闭定时器
if (sequence.count == sequence.func_list.length) {
clearInterval(sequence.timer);
return;
}
if (sequence.running == false) {
try {
sequence.running = true;
//运行下一个函数
sequence.func_list[sequence.count](sequence);
sequence.count += 1;
// 异步执行等待操作
sequence.wait();
} catch (error) {
clearInterval(sequence.timer);
throw error;
}
}
}
}
使用示例
let f1 = function (sequence) {
console.log("f1开始执行");
setTimeout(function () {
console.log("f1执行完成");
sequence.running = false;
sequence.res=1;
}, 10)
}
let f2 = function (sequence) {
console.log("f1执行结果",sequence.res)
console.log("f2开始执行");
setTimeout(function () {
console.log("f2执行完成");
sequence.running=false;
sequence.res=2;
}, 30)
}
let f3 = function (sequence) {
console.log("f2执行结果",sequence.res)
console.log("f3开始执行");
setTimeout(function () {
console.log("f3执行完成");
sequence.running=false;
}, 1)
}
let test = function () {
let sequence=new SequentialExec([f1,f2,f3]);
sequence.next(sequence);
}
运行test函数输出结果:
原理
因为JavaScript的类对象作为参数传递时是浅拷贝(传址),所以可以用SequentialExec对象作为函数参数,用running属性记录异步操作的运行状态,用res属性保存异步操作结果,用count计数器决定执行的函数。在这里,SequentialExec类的对象sequence充当了func_list中函数的观察者,即采用了观察者模式。
3.错误捕获
对于多个顺序执行的函数,如果其中一个函数出现错误,需要终止执行只需关闭定时器即可:
let SequentialExec=class SequentialExec {
....
/**
* 错误处理
*/
error(error) {
clearInterval(this.timer);
this.running=false;
console.log("出现错误,终止执行")
throw error;
}
....
}
例如执行f2出现错误:
....
let f2 = function (sequence) {
console.log("f1执行结果", sequence.res)
console.log("f2开始执行");
setTimeout(function () {
//抛出错误,终止执行
sequence.error("");
console.log("f2执行完成");
sequence.running = false;
sequence.res = 2;
}, 30)
}
....
执行结果
4.注意事项
(1)上述方法适用于函数f1,f2和f3中只有一个异步函数的情形。
(2) sequence.running = false;必须在异步函数中调用,否则顺序执行不起作用。
5.总结
上述方法的优点是保持异步操作执行顺序的同时不会阻塞主线程,方便在多个异步操作中传递数据且处理错误比较方便。
但是缺点也很明显,就是这种方法是侵入式的,会产生大量sequence.running = false调用。