首页 > 编程语言 > 详细

JavaScript同步于异步的区别

时间:2021-06-16 00:00:31      阅读:20      评论:0      收藏:0      [点我收藏+]

JavaScript

? js是一门单线程语言, 不像java类, 类继承Thread再来个thread.start 就可以开辟一个线程,多以, js 就像一条口水线. 仅仅是一条流水线而已, 要么加工,要么包装吗不能进行多个任务和流程.

  • 其实同步与异步, 无论如何做事情的时候都是只有一条流水线(单线程), 同步和异步的差别在意这条流水线上各个流程的执行顺序不同

  • 最近本的异步是setTimeout 和setInterval 函数, 很常见, 但是很少有人知道其实这就是异步, 因为他们可以控制js的执行顺序.我们可以简单理解为: 可以改变程序正常执行顺序的操作就可以看成是异步操作. 如下代码

    <script>
       setTimeout(funtion(){
                  consloe.log(‘2‘)
                  },0);  
       console.log( "1" );
    <acript>
    
    <script>
       let arr = [{
           age: ‘1‘,
           name: ‘张三‘
       }, {
           age: ‘5‘,
           name: ‘李四‘
       }, {
           age: ‘6‘,
           name: ‘王五‘
       }]
       for (const item of arr) {
           //undefined  是特殊的数据类型, 需要使用typeof 来检测
           if (typeof (item.log) == "undefined") {
               console.log(‘hhhhh‘);
           } else {
               console.log(‘不要‘);
           }
       }
    </script>
    

    可见, 尽管我们设置了setTimeout(function, time) 中的等待时间为0, 结果其中的function 还是最后执行

  • 火狐浏览器的api文档有这样一句话:Because even though setTimeout was called with a delay of zero, it‘s placed on a queue and scheduled to run at the next opportunity, not immediately. Currently executing code must complete before functions on the queue are executed, the resulting execution order may not be as expected.

    意思就是:尽管setTimeout的time延迟时间为0,其中的function也会被放入一个队列中,等待下一个机会执行,当前的代码(指不需要加入队列中的程序)必须在该队列的程序完成之前完成,因此结果可能不与预期结果相同。

  • 这里说到了一个“队列”(即任务队列),该队列放的是什么呢,放的就是setTimeout中的function,这些function依次加入该队列,即该队列中所有function中的程序将会在该队列以外的所有代码执行完毕之后再以此执行,这是为什么呢?因为在执行程序的时候,浏览器会默认setTimeout以及ajax请求这一类的方法都是耗时程序(尽管可能不耗时),将其加入一个队列中,该队列是一个存储耗时程序的队列,在所有不耗时程序执行过后,再来依次执行该队列中的程序。

  • 于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。

  • 具体来说,异步运行机制如下:

    (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
    (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
    (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    (4)主线程不断重复上面的第三步。

     只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。  
    

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等),比如$(selectot).click(function),这些都是相对耗时的操作。只要指定过这些事件的回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码,前面说的点击事件$(selectot).click(function)中的function就是一个回调函数。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。例如ajax的success,complete,error也都指定了各自的回调函数,这些函数就会加入“任务队列”中,等待执行。

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

异步任务队列

可能有人告诉你,Javascript内部存在着先进先出的异步任务队列,仅仅用以存储异步任务,与同步任务分开管理。

进程执行完全部同步代码后,每当进程空闲、触发回调或定时器到达规定的时间,Javascript会从队列中顺序取出符合条件的异步任务并执行之。

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

  • 事件循环:JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次Tick会查看任务队列中是否有需要执行的任务。

  • 异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclick, setTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的webcore来执行的,webcore包含下图中的3种 webAPI,分别是DOM Binding、network、timer模块。

    • DOM Binding 模块处理一些DOM绑定事件,如onclick事件触发时,回调函数会立即被webcore添加到任务队列中。
    • network 模块处理Ajax请求,在网络请求返回时,才会将对应的回调函数添加到任务队列中。
    • timer 模块会对setTimeout等计时器进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
  • 主线程:JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。

  • 一个浏览器环境,只能有一个事件循环,而一个事件循环可以多个任务队列,每个任务都有一个任务源(Task source)。

    相同任务源的任务,只能放到一个任务队列中。

    不同任务源的任务,可以放到不同任务队列中。

    又举了一个例子说,客户端可能实现了一个包含鼠标键盘事件的任务队列,还有其他的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它。这样就能保证流畅的交互性,而且别的任务也能执行到了。同一个任务队列中的任务必须按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行。

    结论:一个事件循环可以有多个任务队列,队列之间可有不同的优先级,同一队列中的任务按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行。

setTimeout(function(){
console.log(2);
},0);

new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});

console.log(6);

setTimeout(function(){
console.log(7);
},0);

console.log(8);

输出结果是,3 4 6 8 5 2 7。为什么setTimeout会后于promise.then执行呢,原因或许就是它所处的任务队列优先级较低。

  • 不同任务队列的优先级
    那么接下来,我们探究一下不同任务队列的优先级。

    实际上,对于任务队列的优先级的定义,Promise/A+ 规范中有作详细的解释。

    图灵社区 : 阅读 : 【翻译】Promises/A+规范

    我们都知道,一个Promise的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。

    而上面的Promises规范就规定了,实践中要确保onFulfilled和onRejected异步执行,且应该在then方法被调用的那一轮事件循环以后的新执行栈中执行。

    意思就是,当我们调用resolve()或reject()的时候,触发promise.then(...)实际上是一个异步操作,这个promise.then(...)并不是在resolve()或reject()的时候就立刻执行的,而也是要重新进入任务队列排队的,不过能直接在当前的事件循环新的执行栈中被取出执行(不用等下次事件循环)。

    知道这个以后,我们再看一段代码,这个代码包含常用的大部分异步操作,我们将借此得出不同任务队列的优先顺序
    (其中setImmediate()process.nextTick()是node的语句)

    setImmediate(function(){
    console.log(1);
    },0);
    setTimeout(function(){
    console.log(2);
    },0);
    new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
    }).then(function(){
    console.log(5);
    });
    console.log(6);
    process.nextTick(function(){
    console.log(7);
    });
    console.log(8);
    NodeJs环境输出:

    其中3 4 6 8是同步输出的。 因为注册顺序:1 > 2 > 5 > 7,而输出顺序是7 > 5 > 2 > 1。

    所以可以很容易得到,优先级 :process.nextTick > promise.then > setTimeout > setImmediate。

  • process.nextTick()属于idle观察者,setImmediate()属于check观察者.在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者.

而实际上,上述的Promises规范早已提到异步队列优先级规定的详细定义和解释了,并不需要我们一个一个去测试。

小结

在JS引擎中,我们可以按性质把任务分为两类,macrotask(宏任务)和 microtask(微任务)。

浏览器JS引擎中:

macrotask(按优先级顺序排列): script(你的全部JS代码,“同步代码”), setTimeout, setInterval, setImmediate, I/O,UI rendering
microtask(按优先级顺序排列):process.nextTick,Promises(这里指浏览器原生实现的 Promise), Object.observe, MutationObserver
JS引擎首先从macrotask queue中取出第一个任务,执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行;
然后再从macrotask queue(宏任务队列)中取下一个,执行完毕后,再次将microtask queue(微任务队列)中的全部取出;
循环往复,直到两个queue中的任务都取完。
所以,浏览器环境中,js执行任务的流程是这样的:

第一个事件循环,先执行script中的所有同步代码(即 macrotask 中的第一项任务)
再取出 microtask 中的全部任务执行(先清空process.nextTick队列,再清空promise.then队列)
下一个事件循环,再回到 macrotask 取其中的下一项任务
再重复2
反复执行事件循环…
NodeJS引擎中:

先执行script中的所有同步代码,过程中把所有异步任务压进它们各自的队列(假设维护有process.nextTick队列、promise.then队列、setTimeout队列、setImmediate队列等4个队列)
按照优先级(process.nextTick > promise.then > setTimeout > setImmediate),选定一个 不为空 的任务队列,按先进先出的顺序,依次执行所有任务,执行过程中新产生的异步任务继续压进各自的队列尾,直到被选定的任务队列清空。
重复2...
也就是说,NodeJS引擎中,每清空一个任务队列后,都会重新按照优先级来选择一个任务队列来清空,直到所有任务队列被清空。

现在,你可以根据这个流程再看回前面的代码,其实一切都很容易理解了…

以上,就是Javascript任务队列的顺序机制。

JavaScript同步于异步的区别

原文:https://www.cnblogs.com/sanqi-988/p/14886938.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!