JS异步编程的理解

1.1什么叫异步?

异步async是相对于同步sync来说的,顾名思义

同步就是执行完一件事情后,再去执行下一件事情。而异步 ,比如以下例子

setTimeout(function cbFn() {
    console.log('这是一个异步任务!');
},1000);

console.log('这是一个同步任务');

setTimeout就是一个异步操作,当js的引擎顺序执行到setTimeout的时候发现这是一个异步任务,则会把这个任务 先挂起,继续执行后面的的代码(即同步任务),直到1000ms的时候,回调函数才会执行,这就是异步任务。

在执行异步任务的时候,js不会等setTimeout这个任务执行完了之后才执行’console.log(‘这是一个同步任务’)’,而是先执行后面的代码。

1.2那么问题来了,为什么js需要使用异步呢?

根本原因:js一种单线程的语言(你可能会问,为什么js要设计成单线程的呢?1.3告诉你答案)

由于js是一门单线程的语言,只能在js引擎的主线程上运行,所以js代码就会一条一条执行,js也就不能再同一时间执行多个js任务了。这就导致我们如果在发送AJAX请求或者I/O操作(比如加载一个100M的obj模型)的时候,如果没有异步这种机制,那么用户就会傻傻的等待很长时间,看不到任何响应结果,因为其他任务都没有被执行。

1.3 那那那为什么js不设计成跟java一样多线程的呢?

这主要跟js的历史有关,由于js的初衷是用来处理一些表单的验证和DOM操作而被创建出来的,所以主要为了追求语言的轻量和简单,就采用了单线程的模式。多线程可比单线程复杂多了啊!!!比如,多线程需要处理线程之间的资源共享问题,还要解决状态同步问题。

如果,js是多线程的话,一个线程中往DIV中append一个DIV,而另外一个线程则将这个父DIV删除了,这可咋整?为此,我们就需要为此新增加锁机制等。

好,那么现在我们知道了单线程的JS为了不出现长时间等待的状况,会使用异步来处理。比如执行一个AJAX请求,数据阻塞,10s才返回。我们就不需要傻傻的等待服务器给我们返回数据,而是继续执行下面的同步代码。等到服务器返回数据了,我们再去搞定它。

那么,问题又来了,那那那,常见的异步模式有哪些呢?我只听说过AJAX?

常见的异步模式

1.回调函数

2.事件监听

3.发布/订阅模式(又称观察者模式)

4.promise
后来ES6中,引入了Generator函数;ES8(也就是ES2017)中,async/await更是将异步编程带入了一个全新的阶段。

知道了这几种异步模式,别着急,我们先来了解以下js中是如何实现异步模式的

1.4 JS如何实现异步?

告诉你答案:js的事件循环机制(Event Loop)

具体来说:js被解析的时候,会被引擎分为两类:同步任务(synchronous)和异步任务(asynchronous)

对于同步任务:会被推到执行栈去执行这些任务

对于异步任务:当其可以被执行时,会被放到一个任务队列中等待js引擎去执行。

当执行栈中的所有同步任务完成后,js引擎才会去任务队列中查看是否有任务存在,并将任务放到执行栈中去执行,执行完了之后又到任务队列中查看是否已经有可执行的任务。说了这么多,别忘了我们在讲什么,这就叫做事件循环(Event Loop Event Loop Event Loop!!!)。

其实,对于任务队列,是有更细的分类的,其被分为 微任务(microtask)队列 & 宏任务(macrotask)队列

宏任务: setTimeout、setInterval等,会被放在宏任务(macrotask)队列。

微任务: Promise的then、Mutation Observer等,会被放在微任务(microtask)队列。

Event Loop的执行顺序是:

首先执行执行栈里的任务。
执行栈清空后,检查微任务(microtask)队列,将可执行的微任务全部执行。
取宏任务(macrotask)队列中的第一项执行。
回到第二步。
注意: 微任务队列每次全执行,宏任务队列每次只取一项执行。

举个例子好了

setTimeout(() => {
    console.log('我是第一个宏任务');
    Promise.resolve().then(() => {
        console.log('我是第一个宏任务里的第一个微任务');
    });
    Promise.resolve().then(() => {
        console.log('我是第一个宏任务里的第二个微任务');
    });
}, 0);

setTimeout(() => {
    console.log('我是第二个宏任务');
}, 0);

Promise.resolve().then(() => {
    console.log('我是第一个微任务');
});

console.log('执行同步任务');

// 执行同步任务
// 我是第一个微任务
// 我是第一个宏任务
// 我是第一个宏任务里的第一个微任务
// 我是第一个宏任务里的第二个微任务
// 我是第二个宏任务

1.5JS的异步编程

这里我们已经知道了JS中异步的运行机制,我们翻回头来详细的了解一下常见的各种异步的编程模式。

1.5.1回调函数
function asyncFn() {
    setTimeout(function HDHS() => {
        console.log('asyncFn');
    }, 0)
}

function normalFn() {
    console.log('normalFn');
}

asyncFn();
normalFn();

// normalFn
// asyncFn
// ***asyncFn()中的 HDHS()这函数就是异步操作中的回调函数,这就是异步编程的基本操作。
// ***思考:如果一定要先打印asyncFn呢?没错,将normalFn()的调用写在HDHS()中
1.5.2事件监听

另一种思路是采用事件驱动模式。这种思路是说异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。比如一个我们注册一个按钮的点击事件或者注册一个自定义事件,然后通过点击或者trigger的方式触发这个事件。(个人觉得有点牵强,暂时有点难以理解)

1.5.3发布/订阅模式(又称观察者模式)来来来,划重点了!!

发布订阅模式,可以被理解成是事件监听的升级版

在发布/订阅模式中,你可以想象成,一个消息中心,你可以在那里’注册一条信息’,那么注册过的信息就可以被感兴趣的订阅者们订阅,一旦未来这条“消息被发布”,则所有订阅了这条消息的人都会收到这条通知。

这个就是发布/订阅者模式的设计思路,接下来我们一点一点实现一个简单的发布/订阅模式

首先,实现一个消息中心,用什么实现呢? 当然是通过原生的javascript啦!

// 先实现一个消息中心的构造函数,用来创建一个消息中心
function MessageCenter(){
    var _messages = {}; // 所有注册的消息都存在这里

    this.regist = function(){}; // 用来注册消息的方法
    this.subscribe = function(){};  // 用来订阅消息的方法
    this.fire = function(){};   // 用来发布消息的方法
}

大概设计思路就是这样,我们来简单实现一下

function MessageCenter() {
    var _message = {};
    
    // 注册 (只需要告诉消息中心要注册什么样类型的信息)
    this.regist = function(msgType) {
        iftypeof _message[msgType] === 'undifined'{
        	_message[msgType] = []; // 这里说明 msgType这个msg已经注册成功了,并且维护一个数组来保存订阅者函数subFn(理解成要干的事情)!
        } else {
            console.log('你这个msgType:' + msgType + '已经被注册了!');
        }
    }
    
    // 订阅 (要告诉消息中心,要订阅什么样的信息,并且一旦消息发布,要干什么)
    this.subscribe = function(msgType,subFn) {
        // 同理,首先判断有没有这个消息可以订阅
        if(typeof _messages[msgType] !== 'undefined'){
            _messages[msgType].push(subFn); // 要干的事情放在 _message[msgType]这个数组中
        }else{
            console.log('这个消息还没注册过,无法订阅')
        }
    }
    
    //发布 (要发布某条消息,并通知所有订阅了的订阅者函数)
    this.fire = function(msgType,args) {
        // 同理,判断下有没有这个消息可以发布
        if(typeof _messages[msgType] === 'undefined') {
            console.log('没有这条消息,无法发布');
            return false;
        }
        
        // subFn在执行的时候,可能需要一些额外的参数,通过args传入
        var events = {
            type: msgType,
            args: args || {}
        }
        
        _message[msgType].map( subFn => {
            subFn(events)
        })
    }
}

OK! 那么现在一个简单的发布/订阅模式就完成了,我们现在就可以来做一些简单的异步操作了

// 首先 声明一个消息中心
var msgCenter = new MessageCenter();

// 注册
msgCenter.regist('diff');
// 订阅 订阅函数为subscribeFn()
msgCenter.subscribe('diff',subscribeFn);

function subscribeFn(events) {
    console.log(events.type,events.args);
}

// 注意了!我要发布了!
setTimeout(function () {
   msgCenter.fire('diff','fire msg') 
},1000)


// **一秒钟后,打印出 diff ,fire msg。为什么说这是一种异步的思想?可以这么理解,subscribeFn随时可能被调用,他缺少一个事件的驱动。就像放在任务队列中的异步任务一样,执行完了,等待被轮询。
1.5.4 Promise(面试必考!)

为什么Promise这么重要?Promise是ES6推出的一种异步编程的解决方案。Promise只要解决了回调等解决方案嵌套的问题并且代码更加易读,有种在写同步方法的既视感??

// 新创建一个Promise对象(先对promise有一个初步的印象)
var myFirstPromise = new Promise( function(resolve, reject) {
    setTimeout(function () {
        if (true) {
        	var data = 'myFirstPromise suceess!'
            resolve(data);
        } else {
            reject();
        }
    },1000)
})

console.log('a');
myFirstPromise .then(function (data) {
    console.log('success');
    console.log(data);
}).catch(() => {
    console.log('失败');
}).finally(() => {
    console.log('完成');
console.log('b');    
// a
// b
// success data数据

说一下为什么需要异步编程:比如我们请求了一个数据需要10s,然后var data = response.data又是依赖response这个数据的,那么var data = response.data必须在数据返回后才能执行。

asyncFn1(function () {
  asyncFn2(function() {
    asyncFn3(function() {
        // xxxxx
    });
  });
});

然而,使用了promise,只需要这样写,我们对一下,没有对比就没有伤害!

function asyncFn1() {
    console.log('asyncFn1 run');
    return new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve('asyncFn1执行完了');
        }, 2000)
    })
}

function asyncFn2() {
    console.log('asyncFn2 run');
    return new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve('asyncFn2执行完了');
        }, 1000)
    })
}

function normalFn3() {
    console.log('normalFn3 run');
}

// 这样就比较好理解了,当asyncFn1re的resolve()执行的时候,才会执行.then()里面的函数,同理
asyncFn1().then(asyncFn2).then(normalFn3);
1.5.5 Genertator函数(跟python有点像呀)
// js中给function特地加了个* 感觉Genertator函数意义非凡呀
function* oneGenerator() {
    yield 'learn';
    yield 'In';
    return 'pro';
}

// g相当于就是一个迭代器,可以遍历noeGneraotr对象中内容,
var g = oneGenerator();

g.next();   // {value: "Learn", done: false}
g.next();   // {value: "In", done: false}
g.next();   // {value: "Pro", done: true}
// 接着我们就来用Genertator来实现一下之前的操作把!
function* oneGenerator() {
    yield asyncFn1();
    yield asyncFn2();
    yield normalFn3();
}
1.5.6Async/Await 终极版

Async函数相当于Generator函数+执行器

function asyncFn1(){
    console.log('asyncFn1');
    return new Promise(function(resolve){
        setTimeout(function(){
            resolve('123');
        }, 2000)
    })
}

function asyncFn2() {
    console.log('asyncFn2');
    return new Promise(function(resolve){
        setTimeout(function(){
            resolve('456');
        }, 2000)
    })
}

async function asyncFn () {
    var a = await asyncFn1();
    var b = await asyncFn2();

    console.log(a,b)
}

asyncFn();

// asyncFn1
// asyncFn2
// 123,456
  • 首先async函数返回的是一个Promise对象,不像是Generator返回的是一个迭代器,所以async函数执行后可以继续使用then等方法继续进行下面的逻辑
  • await后面一般跟Promise对象,async函数执行时,遇到await后,等待后面的Promise对象的状态从pending变成resolve的后,将resolve的参数返回并自动往下执行直到下一个await或者结束
  • await后面也可以跟一个async函数进行嵌套使用。

转 : https://www.cnblogs.com/learninpro/p/9271813.html

发布了6 篇原创文章 · 获赞 4 · 访问量 222

猜你喜欢

转载自blog.csdn.net/qq_41022872/article/details/102734769
今日推荐