谈谈JS中异步操作
一、基本认识
单线程:单线程即是主线程,所有的程序只运行在一条线程上。好比工厂里只有一条流水线A,所有的操作都在这条流水线A上完成,遇到复杂的任务时,就容易堵塞后面的工作。JS就属于这种单线程语言(为了保证JS对DOM操作的一致性)。也由于这种方式遇到耗时操作时,就容易堵塞待执行程序的运行(堵塞性)。示例代码如下:
var n=0;
for(var i=0;i<1000000000;i++){
n++;
}
console.log(n);
console.log("堵塞~~~");
————执行结果————
1000000000
堵塞~~~
以上代码延时了一会才会执行,原因我就不重复了。该怎么优化呢???
异步任务:用来解决耗时操作,防止程序发生堵塞。好比在流水线A上开另一条流水线B(异步任务队列)。如果遇到耗时操作的,就将任务放置在流水线B上,这样就不会堵塞流水线A的工作了。异步方式也是提高单线程语言执行效率的主要方式之一。
示例代码如下:
async function compute(){
let res=await new Promise((resolve,reject)=>{
var n=0;
for(var i=0;i<1000000000;i++){
n++;
}
resolve(n);
});
console.log(res);
}
compute();
console.log("堵塞~~~");
————执行结果————
堵塞~~~
1000000000
总结:以上代码堵塞一段时间后分别打印出“堵塞~~~”和“1000000000”。为什么先堵塞后换位置呢?依据以上代码可知,Promise对象内执行的是普通的同步代码,所以遇到for操作先堵塞,for完成后调用resolve输出结果。由于resolve()是异步执行的。所以先打印“堵塞~~~”,后打印“1000000000”;
Promise规范可参考:http://www.ituring.com.cn/article/66566
二、深入总结
1. JS异步运行原理
最原始最暴力的方式,也是Promise出现之前,回调函数处理异步任务的必备技;
JS异步运行原理:由于异步任务具有时间不确定性(很有可能耗时,堵塞线程),所以它不能在主线程上执行的,以防止主线程被阻塞。故异步任务只能在异步任务队列(队列先进先出FIFO原则)中执行。执行顺序如下:
- 在主线程执行完同步任务;
- 在执行期间遇到异步任务时,将异步任务扔在异步队列上执行,以防止堵塞主程序执行;
- 当主线程同步任务执行完毕时,就Event Loop循环异步队列上的异步程序执行的结果;
JS异步结构分析图如下:
2. 了解事件循环(Event Loop)
通过上述所介绍可知,单线程语言是通过异步机制提高执行效率,当任务队列上的异步任务执行结束后,会通过事件循环将结果推入到主线程中(前提是主线程上的同步任务执行完毕);为什么叫“事件”循环?JS是事件机制执行异步任务,示例如下:
/*
**1. setTimeout():声明该方法发起异步任务
**2. 1000ms:1s后“时间到了”事件触发,表示异步任务完成,需将回调函数包含的执行结果推入到消息队列中
**3. 事件循环遍历消息队列结果并推入到主线程中
*/
setTimeout(()=>{
console.log("异步任务执行了");
},1000);
/*
**1. addEventListener():发起异步任务
**2. click:点击dom对象后,表示异步任务完成,并将回调函数包含的执行结果推入到消息队列中
**3. 事件循环遍历消息队列结果并推入到主线程中
*/
dom.addEventListener("click",function(){
//something~~~
});
以上可知事件循环本质上就是无限循环,不过它需要等待执行栈(主线程)中的同步任务执行完毕才能循环;模拟效果代码如下:
while(main.isTask()){ //判断执行栈(主线程)中同步任务是否执行完毕
//开始循环消息队列
queue.get();
}
3. 异步解决方案发展史
对于异步的解决方案,大致分为以下3种方式:
- 回调函数:这个最最熟悉不过的解决方案。异步有多深,嵌套就有多深。简称回调地狱。这里不多做介绍;
- Promise对象:用于对异步任务最终结果(完成或失败)的表示。异步任务成功,调用resolve方法将状态变成Fulfilled(已完成)。任务失败,调用reject方法将状态变成Rejected(已失败)。它解决之前异步任务中通过回调函数来执行相应结果,通过使用链式调用的方式,帮助开发者脱离回调地狱;
//使用Promise模拟ajax请求
var httpAjax=function(url,method,data){
if(typeof(url) == "string"){
/*
**resolve:等待中——>完成状态转换
**reject:等待中——>失败状态转换
*/
return new Promise((resolve,reject)=>{
/*
**模拟HTTP请求异步任务
**模拟请求结果:{flag:"success/error",data:"返回结果",msg:"返回信息"}
*/
setTimeout(function(res){
if(res['flag'] == "success"){
resolve(res['data']);
}else{
reject(res['msg']);
}
});
});
}
}
总结:通过以上代码可知,Promise对象有3中状态。分别是等待中、已完成、已失败。而且这3种状态是单向不可逆;也就是要不完成、要不失败。同时resolve和reject方法中必须异步执行,也就是Ajax请求是异步执行,resolve或reject也是异步执行的。接下来更加详细解释;
3. async/await语法糖:async/await其实就是ES6生成器(Generator)函数和自执行(co)函数的结合体;它让异步代码能够像同步代码一样至上而下执行;
async function test(){
const res=await new Promise((resolve,reject)=>{
setTimeout(()=>{
const res="运行结果";
console.log(res);
resolve(res);
},3000)
});
console.log("123123");
return res;
}
test().then((res)=>{console.log(res);});
console.log("我先执行了~~~");
我先执行了~~~
——————延时3s后执行——————
运行结果
123123
运行结果
async关键字声明test()方法为异步方法。运行test()方法返回是Promise对象。所以可以test().then()这种写法;await关键字必须在async函数里面;await关键字后接Promise对象。注意这不是必须的,但是如果不接Promise对象,就跟普通方法调用执行方式差不多,同时async返回的也不是个Promise对象;示例代码如下:
async function test(){
const res=await (()=>{
setTimeout(()=>{
const res="运行结果";
console.log(res);
},3000)
})();
console.log("123123");
return res;
}
test();
console.log("我先执行了~~~");
我先执行了~~~
123123
——————延时3s后执行——————
运行结果
总结:本以为自己对异步掌握的比较熟练,可总结起来乱的一逼。还是基础不够深。上面根据自己理解和参考网上资料总结而来。若有误请及时指出3Q;