众所周知,JS语言是单线程的。在实际开发过程当中都会面临一个问题,就是同步操做会阻塞整个页面乃至整个浏览器的运行,只有在同步操做完成以后才能继续进行其余处理,这种同步等待的用户体验极差。因此JS中引入了异步编程,主要特色就是不阻塞主线程的继续执行,用户直观感觉就是页面不会卡住。css
首先能够肯定一点是浏览器是多进程的,好比打开多个窗口可能就对应着多个进程,这样能够确保的是页面之间相互没有影响,一个页面卡死也并不会影响其余的页面。一样对于浏览器进程来讲,是多线程的,好比咱们前端开发人员最须要了解的浏览器内核也就是浏览器的渲染进程,主要负责页面渲染,脚本执行,事件处理等任务。为了更好的引入JS单线程的概念,咱们将浏览器内核中经常使用的几个线程简单介绍一下:html
GUI渲染线程 负责渲染浏览器页面,解析html+css,构建DOM树,进行页面的布局和绘制操做,同事页面须要重绘或者印发回流时,都是该线程负责执行。前端
JS引擎线程 JS引擎,负责解析和运行JS脚本,一个页面中永远都只有一个JS线程来负责运行JS程序,这就是咱们常说的JS单线程。node
注意:JS引擎线程和GUI渲染线程永远都是互斥的,因此当咱们的JS脚本运行时间过长时,或者有同步请求一直没返回时,页面的渲染操做就会阻塞,就是咱们常说的卡死了web
事件触发线程 接受浏览器里面的操做事件响应。如在监听到鼠标、键盘等事件的时候, 若是有事件句柄函数,就将对应的任务压入队列。ajax
定时触发器线程 浏览器模型定时计数器并非由JavaScript引擎计数的, 由于JavaScript引擎是单线程的, 若是处于阻塞线程状态就会影响记计时的准确, 它必须依赖外部来计时并触发定时。编程
异步http请求线程 在XMLHttpRequest在链接后是经过浏览器新开一个线程请求将检测到状态变动时,若是设置有回调函数,异步线程就产生状态变动事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。api
由于只有JS引擎线程负责处理JS脚本程序,因此说JS是单线程的。能够理解的是js当初设计成单线程语言的缘由是由于js须要操做dom,若是多线程执行的话会引入不少复杂的状况,好比一个线程删除dom,一个线程添加dom,浏览器就无法处理了。虽然如今js支持webworker多线线程了,可是新增的线程彻底在主线程的控制下,为的是处理大量耗时计算用的,不能处理DOM,因此js本质上来讲仍是单线程的。promise
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。浏览器
任务队列就是用来存放一个个带执行的异步操做的队列,在ES6中又将任务队列分为宏观任务队列和微观任务队列。
宏任务队列(macrotask queue)等同于咱们常说的任务队列,macrotask是由宿主环境分发的异步任务,事件轮询的时候老是一个一个任务队列去查看执行的,"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。
微任务队列(microtask queue)是由js引擎分发的任务,老是添加到当前任务队列末尾执行。另外在处理microtask期间,若是有新添加的microtasks,也会被添加到队列的末尾并执行
异步时间添加到任务队列中后,如何控制他们的具体执行时间呢?JS引擎一旦执行栈中的全部同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
ES5的JS事件循环参考图:
ES6的JS事件循环参考图:
理解了JS程序执行的基本原理,下面就能够步入正题,讨论一下咱们在实际开发中,如何编写异步程序才能让本身的代码易读易懂bug少。
在JavaScript中,回调函数具体的定义为:函数A做为参数(函数引用)传递到另外一个函数B中,而且这个函数B执行函数A。咱们就说函数A叫作回调函数。若是没有名称(函数表达式),就叫作匿名回调函数。
所以callback 不必定用于异步,通常同步(阻塞)的场景下也常常用到回调,好比要求执行某些操做后执行回调函数。
回调函数被普遍应用到JS的异步开发当中,下面分别列举几条开发中经常使用回调函数的状况,如:
setTimeout(function(){
//该方法为回调方法
//code
}, 1000)
setInterval(()=>{
//该方法为匿名回调方法
//code
}, 1000)
复制代码
//node读取文件
fs.readFile(xxx, 'utf-8', function(err, data) {
//该方法为读取文件成功后出发的回调方法
//code
});
复制代码
$.ajax({
type: "post",
url: "xxx",
success: function(data){
//post请求成功回调方法
//code
},
error: fucntion(e){
//post请求错误回调方法
//code
}
})
复制代码
用回调函数的方法来进行异步开发好处就是简单明了,容易理解
回调函数的缺点, 用一个小的实例来讲明一下:
method1(function(err, data) {
//code1
method2(function(err, data) {
//code2
method3(function(err, data) {
//code3
method4(D, 'utf-8', function(err, data) {
//code4
});
});
});
});
复制代码
若是说异步方法以前有明确的前后顺序来执行,稍微复杂的操做很容易写出上面示例的代码结构,若是加上业务代码,程序就显得异常复杂,代码难以理解和调试,这种就是咱们常说的回调地狱。
若是想要实现更加复杂的功能,回调函数的局限性也会凸显出来,好比同时执行两个异步请求,当两个操做都结束时在执行某个操做,或者同时进行两个请求,取优先完成的结果来执行操做,这种都须要在各自的回调方法中监控状态来完成。
随着ES6/ES7新标准的普及,咱们应该寻求新的异步解决方案来替代这种传统的回调方式。
ES6新增Promise对象的支持,Promise提供统一的接口来获取异步操做的状态信息,添加不能的处理方法。
Promise对象只有三种状态:
Promise的状态只能由内部改变,而且只能够改变一次。
下面看看用Promise来实现多级回调能不能解决回调地狱的问题
function read(filename){
return new Promise((resolve, reject) => {
//异步操做code, Example:
fs.readFile(filename, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
})
}
read(filename1).then(data=>{
return read(filename2)
}).then(data => {
return read(filename3)
}).then(data => {
return read(filename4)
}).catch(error=>{
console.log(error);
})
复制代码
经过实践代码 咱们发现用Promise能够像写同步代码同样实现异步功能,避免了层层嵌套的问题。
如何用Promise来实现同时发起多个异步操做的需求
function loadData(url){
return new Promise((resolve, reject)=>{
$.ajax({
type: "post",
url: url,
success: function(data){
//post请求成功回调方法
resolve(data)
},
error: fucntion(e){
//post请求错误回调方法
reject(e)
}
})
})
}
Promise.all([loadData(url1), loadData(url2), loadData(url3)])
.then(data => {
console.log(data)
}).catch(error => {
console.log(error);
})
复制代码
function loadData(url){
return new Promise((resolve, reject)=>{
$.ajax({
type: "post",
url: url,
success: function(data){
//post请求成功回调方法
resolve(data)
},
error: fucntion(e){
//post请求错误回调方法
reject(e)
}
})
})
}
Promise.race([loadData(url1), loadData(url2), loadData(url3)])
.then(data => {
console.log(data)
}).catch(error => {
console.log(error);
})
复制代码
用Promise来写异步能够避免回调地狱,也能够轻松的来实现callback须要引入控制代码才能实现的多个异步请求动做的需求。
固然Promise也有本身的缺点:
带着这些缺点,继续往下学习别的异步编程方案。
**关于Promise的详细文章能够阅读这篇你真的会用 Promise 吗
ES6新增Generator异步解决方案,语法行为与传统方法彻底不同。
Generator函数是一个状态机,封装了多个内部状态,也是一个遍历器对象生成函数,生成的遍历器对象能够一次遍历内部的每个状态。
Generator用function*来声明,除了正常的return返回数据以外,还能够用yeild来返回屡次。
调用一个Generator对象生成一个generator对象,可是还并无去执行他,执行generator对象有两种方法:
Generator的用处不少,本文只讨论利用它暂停函数执行,返回任意表达式的值的这个特性来使异步代码同步化表达。从死路上来说咱们想达到这样的效果:
function loadData(url, data){
//异步请求获取数据
return new Promise((resolve, reject)=>{
$.ajax({
type: "post",
url: url,
success: function(data){
//post请求成功回调方法
resolve(data)
},
error: fucntion(e){
//post请求错误回调方法
reject(e)
}
})
})
}
function* gen() {
yeild loadData(url1, data1);
yeild loadData(url2, data2);
yeild loadData(url3, data3);
}
for(let data of gen()){
//分别输出每次加载数据的返回值
console.log(data)
}
复制代码
但仅仅是这样来实现是不行的,由于异步函数没有返回值,必须经过从新包装的方式来传递参数值。co.js就是一个这种generator的执行库。使用它是咱们只须要将咱们的 gen 传递给它像这样 co(gen) 是的就这样。
function* gen() {
let data1 = yeild loadData(url1, data1);
console.log(data1);
let data2 = yeild loadData(url2, data2);
console.log(data2);
let data3 = yeild loadData(url3, data3);
console.log(data3);
}
co(gen())
.then(data => {
//gen执行完成
}).catch(err => {
//code
})
复制代码
由于ES7中新增了对async/await的支持,因此异步开发有了更好的选择,基本上能够放弃用原生generator来写异步开发,因此咱们只是有个简单的概念,下面咱们着重介绍一下异步编程的最终方案 async/await。
asycn/await方案能够说是目前解决JS异步编程的最终方案了,async/await是generator/co的语法糖,同时也须要结合Promise来使用。该方案的主要特色以下:
还用用代码来讲明问题,用async/await方案来实现最初的需求
//普通函数
function loadData(url){
//异步请求获取数据
return new Promise((resolve, reject)=>{
$.ajax({
type: "post",
url: url,
success: function(data){
//post请求成功回调方法
resolve(data)
},
error: fucntion(e){
//post请求错误回调方法
reject(e)
}
})
})
}
//async函数
async function asyncFun(){
//普通函数的调用
let data1 = await loadData(url1);
let data2 = await loadData(url2);
let data3 = await loadData(url3)
}
asyncFun()
.then(data => {
//async函数执行完成后操做
})
.catch(err => {
//异常抓取
});
复制代码
loadData()函数虽然返回的是Promise,可是await返回的是普通函数resole(data)时传递的data值。
经过和generator方式来的实现对比来看,更加理解了async/await是generator/co方法的语法糖,从函数结构上来讲彻底同样。可是省略了一些外库的引入,一些通用方法的封装,使异步开发的逻辑更加清晰,更加接近同步开发。
处理完有前后顺序的请求处理,下面来个多个请求同时发起的例子
//普通函数
function loadData(url){
//异步请求获取数据
return new Promise((resolve, reject)=>{
$.ajax({
type: "post",
url: url,
success: function(data){
//post请求成功回调方法
resolve(data)
},
error: fucntion(e){
//post请求错误回调方法
reject(e)
}
})
})
}
//async函数
async function asyncFun(){
await Promise.all([loadData('url1'), loadData('url2')]).then(data => {
console.log(data); //['data1', 'data2']
})
}
asyncFun();
//配合Promise的race方法一样能够实现任意请求完成或异常后执行操做的需求
//async函数
async function asyncFun(){
await Promise.race([loadData('url1'), loadData('url2')]).then(data => {
console.log(data);
})
}
复制代码
经过上面四种不一样的异步实现方式的对比能够发现,async/await模式最接近于同步开发,即没有连续回调,也没有连续调用then函数的状况,也没有引入第三方库函数,因此就目前来讲async/await+promise的方案为最佳实践方案。
社区以及公众号发布的文章,100%保证是咱们的原创文章,若是有错误,欢迎你们指正。
文章首发在WebJ2EE公众号上,欢迎你们关注一波,让咱们你们一块儿学前端~~~
再来一波号外,咱们成立WebJ2EE公众号前端吹水群,你们不论是看文章仍是在工做中前端方面有任何问题,咱们均可以在群内互相探讨,但愿可以用咱们的经验帮更多的小伙伴解决工做和学习上的困惑,欢迎加入。