你是否遭受到这样的恐吓?

你是否有过每个表达式前面都console一遍值去找执行顺序?

看了很多js执行机制的文章似乎都是似懂非懂,到技术面问的时候,理不清思绪。总结了众多文章的例子和精华,希望能帮到你们

JavaScript 怎么执行的?

执行机制——事件循环(Event Loop)

通常所说的 JavaScript Engine (JS引擎)负责执行一个个 chunk (可以理解为事件块)的程序,每个 chunk 通常是以 function 为单位,一个 chunk 执行完成后,才会执行下一个 chunk。下一个 chunk 是什么呢?取决于当前 Event Loop Queue (事件循环队列)中的队首。

通常听到的JavaScript EngineJavaScript runtime 是什么?

  • Javascript Engine  :Js引擎,负责解释并编译代码,让它变成能交给机器运行的代码(runnable commands)
  • Javascript runtime :Js运行环境,主要提供一些对外调用的接口 。比如浏览器环境:windowDOM。还有Node.js环境:require 、export

Event Loop Queue (事件循环队列)中存放的都是消息,每个消息关联着一个函数,JavaScript Engine (以下简称JS引擎)就按照队列中的消息顺序执行它们,也就是执行 chunk

例如

setTimeout( function() {console.log('timeout')
}, 1000)复制代码

当JS引擎执行的时候,可以分为3步chunk

  1. setTimeout 启动定时器(1000毫秒)执行
  2. 执行完毕后,得到机会将 callback 放入 Event Loop Queue
  3. 此 callback 执行

每一步都是一个chunk,可以发现,第2步,得到机会很重要,所以说即使延迟1000ms也不一定准的原因。因为如果有其他任务在前面,它至少要等其他消息对应的程序都完成后才能将callback推入队列,后面我们会举个?

像这个一个一个执行chunk的过程就叫做Event Loop(事件循环)

按照阮老师的说法:

总体角度:主线程执行的时候产生栈(stack)和堆(heap),栈中的代码负责调用各种API,在任务队列中加入事件(click,load,done),只要栈中的代码执行完毕后,就会去读取任务队列,依次执行那些事件所对应的回调函数。

执行的机制流程

同步直接进入主线程执行,如果是异步的,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

我们都知道,JS引擎 对 JavaScript 程序的执行是单线程的,为了防止同时去操作一个数据造成冲突或者是无法判断,但是 JavaScript Runtime(整个运行环境)并不是单线程的;而且几乎所有的异步任务都是并发的,例如多个 Job QueueAjaxTimerI/O(Node)等等。

而Node.js会略有不同,在node.js启动时,创建了一个类似while(true)的循环体,每次执行一次循环体称为一次tick,每个tick的过程就是查看是否有事件等待处理,如果有,则取出事件极其相关的回调函数并执行,然后执行下一次tick。node的Event Loop和浏览器有所不同。Event Loop每次轮询:先执行完主代码,期中遇到异步代码会交给对应的队列,然后先执行完所有nextTick(),然后在执行其它所有微任务。

任务队列

任务队列task queue中有微任务队列宏任务队列

  • 微任务队列只有一个
  • 宏任务可以有若干个

根据目前,我们先大概画个草图

具体部分后面会讲,那先说说同步和异步

执行机制——同步任务(synchronous)和异步任务(asynchronous)

事件分为同步和异步

同步任务

同步任务直接进入主线程进行执行

console.log('1');var sub = 0;
for(var i = 0;i < 1000000000; i++) {sub++
}
console.log(sub);console.log('2');
.....复制代码

会点编程的都知道,在打印出sub的值之前,系统是不会打印出2的。按照先进先出的顺序执行chunk。

如果是Execution Context Stack(执行上下文堆栈)

function log(str) {console.log(str);
}
log('a');复制代码

从执行顺序上,首先log('a')入栈,然后console.log('a')再入栈,执行console.log('a')出栈,log('a')再出栈。

异步任务

异步任务必须指定回调函数,所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务进入Event Table后,当指定的事情完成了,就将异步任务加入Event Queue,等待主线程上的任务完成后,就执行Event Queue里的异步任务,也就是执行对应的回调函数。

指定的事情可以是setTimeout的time?

var value = 1;
setTimeout(function(){value = 2;
}, 0)
console.log(value);  // 1复制代码

从这个例子很容易理解,即使设置时间再短,setTimeout还是要等主线程执行完再执行,导致引用还是最初的value

?

console.log('task1');setTimeout(()=>{ console.log('task2') },0);var sub = 0;
for(var i = 0;i < 1000000000;i++) {sub++
}
console.log(sub);
console.log('task3');复制代码

分析一下

  • task1进入主线程立即执行
  • task2进入Event Table,注册完事件setTimeout后进入Event Queue,等待主线程执行完毕
  • sub赋值后进入for循环自增,主线程一直被占用
  • 计算完毕后打印出sub,主线程继续chunk
  • task3进入主线程立即执行
  • 主线程队列已清空,到Event Queue中执行任务,打印task2

不管for循环计算多久,只要主线程一直被占用,就不会执行Event Queue队列里的任务。除非主线任务执行完毕。所有我们通常说的setTimeouttime是不标准的,准确的说,应该是大于等于这个time

来个?体验一下结果

var sub = 0;
(function setTime(){let start = (new Date()).valueOf();//开始时间console.log('执行开始',start)setTimeout(()=>{ console.log('定时器结束',sub,(new Date()).valueOf()-start);//计算差异},0);
})();
for(var i = 0;i < 1000000000;i++) {sub++
}
console.log('执行结束')复制代码

实际上,延迟会远远大于预期,达到了3004毫秒

最后的计算结果是根据浏览器的运行速度和电脑配置差异而定,这也是setTimeout最容易被坑的一点。

AJAX怎么算

那ajax怎么算,作为日常使用最多的一种异步,我们必须搞清楚它的运行机制。

console.log('start');$.ajax({url:'xxx.com?user=123',success:function(res){console.log('success')}
})
setTimeout(() => {console.log('timeout')
},100);console.log('end');复制代码

答案是不肯定的,可能是

start
end
timeout
success复制代码

也有可能是

start
end
success
timeout复制代码

前两步没有疑问,都是作为同步函数执行,问题原因出在ajax身上

前面我们说过,异步任务必须有callback,ajax的callbacksuccess(),也就是只有当请求成功后,触发了对应的callback success()才会被放入任务队列(Event Queue)等待主线程执行。而在请求结果返回的期间,后者的setTimeout很有可能已经达到了指定的条件(执行100毫秒延时完毕)将它的回调函数放入了任务队列等主线程执行。这时候可能ajax结果仍未返回...

Promise的执行机制

再加点料

console.log('执行开始');setTimeout(() => {console.log('timeout')
}, 0);new Promise(function(resolve) {console.log('进入')resolve();
}).then(res => console.log('Promise执行完毕') )console.log('执行结束');复制代码

先别继续往下看,假设你是浏览器,你会怎么运行,自我思考十秒钟

这里要注意,严格的来说,Promise 属于 Job Queue,只有then才是异步。

Job Queue是什么

Job Queue是ES6新增的概念。

Job Queue和Event Loop Queue有什么区别?

  • JavaScript runtime(JS运行环境)可以有多个Job Queue,但是只能有一个Event Loop Queue。
  • JS引擎将当前chunk执行完会优先执行所有Job Queue,再去执行Event Loop Queue。
Promise 中的一个个 then 就是一种 Job Queue

分析流程:

  1. 遇到同步任务,进入主线程直接执行,打印出"执行开始"
  2. 遇到setTimeout异步任务放入Event Table执行,满足条件后放入Event Queue的宏任务队列等待主线程执行
  3. 执行Promise,放入Job Queue优先执行,执行同步任务打印出"进入"
  4. 返回resolve()触发then回调函数,放入Event Queue微任务队列等待主线程执行
  5. 执行同步任务打印出"执行结束"
  6. 主线程清空,到Event Queue微任务队列取出任务开始执行。打印出"Promise执行完毕"
  7. 微任务队列清空,到宏任务队列取出任务执行,打印出"timeout"

? plus

console.log("start");setTimeout(() => {console.log("setTimeout");
}, 0);new Promise((resolve) => {resolve();
})
.then(() => {return console.log("A1");
})
.then(() => {return console.log("A2");
});new Promise((resolve) => {resolve();
})
.then(() => {return console.log("B1");
})
.then(() => {return console.log("B2");
})
.then(() => {return console.log("B3");
});console.log("end");
复制代码

打印结果

运用刚刚说说的,分析一遍

  • setTimeout异步任务,到Event Table执行完毕后将callback放入Event Queue宏任务队列等待主线程执行
  • Promise 放入Job Queue优先进入主线程执行,返回resolve(),触发A1 then回调函数放入微任务队列中等待主线程执行
  • 到第二个Promise,同上,放入Job Queue执行,将B1 then回调函数放入微任务队列
  • 执行同步函数,直接进入主线程执行,打印出"end"
  • 无同步任务,开始从task Queue 也就是 Event Queue里取出异步任务开始执行
  • 首先取出队首的A1 then()回调函数开始执行,打印出"A1",返回promise触发A2 then()回调函数,添加到微任务队首。此时队首是B1 then()
  • 从微任务队首取出B1 then回调函数,开始执行,返回promise触发B2 then()回调函数,添加到微任务队首,此时队首是A2 then(),再取出A2 then()执行,这次没有回调
  • 继续到微任务队首拿回调执行,重复轮询打印出B2B3
  • 微任务执行完毕,到宏任务队首取出setTimeout的回调函数放入主线程执行,打印出"setTimeout"

这样的话,Promise应该是搞懂了,但是微任务和宏任务?很多人对这个可能有点陌生,但是看完这个应该对这两者区别有所了解

异步任务分为宏任务和微任务

宏任务(macrotasks): setTimeout, setInterval, setImmediate(node.js), I/O, UI rendering

微任务(microtasks):process.nextTick(node.js), Promises, Object.observe, MutationObserver

先看一下具有特殊性的API:

process.nextTick

node方法,process.nextTick可以把当前任务添加到执行栈的尾部,也就是在下一次Event Loop(主线程读取"任务队列")之前执行。也就是说,它指定的任务一定会发生在所有异步任务之前。和setTimeout(fn,0)很像。

process.nextTick(callback)
复制代码

setImmediate

Node.js0.8以前是没有setImmediate的,在当前"任务队列"的尾部添加事件,官方称setImmediate指定的回调函数,类似于setTimeout(callback,0),会将事件放到下一个事件循环中,所以也会比nextTick慢执行,有一点——需要了解setImmediatenextTick的区别。nextTick虽然异步执行,但是不会给其他io事件执行的任何机会,而setImmediate是执行于下一个event loop。总之process.nextTick()的优先级高于setImmediate

setImmediate(callback)复制代码

MutationObserver

一定发生在setTimeout之前,你可以把它看成是setImmediateMutationObserver是一个构造器,接受一个callback参数,用来处理节点变化的回调函数,返回两个参数

  • mutations:节点变化记录列表(sequence<MutationRecord>)
  • observer:构造MutationObserver对象。
var observe = new MutationObserver(function(mutations,observer){// code...
})复制代码

在这不说过多,可以去了解下具体用法

Object.observe

Object.observe方法用于为对象指定监视到属性修改时调用的回调函数

Object.observe(obj, function(changes){changes.forEach(function(change) {console.log(change,change.oldValue);});
});复制代码

什么情况下才会触发?

  • 原始JavaScript对象中的变化
  • 当属性被添加、改变、或者删除时的变化
  • 当数组中的元素被添加或者删除时的变化
  • 对象的原型发生的变化

来个大?

总结:

任务优先级

同步任务 >>>  process.nextTick >>> 微任务(ajax/callback) >>> setTimeout = 宏任务 ??? setImmediate

setImmediate是要等待下一次事件轮询,也就是本次结束后执行,所以需要画???

没有把Promise的Job Queue放进去是因为可以当成同步任务来进行处理。要明确的一点是,它是严格按照这个顺序去执行的,每次执行都会把以上的流程走一遍,都会再次轮询走一遍,然后把处理对应的规则。

拿个别人的?加点料,略微做一下修改,给大家分析一下

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')})
}, 1000); //添加了1000ms
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')})
})setImmediate(function(){//添加setImmediate函数console.log('13')
})复制代码

第一遍Event Loop

  • 走到1的时候,同步任务直接打印
  • 遇到setTimeout,进入task 执行1000ms延迟,此时未达到,不管它,继续往下走。
  • 遇到process.nextTick,放入执行栈队尾(将于异步任务执行前执行)。
  • 遇到Promise 放入 Job Queue,JS引擎当前无chunk,直接进入主线程执行,打印出7
  • 触发resolve(),将then 8 放入微任务队列等待主线程执行,继续往下走
  • 遇到setTimeout,执行完毕,将setTimeout 9 的 callback 其放入宏任务队列
  • 遇到setImmediate,将其callback放入Event Table,等待下一轮Event Loop执行

第一遍完毕  17

当前队列

Number two  Ready Go!

  • 无同步任务,准备执行异步任务,JS引擎一看:"嘿!好家伙,还有个process",然后取出process.nextTick的回调函数执行,打印出6
  • 再继续去微任务队首取出then 8,打印出8
  • 微任务队列清空了,就到宏任务队列取出setTimeout 9 callback执行,打印出9
  • 继续往下执行,又遇到process.nextTick 10,放入Event Queue等待执行
  • 遇到Promise ,将callback 放入 Job Queue,当前无chunk,执行打印出 11
  • 触发resolve(),添加回调函数then 12,放入微任务队列

本次Event Loop还没有结束,同步任务执行完毕,目前任务队列

  • 再取出process.nextTick 10,打印出10
  • 去微任务队列,取出then 12 执行,打印出12
  • 本次Event Loop轮询结束 ,取出setImmediate打印出13

第二遍轮询完毕,打印出了 68911101213

当前没有任务了,过了大概1000ms,之前的setTimeout 延迟执行完毕了,放入宏任务

  • setTimeout进入主线程开始执行。
  • 遇到同步任务,直接执行,打印出2
  • 遇到process.nextTick,callback放入Event Queue,等待同步任务执行完毕
  • 遇到Promise,callback放入Job Queue,当前无chunk,进入主线程执行,打印出4
  • 触发resolve(), 将then 5放入微任务队列

同步执行完毕,先看下目前的队列

剩下的就很轻松了

  • 取出process.nextTick 3 callback执行,打印出3
  • 取出微任务 then 5,打印出 5
  • over

总体打印顺序

1
7
6
8
9
11
10
12
13
2
4
3
5复制代码

emmm...可能需要多看几遍消化一下。

Web Worker

现在有了Web Worker,它是一个独立的线程,但是仍未改变原有的单线程,Web Worker只是个额外的线程,有自己的内存空间(栈、堆)以及 Event Loop Queue。要与这样的不同的线程通信,只能通过 postMessage。一次 postMessage 就是在另一个线程的 Event Loop Queue 中加入一条消息。说到postMessage可能有些人会联想到Service Work,但是他们是两个截然不同

Web Worker和Service Worker的区别

Service Worker:
处理网络请求的后台服务。完美的离线情况下后台同步或推送通知的处理方案。不能直接与DOM交互。通信(页面和Service Worker之间)得通过postMessage方法 ,有另一篇文章是关于本地储存,其中运用到页面离线访问Service Work of  Google PWA,有兴趣的可以看下

Web Worker:
模仿多线程,允许复杂的脚本在后台运行,所以它们不会阻止其他脚本的运行。是保持您的UI响应的同时也执行处理器密集型功能的完美解决方案。不能直接与DOM交互。通信必须通过postMessage方法

如果意犹未尽可以尝试去深入Promise另一篇文章——一次性让你懂async/await,解决回调地狱

一次性搞懂JavaScript 执行机制相关推荐

  1. 彻底搞懂JavaScript执行机制

    首先我们大家都了解的是,JavaScript 是一门单线程语言,所以我们就可以得出: JavaScript 是按照语句顺序执行的 首先看: let a = '1' console.log(a) let ...

  2. 真正通俗易懂让你搞懂Javascript 执行机制

    不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况: 给定的几行代码,我们需要知道其输出内容和顺序.因为javascript是一门单线程语言,所以我们 ...

  3. 弄懂 JavaScript 执行机制,宏任务和微任务

    本文的目的就是要保证你彻底弄懂javascript的执行机制. 不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出 ...

  4. 这一次,彻底弄懂 JavaScript 执行机制

    本文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我. 不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定 ...

  5. javascript等待异步线程完成_前端:什么是单线程,同步,异步?彻底弄懂 JavaScript 执行机制...

    javascript是按照语句出现的顺序执行的. js是一行一行执行的: let a = '1';console.log(a);let b = '2';console.log(b); 然而实际上js是 ...

  6. animation 先执行一次 在持续执行_这一次,彻底弄懂 JavaScript 执行机制

    本文来源:ssssyokihttps://juejin.im/post/6844903512845860872 不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇 ...

  7. 一篇文章搞懂JavaScript运行机制

    单线程的JavaScript: 众所周知JavaScript这门语言是单线程,但是为什么要设计成单线程呢?明明多线程更加有效率. 这里我们就要从JavaScript的用途来考虑,JavaScript是 ...

  8. 一次性搞懂JavaScript正则表达式之语法

    本文是『horseshoe·Regex专题』系列文章之一,后续会有更多专题推出 GitHub地址:https://github.com/veedrin/horseshoe 博客地址(文章排版真的很漂亮 ...

  9. javascript执行机制

    转自:ssssyoki  这一次,彻底弄懂 JavaScript 执行机制 在掘金上讲解js执行机制的文章,收获很多.故复制过来,做复习笔记用 不论你是javascript新手还是老鸟,不论是面试求职 ...

最新文章

  1. python调用webservice接口实例_python发布webservice接口
  2. Linux (CentOS)安装VNC+XFCE可视化桌面环境 附安装FireFox浏览器
  3. runtimeexception异常_应用系统的异常管理-持续更新
  4. 今天,我们来详细的聊一聊SpringBoot自动配置原理,学了这么久,你学废了吗?
  5. windows live writer向cnblog发布文章设置(转)
  6. 关于Eclipse中的开源框架EMF(Eclipse Modeling Framework),第三部分
  7. 覆盖所有面试知识点,持续更新中
  8. hdu-1862-EXCEL排序
  9. 关于浏览器的那些事情1【面试】
  10. 物联网已死,API 万岁!
  11. 预产期在线计算机,预产期计算器
  12. 3dm格式catia能打开吗_CATIA 3DXML文件格式介绍 | 坐倚北风
  13. 管理信息系统案例分析_万华集团 IT 规划案例分析
  14. 电力前沿:Hightopo 助力贵州院打造智慧能源生态系统
  15. “视”不可挡:征兵招警,近视手术成“通关法宝”
  16. 互动媒体技术A1作业报告
  17. 转--各种文件后缀名与打开方式大全
  18. Jquery 效果集结号
  19. C++:实现量化Libor市场模型流程测试实例
  20. 北京市高等教育自学考试2023年教材信息表

热门文章

  1. MFS分布式文件系统搭建
  2. SVN仓库安装、备份和迁移基本操作
  3. UUID 查看linux的UUID 与 SVN 工程的 UUID。(两者之间没有联系)
  4. 【开发环境】 irun(ncverilog)无法dump fsdb波形问题解决方法
  5. 如何在Go中找到一个对象的类型?
  6. 为什么我的Spring @Autowired字段为空?
  7. 按值设置选择选项“已选择”
  8. 从其他文件夹导入文件
  9. ros开发增加clion常用模板及初始化配置(三)
  10. pdf模板工具JaspersoftStudio,JasperReport