co模块鉴赏分析-JS异步编程的前世

Posted 编程日课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了co模块鉴赏分析-JS异步编程的前世相关的知识,希望对你有一定的参考价值。

本文关键字:单进程、异步操作、Promise

本文中所用的语法为ES6语法是自己的开发习惯,旨在论述ES6以前的异步处理方式。

单线程的JS

     我们知道JS是一种单线程语言,引起异步操作对于JS来说十分重要。我们在日常开发中,场景的异步操作如ajax请求.

如: $.ajax('/url', function(){ });,这就诞生了一种也是最原始的异步处理方式---回调。通过回调函函数方式,处理简单异步操作确实没有毛病。但是,当面对复杂的异步逻辑时它就无法胜任了。

     假设有一种场景前端需要发起两次AJAX请求,而只会当两次请求返回后才能做相应的处理。还有一种请求,如果需要对返回的进行多次判断。那么,使用回调函数时,自己将要做很多状态的处理。下面我们用setTimeout来模拟异步操作。


function testCase (eventA, eventB) {
let   cnt = 0;
 const eventACallback = () => {
cnt ++;
   
if (cnt === 2) {
console.info('eventACallback->等待完毕:', eventA, eventB);
   
}
};


 
const eventBCallback = () => {
cnt ++;
   
if (cnt === 2) {
console.info('eventBCallback->等待完毕:', eventA, eventB);
   
}
};


 
setTimeout(() => {
eventBCallback()
}, eventB);

 
setTimeout(() => {
eventACallback()
}, eventA);

}

testCase(500, 300);
testCase(300, 500);
//输出结果:
// eventACallback->等待完毕: 500 300
// eventBCallback->等待完毕: 300 500

         你会发现在A回调方法里我需要关注B事件状态的值,而在B回调方法里也需要关注A事件的值。这样的写法着实让人无法接受。想象下如果状态的数量不止两个的情况,那你的状态标志位就必须分享在N个回调函数里,想想都觉得可怕。这时候,在JS的先贤们是怎么解决的?

      上一章我们提到FP世界里,最核心的工具就是闭包。我们是否可以借助闭包做点文章呢?我可否把状态封装在内部?把结果与操作解耦分开,于是出现了一个叫Promise的概念,这个语法在ES6中已经成为了语言标准,下面我们自己动手来写一个简单的Promise实现。

动手写Promise

    我们把自己设置在一个不知道Promise设计的场景下,来考虑上面的这个需求。我们来考虑一个问题,如何将结果和操作分开,上诉的回调函数其实就是操作,而回调后产生的status就是结果。那我们是否可以把这个结果放在闭包存放起来呢?

   1、回调还是要有的,因为,异步的本质就是等它好了,它来通知你。而回调函数就是对方通知我们的手段。

   2、通知完的结果我们要知道,这样,才能避免重复通知;

   3、 我们得提供一个对异步结果检查的手段,才能实现上诉的需求。


结合1、2两点,我即要提供一个回调函数,又需要对方通知我们结果,而结果又要在我们的闭包里,那么,可以推论出用于返回结果的方法必须是我们提供的。你想状态在我们的闭包里进行管理,别人是拿不到的,除非,它使用我们给它的方法,而这个方法里我们可以使用改变量。可能你会问为什么要用闭包?因为,我们刚才的例子太过复杂就是因为状态散落在各个回调里,我们想把它统一到一个地方去处理。好,上代码!

function testCase (eventA, eventB) {
function MyPromise (cb) {

let result = '',
     isDone = false;

   function resolve (data) {
if (isDone) return;
     result = data;
     isDone = true;
   }

cb(resolve);
 }


new MyPromise((resolve) => {
setTimeout(() => {
resolve()
}, eventA);
 });
 new MyPromise((resolve) => {

setTimeout(() => {
resolve()
}, eventB);
 });

}

testCase(500, 300);
testCase(300, 500);

到这里,我们实现了我们内部持有异步处理结果的状态。接下来我们考虑处理第3点,如何获取两次的异步结果呢?答案,还是回调。为什么?因为,异步的结果产生的时机还是未知的,未知的东西只能靠别人来通知你。那这个回调哪里来?只能还是由我们的MyPromise对象给出,没错!因为状态在我们MyPromise的闭包里。

function testCase (eventA, eventB) {
function MyPromise (cb) {

let result = '',
     isDone = false;

   function resolve (data) {
if (isDone) return;
     result = data;
     isDone = true;
   }

cb(resolve);
   return {
then (sucess) {
if (isDone) {
sucess(result)
}
}
}
}


new MyPromise((resolve) => {
setTimeout(() => {
resolve()
}, eventA);
 }).then(data => {
console.info('result is', data);
 });
 new MyPromise((resolve) => {

setTimeout(() => {
resolve()
}, eventB);
 });

}

testCase(500, 300);

我们通过返回一个对象,而这个对象里有一个then方法。方法里放一个外部的回调函数,这样外部就可以通过这个回调函数获取异步的结果。运行代码,没结果,为啥?

       这里我们就得讲讲setTimeout方法,我们知道JS本身是单线程,单线程意味着,当你开始执行某段JS以后,那它必然会一直执行到完毕为止,除非遇到异常错误,而setTimeout方法的作用是产生一个任务放到一个任务队列里,等到所有的JS执行栈全部执行完毕后,就会去任务队列里取任务出来执行。

PS:关于这方面的介绍,后续会有文章进行分析,现在只要知道这些即可。ES6语言级别上有明确的规范

        所以,当你执行then的时候这段代码是在主执行栈里,而setTimeout则是在其执行后执行。所以,上面的代码是不会有输出结果的。

       那我们该怎么办?既然主执行栈是一口气全部跑完,而我们又需要异步获取这个结果,那我们只剩下一个办法了,就是异步结果的返回也用setTimeout返回,我们把then回调函数,放到setTimeout里执行。注意,我们刚才说过setTimeout是一种队列机制,既然是队列机制那就一定有执行顺序,这个顺序就是先进先出。所以,我们可以这样做:

function testCase (eventA, eventB) {
function MyPromise (cb) {

let result = '',
     isDone = false;

   function resolve (data) {
if (isDone) return;
     result = data;
     isDone = true;
   }

cb(resolve);
   return {
then (sucess) {
setTimeout(() => {
if (isDone) {
sucess(result)
}
}, 0);
     }
}
}


new MyPromise((resolve) => {
setTimeout(() => {
resolve()
}, eventA);
 }).then(data => {
console.info('result is', data);
 });
 new MyPromise((resolve) => {

setTimeout(() => {
resolve()
}, eventB);
 });

}

testCase(500, 300);

の,还是没有结果?为什么?分析下,发现是因为我们确实排队了,但是当我们任务执行时候,结果还没算出来?那怎么办?我们只能通过轮询的方式去处理。当然如果底层支持的话,我们也可以基于事件通知,比如异步IO中event poll处理。

function testCase (eventA, eventB) {
function MyPromise (cb) {

let result = '',
     isDone = false;

   function resolve (data) {
if (isDone) return;
     result = data;
     isDone = true;
   }

cb(resolve);
   return {
then (sucess) {
function watchAgain () {
setTimeout(() => {
if (isDone) {
sucess(result)
} else {
watchAgain();
           }
}, 1);
       }

watchAgain();
     }
}
}



const myPromiseA = new MyPromise((resolve) => {
setTimeout(() => {
resolve('a')
}, eventA);
 });
 myPromiseA.then(data => {
console.info('result is', data);
 });
 myPromiseA.then(data => {
console.info('result is', data);
 });


 const myPromiseB = new MyPromise((resolve) => {

setTimeout(() => {
resolve('b')
}, eventB);
 });
 myPromiseB.then(data => {
console.info('result is', data);
 });

}

testCase(500, 300);
//输出结果:
// result is b
// result is a
// result is a

     大家留意下watchAgain的写法,这也是场景FP写法,自递归函数。当然,如果你开心也可以用setInterval进行处理,这个与setTimeout除了循环次数是不断循环外,其它都与setTimeout一样。

    这个时候,我们已经可以在外部拿到内部状态。你会发现,一个MyPromise是可以被then多次的,这也是MyPromise一个很重要的功能,结果可以被多次使用。当然,到这里我们还没有实现我们上面的那个A&B同时发生的需求。

     如何考虑这个问题?

     首先,我们在各个MyPromise实际上可以通过then拿到状态

     其次,现在的需求是要两个Promise都触发后触发回调,那这个回调可不可也是一个MyPromise?答案是可以,因为我们设计MyPromise的目的就是为了将回调函数封装起来。那对我们来说,我们怎么根据这两个Promise去获取一个Promise呢?这是FP的思想又跑了出来,我们需要一个操作,我们需要一个函数。(~ o ~)~zZ,MyPromise.all([promiseA, promiseB]),返回一个MyPromise,而这个MyPromise只有当A&B都完成后才会触发。设计已成,开论实现。

      这个时候,思考的第一个问题应该是我们有什么,这些能否实现我们的需求?也就是,通过我们现有的入参两个MyPormise能实现需求吗?如若不行,我们再想办法加参数!我们知道,每一个MyPromise都有一样逻辑的then函数,编程过程中只要将数据结构统一,那剩下就是流程化的处理就可以实现功能。

     我们只要对每一个Promise都进行then操作,放入统一的cb,而后通过计数器去计算,当临界值到来的时候,触发返回的MyPromise即可。当然,这里我们还要用到我们setTimout神器来处理,否则,你无法使MyPromise.all的计数器判断与MyPromise.then在同一个执行栈中。所以,我们写出了下面的Promise.all

MyPromise.all = (...promise) => {
return new MyPromise(resolve => {
let cnt = 0;
   let result = [];
   promise.forEach(promise => {
promise.then((data) => {
result.push(data);
       cnt++;
     })
});

   function watchAgain () {
setTimeout(() => {
if (cnt === promise.length) {
resolve(result)
} else {
watchAgain();
       }
}, 1);
   }

watchAgain();
 })

};


这就是JS异步操作的前世,接下来我将继续讲述它的今生。最终代码如下:

function testCase (eventA, eventB) {
function MyPromise (cb) {

let result = '',
     isDone = false;

   function resolve (data) {
if (isDone) return;
     result = data;
     isDone = true;
   }

cb(resolve);
   return {
then (sucess) {
function watchAgain () {
setTimeout(() => {
if (isDone) {
sucess(result)
} else {
watchAgain();
           }
}, 1);
       }

watchAgain();
     }
}
}

MyPromise.all = (...promise) => {
return new MyPromise(resolve => {
let cnt = 0;
     let result = [];
     promise.forEach(promise => {
promise.then((data) => {
result.push(data);
         cnt++;
       })
});

     function watchAgain () {
setTimeout(() => {
if (cnt === promise.length) {
resolve(result)
} else {
watchAgain();
         }
}, 1);
     }

watchAgain();
   })

};


 const myPromiseA = new MyPromise((resolve) => {
setTimeout(() => {
resolve('a')
}, eventA);
 });


 const myPromiseB = new MyPromise((resolve) => {
setTimeout(() => {
resolve('b')
}, eventB);
 });
 MyPromise.all(myPromiseA, myPromiseB).then(data => {
console.info('finsishA & B', data);
 });
}

testCase(500, 300);
//输出结果:
// finsishA & B [ 'b', 'a' ]

      这里的MyPromise是很粗糙的实现。没有考虑异常错误的处理,也没有完整实现ES6的Promise规范。有兴趣的朋友,可以去看https://github.com/stefanpenner/es6-promise.git,这是一个完整的Promise腻子函数。

以上是关于co模块鉴赏分析-JS异步编程的前世的主要内容,如果未能解决你的问题,请参考以下文章

浅谈.Net异步编程的前世今生----TPL篇

浅谈.Net异步编程的前世今生----APM篇

Node.js中yield的异步编程使用&记一次解决map与co库配置使用的问题

Node.js中yield的异步编程使用&记一次解决map与co库配置使用的问题

浅谈.Net异步编程的前世今生----EAP篇

浅谈.Net异步编程的前世今生----异步函数篇(完结)