ajax 同步_JS同步、异步、阻塞、非阻塞、事件循环、消息队列、宏任务与微任务

一、单线程

  • 主线程:JavaScript是单线程的,所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个,叫它主线程;
  • 工作线程:实际上浏览器还存在其他的线程,例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等,这些线程可能存在于JS引擎之内,也可能存在于JS引擎之外,在此我们不作区分,统一叫它们工作线程;

总结:

  1. JavaScript引擎是单线程运行的,浏览器无论在什么时候都有且只有一个线程在运行JavaScript程序;
  2. JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化;

二、同步和异步、阻塞和非阻塞

  • 区别:在于程序中的各个任务是否按顺序执行,异步操作可以改变程序的正常执行顺序;
console.log("1");
setTimeout(function() {
    console.log("2")
}, 0);
setTimeout(function() {
    console.log("3")
}, 0);
setTimeout(function() {
    console.log("4")
}, 0);
console.log("5");
// 1
// 5
// 2
// 3
// 4

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行(即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒)

setInterval(fn,ms)每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

什么是异步任务?上述代码中,尽管setTimeout的time延迟时间为0,其中的function也会被放入任务队列中等待下一个机会到来时执行,而不需要加入任务队列中的程序必须在任务队列的程序之前完成,因此程序的执行顺序可能和代码中的顺序不一致;

任务队列的执行时机:任务队列中的回调函数必须等待任务队列之外的所有代码执行完毕之后再执行,这是因为执行程序的时候,浏览器默认setTimeout以及ajax请求这一类的方法为耗时程序(尽管有时候并不耗时),将其加入一个队列,该队列是一个存储耗时程序的队列,在所有不耗时程序执行完后,再来依次执行任务队列中的程序;

任务排队:因为javascript是单线程的,这意味着所有的任务需要排队处理,前一个任务结束,才会执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着,于是就有了任务队列这个概念;如果排队是因为计算量大,CPU忙不过来倒也还好,很多时候CPU是闲着的,因为IO设备很慢(比如AJAX操作从网络读取数据),不得不等着结果出来,再往下执行,于是JS语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务,等到IO设备返回了结果,再回来把挂起的任务继续执行下去;

两种任务:一种是同步任务(synchronous),是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;二是异步任务(asynchronous):是指不进入主线程、而进入“任务队列”(task queue)的任务,只有等主线程任务执行完毕,任务队列才开始通知主线程请求执行任务,该任务才会进入主线程执行;具体的运行机制:

  1. 所有的同步任务都在主线程上执行,形成一个执行栈(execution context stack);
  2. 主线程之外,还存在一个“任务队列”(task queue),只要异步任务有了运行结果,就在“任务队列”之中放置一个事件;
  3. 一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”(会去Event Queue读取对应的函数),看看里面有哪些事件,哪些对应的异步任务,于是结束等待状态,开始执行;
  4. 主线程不断重复上面的第三步进行事件循环,只要主线程空了(js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空),就会去读取“任务队列”,这就是JS的运行机制,这个过程会不断重复,也就是常说的Event Loop(事件循环);

d1415cecee01398d5e838507aa392796.png

任务队列中的事件:任务队列是一个事件的队列,也可以理解成消息的队列,IO设备完成一项任务,就在任务队列中添加一个事件,表示相关的异步任务可以进入“执行栈”了,主线程读取“任务队列”,就是读里面有哪些事件;任务队列中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等),比如$(selectot).click(function),这些都是相对耗时的操作,只要指定过这些事件的回调函数,这些事件发生时就会进入任务队列等待主线程读取;

回调函数(callback):就是那些会被主线程挂起来的代码,前面所说的点击事件$(selectot).click(function)中的function就是一个回调函数,异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数,例如ajax的success,complete,error也都指定了各自的回调函数,这些函数就会加入任务队列中,等待执行;

下面以AJAX请求为例,来看一下同步和异步的区别:

异步AJAX:

主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

AJAX线程:“好的,主线程。我马上去发,但可能要花点儿时间呢,你可以先去忙别的。”

主线程::“谢谢,你拿到响应后告诉我一声啊。”
(接着,主线程做其他事情去了。一顿饭的时间后,它收到了响应到达的通知。)

同步AJAX:

主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

AJAX线程:“......”

主线程::“喂,AJAX线程,你怎么不说话?”

AJAX线程:“......”

主线程::“喂!喂喂喂!”

AJAX线程:“......”

(一炷香的时间后)
主线程::“喂!求你说句话吧!”

AJAX线程:“主线程,不好意思,我在工作的时候不能说话。你的请求已经发完了,拿到响应数据了,给你。”

正是由于JavaScript是单线程的,而异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择;

同步阻塞案例:

// 这是一个阻塞式函数, 将一个文件复制到另一个文件上
// 调用这个”copyBigFile()”函数,将一个大文件复制到另一个文件上,将耗时1小时,
//意味着这个函数的将在一个小时之后返回
function copyBigFile(afile, bfile){
    var result = copyFileSync(afile,bfile);
    return result;
}

//这是一段程序
console.log("start copying ... ");    
var a = copyBigFile('A.txt', 'B.txt');  //这行程序将耗时1小时
console.log("Finished");   // 这行程序将在一小时后执行
console.log("处理一下别的事情");  // 这行程序将在一小时后执行
console.log("Hello World, 整个程序已加载完毕,请享用"); // 这行程序将在一小时后执行

同步非阻塞案例:

// 这是一个非阻塞式函数
// 如果复制已完成,则返回 true, 如果未完成则返回 false
// 调用这个函数将立刻返回结果
function copyBigFile(afile,bfile){
    var copying = copyFileAsync(afile, bfile);
    var isFinished = !copying;
    return !isFinished; 
}

console.log("start copying ... ");    
// 同步的程序需要在一个循环中轮询结果
while( a = copyBigFile('A.txt', 'B.txt')){
  console.log("在这之间还可以处理别的事情");
} ;  
console.log("Finished");   // 这行程序将在一小时后执行
console.log("Hello World, 整个程序已加载完毕,请享用"); // 这行程序将在一小时后执行

// 非阻塞式的函数给编程带来了更多的便利,在长IO操作的同时,
//可以写点其他的程序,提高效率,执行结果如下:

// start copying ...
// 在这之间还可以处理别的事情
// 在这之间还可以处理别的事情
// 在这之间还可以处理别的事情
// ...
// Finished
// Hello World, 整个程序已加载完毕,请享用

异步非阻塞案例:

同步的程序需要在一个循环中轮询结果,循环里面的程序会被执行好多遍,所以并不好控制来写一些正常的程序,很难再利用起来,更为合理的方式是对非阻塞式的函数进行利用,也就是主线程不会主动地去询问结果,而是当任务有了结果的时候再来通知主线程;

//非阻塞式的有异步通知能力的函数
//以下不需要看懂,只需知道这个函数会在完成copy操作之后,执行success
function copyBigFile(afile,bfile, callback){
    var copying = copyFileAsync(afile, bfile, function(){ callback();});
    var isFinished = !copying;
    return !isFinished; 
}

// 不同于上一个同步非阻塞函数的地方在于它具有通知功能,能够在完成操作之后主动地通知程序,
//“我完成了”
console.log("start copying ... ");    
copyBigFile("A.txt","B.txt", function(){
          console.log("Finished");   //一个小时后被执行
          console.log("Hello World, 整个程序已加载完毕,请享用"); //一个小时后被执行
          })
console.log("干别的事情"); 
console.log("做一些别的处理"); 

// 程序在调用copyBigFile函数之后,可以立即获得返回值,线程没有被阻塞住,于是还可以去干些别的事情,
//然后当copyBigFile完成之后,会执行指定的函数
// start copying ...
// 干别的事情
// 做一些别的处理
// Finished
// Hello World, 整个程序已加载完毕,请享用

三、异步过程的构成要素

从上文可以看出,异步函数实际上很快就调用完成了,但是后面还有工作线程执行异步任务、通知主线程、主线程调用回调函数等很多步骤,我们把整个过程叫做异步过程,异步函数的调用在整个异步过程中,只是一小部分;

一个异步过程通常是这样的:主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数);

异步调用一般分为两个阶段,提交请求和处理结果,这两个阶段之间有事件循环的调用,它们属于两个不同的事件循环(tick),彼此没有关联,异步调用一般以传入callback的方式来指定异步操作完成后要执行的动作,而异步调用本体和callback属于不同的事件循环;

try/catch语句只能捕获当次事件循环的异常,对callback无能为力

// 异步函数通常具有以下的形式:
A(args...,callbackFn)

// 它可以叫做异步过程的发起函数,或者叫做异步任务注册函数,args是这个函数需要的参数,
///callbackFn也是这个函数的参数,但是它比较特殊所以单独列出来;

从主线程的角度看,一个异步过程包括下面两个要素:

  1. 发起函数(或叫注册函数)A提交请求
  2. 回调函数callbackFn

它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果

// 举个具体的例子:
setTimeout(fn, 1000);
// 其中的setTimeout就是异步过程的发起函数,fn是回调函数。
// 注意:前面说的形式A(args..., callbackFn)只是一种抽象的表示,
//并不代表回调函数一定要作为发起函数的参数,例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数
// 发起函数和回调函数就是分离的。

四、消息队列和事件循环

JS是单线程的,但却能执行异步任务,这主要是因为JS中存在事件循环(Event Loop)和任务队列(Task Queue);

事件循环(是js实现异步的一种方法,也是js的执行机制:JS会创建一个类似于while(true)的循环,每执行一次循环体的过程称为Tick,每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行,待处理的事件会存储在一个任务队列中,也就是每次Tick都会查看任务队列中是否有需要执行的任务;实际上事件循环是指主线程重复从消息队列取出消息、执行的过程;

任务队列:也称为消息队列,是一个先进先出的队列,它里面存放着各种消息,即异步操作的回调函数,异步操作会将相关回调添加到任务队列中,而不同的异步操作添加到任务队列的时机也不同,如onclick,setTimeout,ajax处理的方式都不同,这些异步操作都是由浏览器内核的不同模块来执行的:

  1. onclick由浏览器内核的DOM Binding模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中;
  2. setTimeout会由浏览器内核的timer模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中;
  3. ajax会由浏览器内核的network模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中;

主线程:JS只有一个线程,称之为主线程,而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的,因此主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行,只有当主线程中执行栈为空的时候,即同步代码执行完后,才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行;

ES6新增的任务队列:ES6 中新增的任务队列是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务队列优先级更高,如 Promise 就使用了 ES6 的任务队列特性;

AJAX异步:JS是单线程运行的,XMLHttpRequest在连接后是异步的,请求是由浏览器新开一个线程请求的,当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理,当任务被处理时,JS引擎始终是单线程运行回调函数,即onreadystatechange所设置的函数;

异步过程中,工作线程在异步操作完成后需要通知主线程,那么这个通知机制是怎样实现的呢?答案是利用消息队列事件循环

工作线程将消息放到消息队列,主线程通过事件循环过程去取消息

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消消息、再执行,当消息队列为空时,就会等待直到消息队列变成非空,而且主线程只有在将当前的消息执行完成后,才会去取下一个消息,这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环;

// 事件循环用代码表示大概是这样的:

while(true) {
    var message = queue.get();
    execute(message);
}

那么消息队列中放的消息具体是什么东西呢?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:消息就是注册异步任务时添加的回调函数;

// 再次以异步AJAX为例,假设存在如下的代码:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是响应:', resp);
});

// 其他代码
...
...
...

主线程在发起AJAX请求后,会继续执行其他代码,AJAX线程负责请求http://segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构造一条消息:

// 消息队列中的消息就长这个样子
var message = function () {
    callbackFn(response);
}

其中的callbackFn就是前面代码中得到成功响应时的回调函数,主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它,到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行,如果一开始主线程就没有提供回调函数,AJAX线程在收到HTTP响应后,也就没必要通知主线程,从而也没必要往消息队列放消息
异步过程的回调函数,一定不在当前这一轮事件循环中执行;

还有一点需要注意的是:触发和执行并不是同一概念,计时器的回调函数一定会在指定delay的时间后被触发,但并不一定立即执行,可能需要等待,所有的js代码都是在同一个线程里执行的,但像鼠标点击和计时器之类的事件只有在js单线程空闲时才执行;

五、异步与事件

上文中所说的“事件循环”,为什么里面有个事件呢?那是因为:消息队列中的每条消息实际上都对应着一个事件;

上文中一直没有提到一类很重要的异步过程:DOM事件;

// 举例
var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
    console.log();
});
  • 从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器,当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用;
  • 从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数,事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行;
  • 事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制,我觉得它的存在是为了编程接口对开发者更友好
  • 另一方面,所有的异步过程也都可以用事件来描述,例如:setTimeout可以看成对应一个时间到了的事件,前文的setTimeout(fn,1000);可以看成:
timer.addEventListener('timeout', 1000, fn);
  • 从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器,当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用;
  • 从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数,事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行;
  • 事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制,我觉得它的存在是为了编程接口对开发者更友好
  • 另一方面,所有的异步过程也都可以用事件来描述,例如:setTimeout可以看成对应一个时间到了的事件,前文的setTimeout(fn,1000);可以看成:
timer.addEventListener('timeout', 1000, fn);

六、生产者与消费者

从生产者与消费者的角度看,异步过程是这样的:

工作线程是生产者,主线程是消费者(只有一个消费者)。工作线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空

七、Event Loop的其他解释

Event Loop是一个程序结构,用于等待和发送消息和事件;

简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程");

八、事件循环中的异步队列:宏任务与微任务

事件循环中的异步队列有两种:宏任务与微任务,宏任务队列可以有多个,微任务队列只有一个

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate,I/O操作,UI渲染
  • micro-task(微任务):new.Promise().then(回调),process.nextTick,MutationObserver(html5新特性)等。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,举例:

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

new Promise(function(resolve) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('then');
})

console.log('console');
  • 这段代码作为宏任务(script中包含全部代码),进入主线程。
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue(任务队列)。
  • 接下来遇到了Promisenew Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log(),立即执行。
  • 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束

事件循环,宏任务,微任务的关系如图所示:

7c8dab7c8a7887127ce6dc88a24f996c.png

图例说明:当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队里里的任务,

依次类推

分析一段较复杂的代码:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

完整的输出为1,7,6,8,2,4,3,5,9,11,10,12

第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1
  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2

446071e359598257955fa8d4bab5d05d.png
  • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
  • 我们发现了process1then1两个微任务。
  • 执行process1,输出6。
  • 执行then1,输出8。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2

c82b8bbe26bd14780d0fa432f78b8bb1.png
  • 第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。
  • 输出3。
  • 输出5。
  • 第二轮事件循环结束,第二轮输出2,4,3,5。
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3

be86dcfa5350c2ee2c4fc4809a040dc9.png
  • 第三轮事件循环宏任务执行结束,执行两个微任务process3then3
  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)。

最后:

js的异步

javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。Event Loop是javascript的执行机制

javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值