javaScript 中的异步编程
javaScript 中的异步编程
- 回调函数
- 事件监听
- 发布/订阅
- promise模式
function fn1(){ var arr=[]; var result; //模拟耗时的任务; for(let i=0;i<100000;i++){ for(let j=0;j<100000;j++){ result+=j; } } arr.push(new Array(10000).join('*')); console.log("fn1 task has done"); } function fn2(){ console.log("fn2 task has done"); } fn1(); fn2();
用setTimeout和回调函数来进行“异步”
//匿名函数 ,闭包,没有了return 和 throw exception的特性滴啊 function fn1(Callback){ setTimeout(function() { var arr=[]; var result; //模拟耗时的任务; for(let i=0;i<100000;i++){ for(let j=0;j<100000;j++){ result+=j; } } arr.push(new Array(10000).join('*')); console.log("fn1 task has done"); Callback(); }, 1000); } //采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行 function fn2(){ console.log("fn2 has done.."); } fn1(fn2); console.log("mian line has done.."); //这种方式仅仅是防止堵塞fn2的执行,和主线层的执行,。。。。还算不得真正的异步;
总结,第一种做法,堵塞fn2的执行和主线程的执行;
第二种,没堵塞主线程的执行,fn2成为fn1,的回调函数,执行完之后,再执行fn2;
问,如果主线程也遇到一个堵塞操作呢;? 那就要看事件的注册顺序了!因为js的执行是单线程的,如果是多线程的话,
就会出现交替输出的效果;
如果,我们将代码改着下面这样;
function fn1(Callback){ setTimeout(function() { var arr=[]; var result; //模拟耗时的任务; for(let i=0;i<100000;i++){ for(let j=0;j<100000;j++){ console.log(j); } } //arr.push(new Array(10000).join('*')); console.log("fn1 task has done"); Callback(); }, 1000); } //采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行 function fn2(){ console.log("fn2 has done.."); } fn1(fn2); console.log("mian line has done.."); while(true){ console.log("......."); }
控制台中一直输出:.......;
js的特点之一:单线程,
缺点:
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,
各个部分之间高度耦合(Coupling),流程会很混乱,
而且每个任务只能指定一个回调函数。
事件监听,
第二种,就是采用事件驱动的模式;任务的执行不取决于
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
我们用node中的事件为示例,node中是出名的基于事件驱动的。
// 引入 events 模块 var events = require('events'); // 创建 eventEmitter 对象 var eventEmitter = new events.EventEmitter(); function f2(){ console.log("f2 has done..."); } // 绑定事件及事件的处理程序 eventEmitter.on('someEvent', f2); function fn1(){ //继续我们的耗时任务; var result; for(let i=0;i<100000;i++){ for(let j=0;j<100000;j++){ result+=j; } } console.log("fn1 task has done"); //触发事件; eventEmitter.emit("someEvent"); } fn1(); console.log("main line has done...");
这种方法的优点是比较容易理解,
可以绑定多个事件,每个事件可以指定多个回调函数,
而且可以"去耦合"(Decoupling),
有利于实现模块化。缺点是整个程序都要变成事件驱动型,
运行流程会变得很不清晰。
上一段代码的"事件",完全可以理解成"信号"。
ps:
Nginx跟Node.js都不是基于多线程模型的,
因为线程跟进程都需要非常大的内存开销。
他们都是单线程的,但是基于事件的。
这种基于单线程的模型消除了为了处理很多请求而创建成百上千个线程或进程带来的开销。
这里不得不用聊一聊我们的基于事件的编程的模式,或者叫publish 和subscribe 的;
发布/订阅(PUB/SUB)是一种消息模式,它有两个参与者:发布者和订阅者。
发布者向某个信道(channel)发布一条消息,
订阅者绑定这个信道,
当有消息发布至信道时就会接受到一个通知。
最重要的一点是,发布者和订阅者是完全解耦的,
彼此并不知道对方的存在。
两者仅仅共享一个信道的名称。
发布者和订阅者的解耦可以让你的运用易于拓展
var PS={ //订阅者角色; subscribe :function (ev,callBack){ //创建_callback对象,除非它已经存在。 var calls=this._callBack || (this._callBack={}); /* * 针对给定的事件ev创建一个数组,除非这个数组已经存在 * 然后调用函数追加到这个数组中 * */ (this._callBack[ev] || (this._callBack[ev]=[]).push(callBack)) return this; }, //发布者角色; publish:function (){ //将arguments对象转换成为真正的数组 var args=Array.prototype.slice.call(arguments,0); //取出第一个参数,就是我们的事件名滴呀; var evName=args.shift(); /* * 如果不存在_callback对象,则返回 * 或者如果不包含给定事件对应的数组 * */ var list,calls,i,l; if(!(calls=this._callBack)) return this; if(!(list=this._callBack[ev]))return this; //循环触发我们的回调事件; for(let i=0,l=list.length;li<l;i++){ list[i].apply(this,args); } return this; } }; //订阅感兴趣的东东; PS.subscribe("life",function (){ // 回调.... }) PS.publish("life");
对了,注意到没有,上面的代码,没有取消事件的方法,我靠;,接下来,我们看一个完整的代码;
function SourceCribe () { // 生成发布/订阅器DOM节点 var body = document.querySelector('body'); if (!document.querySelector('.magazine')) { var element = document.createElement('mark'); element.setAttribute("class", "magazine"); body.appendChild(element); } this.magazine = document.querySelector('.magazine'); // 消息发布实现 this.publish = function (source, data) { if (!typeof source === 'string') { return false; } var oEvent = new CustomEvent(source, { bubbles: true, cancelable: false, detail:data }); this.magazine.dispatchEvent(oEvent); }; // 订阅实现,handler需要使用显式声明函数,不要使用匿名函数 this.subscribe = function (source, handler) { if(!typeof source === 'string' || !typeof value === 'function') { return false; } this.magazine.addEventListener(source, handler, false); }; // 取消订阅 this.unsubcribe = function (source, handler) { if(!typeof source === 'string' || !typeof value === 'function') { return false; } this.magazine.removeEventListener(source, handler, false); }; } (function(window){ window.addEventListener('load',function(evt){ var sourceCribe = new SourceCribe(); var loveHandlerAlways = function (evt) { console.log("always " + evt.detail); }; var loveHandlerEver = function (evt) { console.log("ever " + evt.detail); }; sourceCribe.subscribe('love', loveHandlerAlways); sourceCribe.subscribe('love', loveHandlerEver); sourceCribe.publish('love','500 days with summer'); sourceCribe.unsubcribe('love', loveHandlerAlways); sourceCribe.publish('love','500 days with summer'); }); })(window)
最后,我们推荐一款,非常简单,并且,牛逼的事件订阅插件的,效果是非常好得呀;
(function($) { var o = $({}); $.subscribe = function() { o.on.apply(o, arguments); }; $.unsubscribe = function() { o.off.apply(o, arguments); }; $.publish = function() { o.trigger.apply(o, arguments); }; }(jQuery));
我靠,好像有点侧偏了,明明再说或JavaScript中的异步编程,却扯到我们的额事件上去,不过,我们在学习node,深入理解一下,基于事件的编程是非常完美;
接下来,我们就要将这个发布/订阅 模式了,是基于上面这个框架滴呀;
//这里还有更详尽的文章:https://msdn.microsoft.com/en-us/magazine/hh201955.aspx
function f2(){ console.log("f2 has done..."); } jQuery.subscribe("done", f2); function f1(){ setTimeout(function () { // f1的任务代码 jQuery.publish("done"); }, 1000); } // jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。 // 此外,f2完成执行后,也可以取消订阅(unsubscribe)。 //jQuery.unsubscribe("done", f2); //这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心", //了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
//最后,就是我们的promise对象了滴呀;
https://segmentfault.com/a/1190000003028634
它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号
http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
https://segmentfault.com/a/1190000003028634