JS异步处理

515 阅读7分钟

一、前言

作为一个新人小白来说,JS的异步处理让我很头疼。。

那么什么是异步,为什么产生,又怎么处理呢?且看下文

单线程

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

单线程最大的好处就是实现简单,执行环境单纯

但是也有一个很致命的缺点:当一个线程需要执行很长时间,那么后续线程就需要一直等着,如果这个线程陷入了死循环,那么后续线程都将得不到执行的机会,页面卡死。

所以为了避免这个问题,JS制定了两种执行模式,同步模式和异步模式

异步与同步

同步模式 就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行的调入和完成顺序与任务的排列顺序是一致的、同步的;

异步模式 则完全不同,当一个任务执行时,若它执行时间很短,则它执行完后,后面任务紧跟着它执行,若它涉及异步操作,则将此任务调入主线程队列,异步操作给相应的异步操作理模块执行,与此同时调取下一个任务执行,等到异步模块处理完异步操作后,给主线程一个成功或失败信号,再将异步操作结果调入主线程队列排队,直到执行完。(具体流程参见js事件轮询机制 -_-||)

所以在我理解,异步的关键就是如何实现在异步完成后返回这个信号,来告诉主线程可以进行异步操作结果的处理。

二、异步实现方式

1. 回调函数

回调函数 就是将A函数作为B函数的参数传递给B,等B函数执行完成后再执行

比如我想实现B函数实现一系列复杂运算(或异步操作),实现完成后改变dom,而A函数需要在B函数更新完dom后,再对dom执行一些操作

用回调实现:

var domList = [];
function B(dom,callback){
    setTimeout(()=>{
        dom.push(1);
        callback();
    },2000);
}

function A(){
    console.log(domList);
}

B(domList,A)
 

这里用延迟执行来代替异步操作,上面结果在两秒后打印 [1],在B中的callback调用这里,就是手动模拟了异步操作完成的信号,告诉A可以进行后续操作了。 

注意:处理异步可以用回调,但是回调不一定就是异步

2. 事件监听

采取事件驱动模式,即定义一个事件,当这个事件发生时,执行一些处理。

在异步中的实现即 先定义事件发生时的处理函数并且监听这个事件,当进入异步且执行完后触发事件,进而触发监听这个事件相关联的处理函数。

监听的函数有:on、bind、listen、addEventaddEventListener、observe

还是上述功能,用事件监听实现:

var domList = [];
function A(){
    console.log(domList);
}
// 绑定监听 当B发生updateComplate事件时,执行A函数
B.on('updateComplate',A);
function B(){
    setTimeout(()=>{
        domList.push(1);
        B.trigger('updateComplate');
    },2000);
}

A();

这种方式容易理解,也可以为一个事件监听绑定多个事件处理函数

但是程序会变成事件驱动型的,运行流程不清晰

3. 发布/订阅

订阅发布模式又称为观察者模式,定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有的观察者对象。

发布者发出通知 =>主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作。

// 一个发布者 publisher,功能就是负责发布消息 - publish
        var pub = {
            publish: function () {
                dep.notify();
            }
        }
        // 多个订阅者 subscribers, 在发布者发布消息之后执行函数
        var sub1 = { 
            update: function () {
                console.log(1);
            }
        }
        var sub2 = { 
            update: function () {
                console.log(2);
            }
        }
        var sub3 = { 
            update: function () {
                console.log(3);
            }
        }
        // 一个主题对象
        function Dep() {
            this.subs = [sub1, sub2, sub3];
        }
        Dep.prototype.notify = function () {
            this.subs.forEach(function (sub) {
                sub.update();
            });
        }
        // 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法
        var dep = new Dep();
        pub.publish();

4. promise对象

promise对象用于处理异步操作

它代表一个未完成、但是预计将来会完成的操作

三种状态

  • pending:初始值,不是resolved,也不是rejected
  • resolved:操作成功
  • rejected:操作失败
状态转换
  • 由pending转换为resolved
  • 由pending转换为rejected
promise对象一旦创建,就立即执行,转换为成功或失败的状态后就不能再向别的状态转换

对上述功能的实现:

var domList = [];
function B(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            domList.push(1);
            resolve(domList);
        },2000);
    });
}

function A(){
    console.log(domList);
}
B().then((result)=>{
    A();
}).catch(err=>{
    console.log(err)
});

promise中,resolve和reject相当于异步操作完成的触发信号,then相当于resolve后的成功处理函数,catch相当于reject后的失败处理函数

promise的then可以链式调用

var domList = [];
function B(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            domList.push(1);
            resolve(domList);
        },2000);
    });
}

function A(){
    console.log(domList);
    domList.push(2);
}
B().then((result)=>{
    A();
}).then((result)=>{
    A();
}).catch(err=>{
    console.log(err)
});

5. async/await

初识async/await是在实际代码中,是怎么写的呢,async定义一个函数,在里面 使用await 可以让后面的代码等待此行执行完后再执行,我去,这我一理解,那我需要耗时的函数定义为async ,实际耗时操作加一个await,那不是分分钟变异步为同步了吗(-_-'')

于是年轻的我开始写测试了

async function test(){
    console.log(1);
    await setTimeout(()=>{
        console.log(2);
    },2000);
    console.log(3);
}
test();

test执行应该是 1 ,两秒后 2,3 了呗,结果呢。。。。 1,3,两秒后 2,控制台还多了个什么Promise

好吧,还是安安静静回去看教程吧!!

使用方法

  • async放于function定义之前,await写在async函数中,单独放在外层是不起作用的
  • async/await基于promise实现,被定义的异步函数执行后返回promise(这就能解释为啥多打印出来个promise对象了),若成功,使用.then()来获取结果,不成功呢,用.catch()获取失败结果
  • await等待的后续结果必须返回一个promise,一般的函数调用不管用(。。。好吧,基于promise实现的,证明人家认识promise,愿意等promise,不认识的还不乐意等了,这就是为啥我等延时执行不管用)
所以要想实现上述功能,得让await等的是个promise啊,

function asyncF(){
	return new Promise((resove,reject)=>{
		setTimeout(()=>{
			resove('2');
		},2000);
	})
}
async function callAsync(){
	console.log('1')
	await asyncF().then(result=>{
		console.log(result);
	});
	console.log('3');
}
		
callAsync();

这样就可以执行了

async/await基于promise实现的,那他比promise有哪些优势呢?(毕竟新整的东西还不如以前的还不如不整)

  • async/await更加语义化,async 是“异步”的简写,async function 用于申明一个 function 是异步的; await,可以认为是async wait的简写, 用于等待一个异步方法执行完成;
  • async/await是一个用同步思维解决异步问题的方案(等结果出来之后,代码才会继续往下执行)
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

function asyncF(x) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(2*x);
		}, 2000);
	})
}
async function callAsync() {
	let r1 = await asyncF(1);
	let r2 = await asyncF(r1);
	let r3 = await asyncF(r2);
	console.log(r3)
}
callAsync();

类似同步的异步使用,以及async函数对promise对象的自动转换操作简便