谈谈JS中异步操作

谈谈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原则)中执行。执行顺序如下:

  1. 在主线程执行完同步任务;
  2. 在执行期间遇到异步任务时,将异步任务扔在异步队列上执行,以防止堵塞主程序执行;
  3. 当主线程同步任务执行完毕时,就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种方式:

  1. 回调函数:这个最最熟悉不过的解决方案。异步有多深,嵌套就有多深。简称回调地狱。这里不多做介绍;
  2. 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;

猜你喜欢

转载自blog.csdn.net/u012475786/article/details/91415562