前段时间面试,考察比较多的是js异步编程方面的相关知识点,现在,正好轮到本身分享技术,因此想把js异步编程学习下,作个总结。
下面这个demo 归纳了大多数面试过程当中遇到的问题:javascript
for(var i = 0; i < 3; i++) { setTimeout(function() { console.log('timeout' + i); }) } new Promise(function(resolve) { console.log('promise1'); for(var i = 0; i < 1000; i++) { i == 99 && resolve(); } console.log('promise2'); }).then(function() { console.log('then1'); }) console.log('global1');
经过验证能够得知这个demo的结果为:html
但是为何会是这样的结果,咱们可能须要先了解下下面两个知识点html5
浏览器的内核是多线程的,他们在内核的控制下互相配合以保持同步,一个浏览器至少实现三个常驻的线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。java
1)js引擎,基于事件驱动单线程执行的,js引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JS线程在运行JS程序。
2)GUI线程,当界面须要重绘或因为某种操做引起回流时,该线程就会执行。它和JS引擎是互斥的。
3)浏览器事件触发线程,当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待js引擎的处理,这些事件可来自JavaScript引擎当前执行的代码块如,setTimeOut, 也能够来自浏览器内核的其余线程如鼠标点击,AJAX异步请求等,但因为JS的单线程关系,全部这些事件都得排队等待JS引擎处理。node
1)任务队列又分为macro-task(宏任务)与micro-task(微任务),
在最新标准中,它们被分别称为task与jobs。git
2)macro-task大概包括:script(总体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。github
3)micro-task【先执行】大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)面试
setTimeout/Promise等咱们称之为任务源。而进入任务队列的是他们指定的具体执行任务。ajax
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(总体代码)开始第一次循环。以后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),而后执行全部的micro-task。当全部可执行的micro-task执行完毕以后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,而后再执行全部的macro-task,这样一直循环下去。编程
经过这个事件循环的顺序,咱们就知道,为何上面提到的面试题为何是这样的输出结果了。
接下来咱们看下三类异步编程的实现。
demo1:
// 一个简单的封装 function want() { console.log('这是你想要执行的代码'); } function fn(want) { console.log('这里表示执行了一大堆各类代码'); // 其余代码执行完毕,最后执行回调函数 want && want(); } fn(want);
demo2:
//callback hell doSomethingAsync1(function(){ doSomethingAsync2(function(){ doSomethingAsync3(function(){ doSomethingAsync4(function(){ doSomethingAsync5(function(){ // code... }); }); }); }); });
能够发现一个问题,在回调函数嵌套层数不深的状况下,代码还算容易理解和维护,一旦嵌套层数加深,就会出现“回调金字塔”的问题,就像demo2那样,若是这里面的每一个回调函数中又包含了不少业务逻辑的话,整个代码块就会变得很是复杂。从逻辑正确性的角度来讲,上面这几种回调函数的写法没有任何问题,可是随着业务逻辑的增长和趋于复杂,这种写法的缺点立刻就会暴露出来,想要维护它们实在是太痛苦了,这就是“回调地狱(callback hell)”。
回调函数还有一个问题就是咱们在回调函数以外没法捕获到回调函数中的异常,通常咱们用try catch来捕捉异常,咱们尝试下捕捉回调中的异常
能够看到,不能捕捉到callback中的异常。
事件监听是一种很是常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦颇有用处。一般状况下,咱们须要考虑哪些部分是不变的,哪些是容易变化的,把不变的部分封装在组件内部,供外部调用,须要自定义的部分暴露在外部处理。从某种意义上说,事件的设计就是组件的接口设计。
1)jQuery事件监听
$('#btn').on('myEvent', function(e) { console.log('There is my Event'); }); $('#btn').trigger('myEvent');
2)发布/订阅模式
var PubSub = function(){ this.handlers = {}; }; PubSub.prototype.subscribe = function(eventType, handler) { if (!(eventType in this.handlers)) { this.handlers[eventType] = []; } this.handlers[eventType].push(handler); //添加事件监听器 return this;//返回上下文环境以实现链式调用 }; PubSub.prototype.publish = function(eventType) { var _args = Array.prototype.slice.call(arguments, 1); for (var i = 0, _handlers = this.handlers[eventType]; i < _handlers.length; i++) { _handlers[i].apply(this, _args);//遍历事件监听器 } return this; }; var event = new PubSub;//构造PubSub实例 event.subscribe('list', function(msg) { console.log(msg); }); event.publish('list', {data: ['one,', 'two']}); //Object {data: Array[2]}
这种模式实现的异步编程,本质上仍是经过回调函数实现的,因此3.1中提到的回调嵌套和没法捕捉异常的问题仍是存在的,接下来咱们看ES6提供的Promise对象,是否解决这两个问题。
ES 6中原生提供了Promise对象,Promise对象表明了某个将来才会知道结果的事件(通常是一个异步操做),而且这个事件对外提供了统一的API,可供进一步处理。
使用Promise对象能够用同步操做的流程写法来表达异步操做,避免了层层嵌套的异步回调,代码也更加清晰易懂,方便维护,也能够捕捉异常。
一个简单例子:
function fn(num) { return new Promise(function(resolve, reject) { if (typeof num == 'number') { resolve(); } else { reject(); } }) .then(function() { console.log('参数是一个number值'); }) .then(null, function() { console.log('参数不是一个number值'); }) } fn('haha'); fn(1234);
为何Promise 能够这样实现异步编程,在这咱们简单分析下Promise实现过程:
1)极简Promise雏形
// 极简promise雏形 function Promise(fn) { var value = null, callbacks = []; //callbacks为数组,由于可能同时有不少个回调 this.then = function (onFulfilled) { callbacks.push(onFulfilled); }; function resolve(value) { callbacks.forEach(function (callback) { callback(value); }); } fn(resolve); }
2)加入延时处理
// 极简promise雏形,加入延时处理 function Promise(fn) { var value = null, callbacks = []; //callbacks为数组,由于可能同时有不少个回调 this.then = function (onFulfilled) { callbacks.push(onFulfilled); }; function resolve(value) { setTimeout(function() { callbacks.forEach(function (callback) { callback(value); }); }, 0) } fn(resolve); }
3)加入状态判断
// 极简promise雏形,加状态判断 function Promise(fn) { var state = 'pending', value = null, callbacks = []; this.then = function (onFulfilled) { if (state === 'pending') { callbacks.push(onFulfilled); return this; } onFulfilled(value); return this; }; function resolve(newValue) { value = newValue; state = 'fulfilled'; setTimeout(function () { callbacks.forEach(function (callback) { callback(value); }); }, 0); } fn(resolve); }
4)链式promise
// 极简promise雏形,链式promise function Promise(fn) { var state = 'pending', value = null, callbacks = []; this.then = function (onFulfilled) { return new Promise(function (resolve) { handle({ onFulfilled: onFulfilled || null, resolve: resolve }); }); }; function handle(callback) { if (state === 'pending') { callbacks.push(callback); return; } //若是then中没有传递任何东西 if(!callback.onResolved) { callback.resolve(value); return; } var ret = callback.onFulfilled(value); callback.resolve(ret); } function resolve(newValue) { if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { var then = newValue.then; if (typeof then === 'function') { then.call(newValue, resolve); return; } } state = 'fulfilled'; value = newValue; setTimeout(function () { callbacks.forEach(function (callback) { handle(callback); }); }, 0); } fn(resolve); }
利用Promise的知识,对ajax进行一个简单的封装。看看会是什么样子:
//demo3 promise封装ajax var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10'; function getJSON(url) { return new Promise(function(resolve, reject) { var XHR = new XMLHttpRequest(); XHR.open('GET', url, true); XHR.send(); XHR.onreadystatechange = function() { if (XHR.readyState == 4) { if (XHR.status == 200) { try { var response = JSON.parse(XHR.responseText); resolve(response); } catch (e) { reject(e); } } else { reject(new Error(XHR.statusText)); } } } }) } getJSON(url).then(resp => console.log(resp));
除了串行执行若干异步任务外,Promise还能够并行执行异步任务。
当有一个ajax请求,它的参数须要另外2个甚至更多请求都有返回结果以后才能肯定,那么这个时候,就须要用到Promise.all来帮助咱们应对这个场景。
Promise.all接收一个Promise对象组成的数组做为参数,当这个数组全部的Promise对象状态都变成resolved或者rejected的时候,它才会去调用then方法。
// demo4 promise.all var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10'; var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10'; function renderAll() { return Promise.all([getJSON(url), getJSON(url1)]); } renderAll().then(function(value) { console.log(value); //将获得一个数组,里面是两个接口返回的值 })
结果:
有些时候,多个异步任务是为了容错。好比,同时向两个URL读取用户的我的信息,只须要得到先返回的结果便可。这种状况下,用Promise.race()实现。
与Promise.all类似的是,Promise.race都是以一个Promise对象组成的数组做为参数,不一样的是,只要当数组中的其中一个Promsie状态变成resolved或者rejected时,就能够调用.then方法了
// demo5 promise.race function renderRace() { return Promise.race([getJSON(url), getJSON(url1)]); } renderRace().then(function(value) { console.log(value); })
这里then()传的value值将是接口返回比较快的接口数据,另一个接口仍在继续执行,但执行结果将被丢弃。
结果:
Generator函数是协程在ES 6中的实现,最大特色就是能够交出函数的执行权(暂停执行)。
注意:在node中须要开启--harmony选项来启用Generator函数。
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操做须要暂停的地方,都用yield语句注明。
看个简单的例子:
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); var r1 = g.next(); // { value: 3, done: false } console.log(r1); var r2 = g.next() // { value: undefined, done: true } console.log(r2);
须要注意的是Generator函数的函数名前面有一个"*"。
上述代码中,调用Generator函数,会返回一个内部指针(即遍历器)g,这是Generator函数和通常函数不一样的地方,调用它不会返回结果,而是一个指针对象。调用指针g的next方法,会移动内部指针,指向第一个遇到的yield语句,上例就是执行到x+2为止。
换言之,next方法的做用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示Generator函数是否执行完毕,便是否还有下一个阶段。
对Generator函数,只有一个感性认知,没有实践过,因此就先介绍到这了,后面还有ES7新的知识点async await,看了下网上的资料,理解得还不够,但愿后面本身接触得更多再来这里补上,未完待续...
参考资料:
1) http://www.jianshu.com/p/12b9f73c5a4f
2) http://www.jianshu.com/p/fe5f173276bd
3) https://mengera88.github.io/2017/05/18/Promise%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/
4) http://www.cnblogs.com/nullcc/p/5841182.html