关于javascript:Javascript-运行机制

8次阅读

共计 5153 个字符,预计需要花费 13 分钟才能阅读完成。

1. 单线程的 JavaScript

JavaScript 是单线程的语言这,由它的用处决定的, 作为浏览器的脚本语言,次要负责和用户交互,操作 DOM。

如果 JavaScript 是多线程的,有两个线程同时操作一个 DOM 节点,一个负责删除 DOM 节点,一个在 DOM 节点上增加内容,浏览器该以哪个线程为规范呢?

所以,JavaScript 的用处决定它只能是单线程的,过来是,未来也不会变。

HTML5 的 Web Worker 容许 JavaScript 主线程创立多个子线程,然而这些子线程齐全受主线程的管制,且不可操作 DOM 节点,所以 JavaScript 单线程的实质并没有产生扭转。

2. 同步工作和异步工作

JavaScript 是单线程语言,就意味着工作须要排队执行,只有前一个执行实现,后一个才能够执行。

如果前一个工作十分耗时呢?比方操作 IO 设施、网络申请等,前面的工作就会被阻塞,页面就会被卡住,甚至解体,用户体验十分差。

如果 JavaScript 的主线程在遇到这些耗时的工作时,将其挂起,先执行前面的工作,等挂起的工作有后果当前再回头执行,这样就能够解决耗时工作阻塞主线程的问题了。

于是,所有的工作就能够分为两种,同步工作和异步工作,同步工作放在主线程中执行,异步工作被挂起,不进入主线程执行(让主线程阻塞期待),当其有后果了,再放入主线程中执行。

3. 工作队列和 Event Loop

3.1 工作队列

工作队列是一个事件队列,也能够了解成音讯队列,当挂起的异步工作就绪当前就会在工作队列中搁置相应的事件,示意该工作能够进入主线程中执行了。

工作队列中的事件,除了 IO 设施的事件,还有网络申请,鼠标点击、滚动等,只有为事件指定过回调函数,这些事件产生时就会进入工作队列,期待主线程来读取,而后执行相应的回调函数。

回调函数其实就是被挂起来的异步工作,比方:Ajax 申请,申请胜利或失败当前执行的回调函数就是异步工作。

工作队列是一个先进先出的数据结构,排在后面的事件,只有主线程一空,就会优先被读取。

3.2 Event Loop

主线程从工作队列读取事件,这个过程是循环不断的,所以 JavaScript 这种运行机制又称为 Event Loop(事件循环)

4. 宏工作和微工作

异步工作可进一步划分为宏工作和微工作,相应的工作队列也有两种,别离为宏工作队列和微工作队列。

4.1 宏工作

setTimeout、setInterval、setImmediate 会产生宏工作

4.2 微工作

requestAnimationFrame、IO、读取数据、交互事件、UI render、Promise.then、MutationObserve、process.nextTick 会产生微工作

4.3 浏览器中的 JavaScript 脚本执行过程

4.3.1 过程形容

a. JavaScript 脚本进入主线程, 开始执行

b. 执行过程中如果遇到宏工作和微工作,别离将其挂起,只有当工作就绪时将事件放入相应的工作队列

c. 脚本执行实现,执行栈清空

d. 去微工作队列顺次读取事件,并将相应的回调函数放入执行栈运行,如果执行过程中遇到宏工作和微工作,解决形式同 b, 直到微工作队列为空

e. 浏览器执行渲染动作, GUI 渲染线程接管,直到渲染完结

f. JS 线程接管,去宏工作队列顺次读取事件,并将相应的回调函数放入执行栈, 开始下一个宏工作的执行,过程为 b -> c -> d -> e -> f, 如此循环

g. 直到执行栈、宏工作队列、微工作队列都为空,脚本执行完结

4.3.2 示例

4.3.2.1 示例一

// 脚本

console.log(1)

setTimeout(() => {console.log(2)
}, 0)

const p = new Promise((resolve) => {setTimeout(() => {console.log(3)
    resolve()}, 1000)
  console.log(4)
})

p.then(() => {console.log(5)
})

console.log(6)

执行过程

a. 脚本放入执行栈开始履行

b. 执行到 console.log(1), 输出 1

c. 执行到 setTimeout,遇到宏工作,将其挂起,因为延时 0ms,将在 4ms 后在宏工作队列产生一个定时事件, 咱们叫定时 A

d. 程序持续向下执行,执行 new Promise(),并运行其参数,遇到第二个定时工作 ( 宏工作),叫它定时 B,并将其挂起,执行 console.log(4), 输入 4

e. 遇到微工作 p.then(), 将其挂起

f. 向下执行遇到 console.log(6), 输入 6

g. 执行栈清空,读取微工作队列,发现为空,因为 p.then() 含没有就绪,它的就绪依赖与第一个定时工作(定时 A)的执行

h. 执行栈为空,微工作队列为空,执行浏览器的渲染动作

i. 读取宏工作队列,读取第一个就绪的宏工作,为定时工作 A,将其回调函数放入执行栈开始执行,执行 console.log(2),输出 2

j. 执行栈清空,微工作队列为空,渲染

k. 开始执行下一个就绪的宏工作,定时工作 B,并将其回调函数放入执行栈执行,执行 console.log(3), 输入 3,并执行 resolve(),p.then() 就绪,在微工作队列放入相应的事件

o. 执行栈清空,读取微工作队列,发现不为空,读取第一个就绪的事件,并将其对应的回调函数放入执行栈执行,执行 console.log(5),输入 5

p. 执行栈清空,微工作队列为空,渲染,而后发现宏工作队列为空,本次脚本执行彻底完结

输入后果为:1 4 6 2 3 5

4.3.2.2 示例二

async function async1 () {console.log('async1_1')
  await async2()
  console.log('async1_2')
}
async function async2 () {console.log('async2')
}
console.log('script start')
setTimeout(() => {console.log('setTimeout')
}, 0)
async1()
new Promise(resolve => {console.log('promise executor')
  resolve()}).then(() => {console.log('promise then')
})
console.log('script end')

阐明

函数前加 async,实际上返回的是一个 promise,比方这里的 async2 函数,返回的是一个立刻 resoved  promise

await 会将前面的同步代码执行实现(async2),而后让出线程,将异步工作(Promise.then) 挂起,这里的立刻 resolved promise,所以会在微工作队列增加一个事件,且排在上面的 Promise.then 之前

输入后果

如果上一个示例看懂了,再饥饿和该示例的阐明信息,答案就跃然纸上了:

script start => async1_1 => async2 => promise executor => script end => async1_2 => promise then => setTimeout

4.3.3 外链

外链

4.3.4 总结

如果把 JavaScript 脚本也当作初始的宏工作,那么 JavaScript 在浏览器端的执行过程就是这样:

先执行一个宏工作,而后执行所有的微工作

再执行一个宏工作,而后执行所有的微工作

如此重复,执行执行栈和工作队列为空

4.4 node.js 中 JavaScript 脚本的执行过程

JavaScript 脚本执行过程在 node.js 和浏览器中有些不同, 造成这些差别的起因在于,浏览器中只有一个宏工作队列,然而 node.js 中有好几个宏工作队列,而且这些宏工作队列还有执行的先后顺序,而微工作时穿插在这些宏工作之间执行的

4.4.1 执行程序

  各个事件类型, 履行程序自上而下
   ┌───────────────────────┐
┌─>│        timers         │<————— 执行 setTimeout()、setInterval() 的回调
│  └──────────┬────────────┘
|             |<-- 先执行 process.nextTick, 再执行 MicroTask Queue 的回调
│  ┌──────────┴────────────┐
│  │     pending callbacks │<————— 执行由上一个 Tick 提早下来的 I/O 回调
│  └──────────┬────────────┘
|             |<-- 先执行 process.nextTick, 再执行 MicroTask Queue 的回调
│  ┌──────────┴────────────┐
│  │     idle, prepare     │<————— 外部调用(可疏忽)│  └──────────┬────────────┘     
|             |<-- 先执行 process.nextTick, 再执行 MicroTask Queue 的回调
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │ - ( 执行简直所有的回调,除了 close callbacks 以
|  |                       |      |               |     及 timers 调度的回调和 setImmediate() 调度
|  |         poll          |<-----|   connections,|        的回调,在失当的机会将会阻塞在此阶段 )
│  │                       │      |               │ 
│  └──────────┬────────────┘      │   data, etc.  │ 
│             |                   |               | 
|             |                   └───────────────┘
|             |<-- 先执行 process.nextTick, 再执行 MicroTask Queue 的回调
|  ┌──────────┴────────────┐      
│  │        check          │<————— setImmediate() 的回调将会在这个阶段执行
│  └──────────┬────────────┘
|             |<-- 先执行 process.nextTick, 再执行 MicroTask Queue 的回调
│  ┌──────────┴────────────┐
└──┤    close callbacks    │<————— socket.on('close', ...)
   └───────────────────────┘

4.4.2 示例

4.4.2.1 根本示例

console.log(1)

setTimeout(() => {console.log('timer1')
  Promise.resolve().then(() => {console.log('promise1')
  })
}, 0)

setTimeout(() => {console.log('timer2')
  Promise.resolve().then(() => {console.log('promise2')
  })
}, 0)

console.log(2)

这段代码在浏览器中的执行后果为:1 2 timer1 promise1 timer2 promise2

在 node.js 中的执行后果则为:1 2 timer1 timer2 promise1 promise2

4.4.2.2 setTimeout 和 setImmediate 的程序

它们两个程序从上图看不言而喻,timers 队列在 check 队列执行运行,然而有个前提,事件曾经就绪

setTimeout(() => {console.log('timeout')
}, 0)

setImmediate(() => {console.log('immediate')
})

以上代码在 node.js 中的运行后果为:immediate timeout,起因如下:

在程序运行时 timer 事件未就绪,所以第一次去读 timer 队列时,队列为空,持续向下执行,在 check 队列读取到了就绪的事件,所以先执行 immediate,再执行 timeout,因为即便 setTimeout 的延时工夫未 0,然而 node.js 个别会设置为 1ms, 所以,当 node 筹备 Event Loop 的工夫大于 1ms 时,就会先输入 timeout,后输入 immediate,否则先输入 immediate 后输入 timeout

const fs = require('fs')

// 读取文件
fs.readFile('xx.txt', () => {setTimeout(() => {console.log('timeout')
  })

  setImmediate(() => {console.log('immediate')
  })
})

以上代码的输入程序肯定为:immediate timeout,起因如下:

setTimeout 和 setImmediate 都写在 I /O callback 中,意味着处于 poll 阶段,而后是 check 阶段,所以,此时无论 setTimeout 就绪多快(1ms),都会优先执行 setImmediate,实质上,从 poll 阶段开始执行,而不是一个 Tick 初始阶段。

正文完
 0