JavaScript同步任务先于异步任务执行,那为什么图中异步任务b在a未结束运行的时候就开始执行?

JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

1.1 单线程代码示例

1.2 浏览器的渲染进程是提供多个线程

也称为JS内核,负责处理JavaScript脚本。(例如V8引擎)
①JS引擎线程负责解析JS脚本,运行代码。
②JS引擎一直等待着任务队列中的任务的到来,然后加以处理。
③一个Tab页(renderer进程)中无论什么时候都只有一个JS线程运行JS程序。
归属于渲染进程而不是JS引擎,用来控制事件循环
①当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、Ajax异步请求等),会将对应任务添加到事件线程中。
②当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
注意:由于JS的单线程关系,所以这些待处理队列中的事件都是排队等待JS引擎处理,JS引擎空闲时才会执行。
①浏览器定时计数器并不是由JS引擎计数的。
②JS引擎时单线程的,如果处于阻塞线程状态就会影响计时的准确,因此,通过单独的线程来计时并触发定时。
③计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。
注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
XMLHttpRequest在连接后通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中,再由JS引擎执行。
负责渲染浏览器界面,包括:
②重绘(Repaint)以及回流(Reflow)处理。

  1. 一个进程包含一个或多个线程
  2. chrome为例,打开一个Tab(创建一个进程),又会包含多个线程(JS引擎线程、渲染线程)

“任务队列”是一个事件的队列(也可以理解成消息的队列)

任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务 指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务 指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
“任务队列”是一个先进先出(FIFO)的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的定时器功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

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

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。

3.1 执行栈和事件队列

//b、a、c复制代码

就算延时为 0ms,只是 timer 2 的回调函数会立即加入消息队列而已,回调的执行还是得等执行栈为空(JS引擎线程空闲)时执行。

其实 setTimeout 的第二个参数并不能代表回调执行的准确的延时事件,它只能表示回调执行的最小延时时间,因为回调函数进入消息队列后需要等待执行栈中的同步任务执行完成,执行栈为空时才会被执行。

浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。

有些地方会列出来UI Rendering,说这个也是宏任务
可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤



requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行

Promise中注意是回调,而new Promise在实例化的过程中所执行的代码都是同步进行的

  1. 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。
  2. 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
  3. 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
  4. 执行渲染操作,更新界面
  5. 检查是否存在 Web worker 任务,如果有,则对其进行处理
  6. 上述过程循环往复,直到两个队列都清空

3.4 事件循环伪代码

四、事件循环(进阶)与异步

// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行

直到 2 秒后,主线程中的任务才执行完成,这才去执行 macrotask 中的 setTimeout 回调任务。

  1. 函数前面async关键字的作用就2点:①这个函数总是返回一个promise。②允许函数内使用await关键字。
  2. 关键字await使async函数一直等待(执行栈当然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。await只在async函数函数里面奏效。
  3. async函数只是一种比promise更优雅得获取promise结果(promise链式调用时)的一种语法而已。

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境

JS的运行环境主要有两个:浏览器、Node。
在两个环境下的Event Loop实现是不一样的,在浏览器中基于规范来实现,不同浏览器可能有小小区别。在Node中基于libuv这个库来实现。

  1. 解析后的代码,调用Node API
  2. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
  3. V8引擎再将结果返回给用户

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

浏览器和Node 环境下,microtask 任务队列的执行时机不同:

  1. Node端,microtask 在事件循环的各个阶段之间执行

先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务

  1. 从上往下执行代码,先执行同步代码,输出script start
  2. 接着往下执行,输出promise1,把 .then()放到微任务队列中。(注意Promise本身是同步的立即执行函数,.then是异步执行函数!)
  3. 接着往下执行, 输出script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
  4. 依次执行微任务中的代码,依次输出async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出setTimeout

这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务

  1. 从上往下执行代码,先执行同步代码,输出start
  2. 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出children2,此时,会把 Promise.resolve().then放到微任务队列中。
  3. 宏任务①中的代码执行完成后,会查找微任务队列,于是输出children3;然后开始执行宏任务②,即第二个 setTimeout,输出children5,此时将.then放到微任务队列中。
  4. 宏任务②中的代码执行完成后,会查找微任务队列,于是输出children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出children6

  1. 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出3
  2. 遇到 p().then 会先放到微任务队列中,接着往下执行,输出end
  3. 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出2, 接着执行p().then, 输出4
  4. 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时1不会输出。

  1. 一开始执行栈的同步任务(这属于宏任务) 执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出Promise1,同时会生成一个宏任务 setTimeout2
  2. 在执行宏任务setTimeout1时会生成微任务Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出Promise2
  3. 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是setTimeout2

由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。

}

nodejs并不是真正的单线程架构,其中还有I/O线程,这些线程是由更底层的libuv/

  1. 线程的同步、冲突问题不需要担心
  1. 无法充分利用CPU资源
  2. 只能用一个cpu,一旦其被某个计算占用,后续请求就会被一直挂起

Node.js中的事件循环机制

事件循环允许Node.js执行非阻塞I/O操作.尽管JavaScript是单线程的.通过尽可能将操作卸载到系统内核。由于大多数现代内核都是多线程的,因此它们可以处理在后台执行的多个操作。当其中一个操作完成时,内核会告诉Node.js,以便可以将相应的回调添加到轮询队列中以最终执行

 EventLoop 中的 6个步骤都是相对于宏任务(Macro)的讲述的。NodeJs执行完成一个宏任务后会立即清空当前队列中产生的所有微任务。

上一次循环队列中还未执行完毕的会在这个阶段执行,比如延迟到下一个Loop之中的I/O操作,

尽在NodeJs内部调用,无法操作

  • 如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。

  • 如果脚本 未被 setImmediate()调度,则事件循环将等待回调被添加到队列中,然后立即执行。

poll可以提前结束使事件循环,提升效率


    • 等待被调用方执行完毕才能继续执行
  • 不需要一直等待被调用方响应,调用方的主动轮询和被调用方的主动通知
  • 不会阻塞后面代码的执行
  • 区别:调用过程中是主动等待还是被动通知,是否阻塞
    • 区别:调用状态,调用方在获取结果的过程中是干等还是互不耽误
    • 异步非阻塞是节约调用方时间的(nodejs 一大特点)

这里打印输出出来的结果,并没有什么固定的先后顺序,偏向于随机。(event loop启动需要时间)

如果没有到1ms,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,于是往下继续,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

问题总结:而我们在==执行启动代码==的时候,进入timers的时间延迟其实是==随机的==,并不是确定的,所以会出现两个函数执行顺序随机的情况。


单线程怎么支持高并发呢?

比如有个客户端请求A进来,需要读取文件,读取文件后将内容整合,最后数据返回给客户端。但在读取文件的时候另一个请求进来了,那处理的流程是怎么样的?

  • 请求A进入服务器,线程开始处理该请求
  • A 请求需要读取文件,ok,交给文件 IO 处理,但是处理得比较慢,需要花 3 秒,这时候 A 请求就挂起(这个词可能不太恰当),等待通知,而等待的实现就是由事件循环机制实现的,
  • 在A请求等待的时候,cpu 是已经被释放的,这时候B请求进来了, cpu 就去处理B请求
  • 两个请求间,并不存在互相竞争的状在 js 引擎上执行的,执行栈一直卡态。那什么时候会出现请求阻塞呢?涉及到大量计算的时候,因为计算是着,别的函数就没法执行,举个栗子,构建一个层级非常深的大对象,反复对这个这个对象 JSON.parse(JSON.stringify(bigObj))

 cluster 模块可以创建共享服务器端口的子进程

在一个进程的前提下开启多个线程

可开辟线程执行大量计算,返回结果


    1. 主线程通过event loop事件循环触发的方式来运行程序
    1. node 具有异步 I/O 特性,每当有 I/O 请求发生时,node 会提供给该请求一个 I/O 线程。然后 node 就不管这个 I/O 的操作过程了,而是继续执行主线程上的事件,只需要在该请求返回回调时再处理即可。也就是 node 省去了许多等待请求的时间

node中每一个模块都有一个自己的module对象


    • 如果已经 require 过,不会重复执行加载,直接可以拿到里面的接口对象
    • 目的是避免重复加载,提高模块加载效率
    • 自定义模块(路径形式的模块标识)
    • 首位的/是绝对路径,代表当前文件模块所属磁盘根目录c:/

  1. 记住依赖信息,提升加载速度
  2. 为了系统的稳定性考虑,锁定版本号,防止自动升级
  1. 保存第三方包的依赖信息,比 node_modules 清晰。package.json 文件相当于给他人使用时,提供了一份安装所有依赖包的自动下载索引
    • dependencies:在生产环境中需要用到的依赖

  • 解析后的代码,调用Node API。
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

  • 防止DOM渲染冲突的问题;

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

}

我要回帖

更多关于 django异步任务进度 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信