js运行机制、js事件循环、js延迟加载的方式

17 阅读6分钟

js运行机制

Javascript 是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线 程就是阻塞,而实现单线程非阻塞的方法就是事件循环

  1. 首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行(即事件队列(event queue)
  2. 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
  3. 当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行
  4. 任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行
  5. 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
线程、进程
  • 线程是最小的执行单元
  • 进程是最小的资源管理单元
  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程

事件循环(event loop)

js代码执行过程中,所有的任务都可以分为:

  • 同步任务立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务异步执行的任务,比如ajax网络请求,setTimeout, 定时函数等

同步任务与异步任务的运行流程图如下:

image.png

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执 行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

宏任务与微任务

异步任务还可以细分为微任务(microtask)与宏任务(macrotask)。js引擎会优先执行微任务

对于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件 会优先被主线程读取,

微任务

一个需要异步执行的函数,执行时机是主函数执行结束之后,当前宏任务结束之前。

常见的微任务:

  • promise.then 的回调
  • Node.js 中的 process.nextTick(process.nextTick指定的异步任务总是发生在所有异步任务之)
  • 对 Dom 变化监听的 MutationObserver
宏任务

宏任务的时间粒度比较大,执行的时间问隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script脚本的执行(可以理解为外层同步代码)
  • setTimeoutsetlntervalsetImmediate 一类的定时事件
  • UI rendering/U事件(UI渲染)
  • postMessage, MessageChannel
  • I/0 (Node.js)

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

image.png

按照这个流程,它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

小题:

console.log(1)

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

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

执行结果: 1 new Promise 3 then 2


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

new Promise(function(resolve, reject) {
  console.log(2);
  resolve()
}).then(function() {
  console.log(3) 
});

process.nextTick(function () {
  console.log(4)
})
console.log(5)

第一轮:主线程开始执行,遇到setTimeout,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick两个微任务,先执行哪个呢?process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。

执行结果: 2 5 4 3 1

async与await

async用来声明一个异步的方法,而await是用来等待异步方法的执行。

async函数返回一个promise对象,以下两种方法是等效的:

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!

async function asyncF() {
    return 'TEST';
}

await 正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值

async function f(){
    // 等同于
    // return 123
    return await 123
}

f().then(v => console.log(v)) // 123

不管await后面跟着什么,await都会阻塞后面的代码,使后面的代码加入微任务队列。

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) 
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

执行结果:1 fn2 2 3

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')

}

async function async2() {
    console.log('async2')
}

console.log('script start')

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

async1()

new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})

console.log('script end')

执行结果:

  • 'script start'
  • 'async1 start'
  • 'async2'
  • 'promise1'
  • 'script end'
  • 'async1 end'
  • 'promise2'
  • settimeout
  1. 执行整段代码,遇到 console.log('script start")直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印async1 start,下面遇到await 怎么办?先执行 async2,打印async2,然后阻塞下面代码(即加入微任务列表),跳出去执行 同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到•then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即await 下面的代码,打印async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise 2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

JS延迟加载的方式

JavaScript 是单线程(js不走完下面不会走是因为同步)会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。

  1. 把JS放在页面的最底部
  2. script标签的defer属性:脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)
  3. 是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行, 该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)
  4. 动态创建script标签,监听dom加载完毕再引入js文件