Simonzhangs' blog Simonzhangs' blog
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • JS设计模式总结
  • 《Vue》
  • 《React》
  • 《TypeScript 从零实现 axios》
  • TypeScript
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • apple music
  • extension
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Simonzhangs

前端学习探索者
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • JS设计模式总结
  • 《Vue》
  • 《React》
  • 《TypeScript 从零实现 axios》
  • TypeScript
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • apple music
  • extension
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • HTML

  • CSS

  • JavaScript文章

    • 33个非常实用的JavaScript一行代码
    • new命令原理
    • ES5面向对象
    • ES6面向对象
    • 多种数组去重性能对比
    • JS随机打乱数组
    • 判断是否为移动端浏览器
    • 将一维数组按指定长度转为二维数组
    • 防抖与节流函数
    • JS获取和修改url参数
    • 比typeof运算符更准确的类型判断
    • 常见面试题

      • 四级文件(测试)
      • 原型与原型链
      • JS数据类型
      • call、apply、bind
      • JavaScript运行机制
        • JavaScript 单线程语言
          • 同步与异步
          • 单线程
          • process.nextTick 和 setImmediate
        • JavaScript 的事件循环(eventloop)
          • 宏任务与微任务
        • JS 执行顺序总结
        • 扩展-浏览器的进程
          • GUI 线程
          • JS 引擎线程
          • 定时器线程
          • 事件触发线程
          • 异步 HTTP 请求线程
        • Event Loop
          • 浏览器的 Event Loop
          • 定时器不准
          • 微任务
          • 宏任务
          • 微任务
        • 补充
          • 重绘重排
          • 进程与线程
      • promise
      • JS中在new的时候发生了什么
  • 学习笔记

  • 前端
  • JavaScript文章
  • 常见面试题
simonzhangs
2022-04-16
目录

JavaScript运行机制

JavaScript 是单线程的,当有异步任务时候运行结果会是什么样的?理解 JS 的运行机制很重要,在大型项目中应用需要理解透。

先看这段代码会输出什么结果:

console.log("script start");
setTimeout(function () {
  console.log("setTimeout");
}, 0);

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

console.log("script end");
1
2
3
4
5
6
7
8
9
10
11
12
13

执行结果为:

script start
promise1
script end
promise2
setTimeout
1
2
3
4
5

原因: 首先,new Promise 是同步任务,会被放到主进程中去立即执行(当作立即执行函数去理解);而 then()函数是异步任务,会放到异步队列中去,当这个 promise 状态结束(执行 reject 或 resolve)的时候,就会立即放到异步队列中去。

上述代码包括 promise 和 setTimeout 的执行顺序,如果加上 async/await 呢,看下面代码:

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");
}, 0);
async1();
new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});

console.log("script end");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

先看结果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
1
2
3
4
5
6
7
8

JS 执行机制:

  1. 从上到下执行所有同步代码;
  2. 在执行过程中,遇到宏任务就放到宏任务队列中;遇到微任务就放到微任务队列中;
  3. 当所有同步代码执行完毕之后,就执行微任务队列中满足需求的所有回调;
  4. 当微任务队列所有满足需求回调执行完毕之后,就执行宏任务队列中满足需求的所有回调。

在上面例子中:

  1. new Promise 是同步任务;
  2. Promise.then() 是微任务,此外 process.nextTick 是 node 中的微任务;
  3. 整体代码 script、setTimeout、setInterval 是宏任务;
  4. async 关键字函数会返回一个 promise 对象,如果里面没有 await,执行起来等同于普通函数;
  5. 如果有 await 关键字(await 要在 async 关键字函数内部),await 就会让出线程,阻塞 async 内后续代码,先去执行 async 外的代码,等外面同步代码执行完毕,才会执行后续的代码。
  6. await 如果等的是 promise 对象,则会阻塞后面的代码,等 promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

# JavaScript 单线程语言

JavaScript 是单线程语言,单线程意味着,所以的任务都需要排队,前一个任务结束才会执行下一个任务,如果前面任务耗费的时间很长,那后面的任务就得一直等着,这样的话会造成资源分配的浪费。所以为了规避这个问题,JavaScript 把所有任务分为两种:同步任务和异步任务。

  • 同步任务:在主线程上排队的任务,前一个任务执行完毕,才能执行下一个任务;
  • 异步任务:不进入主线程,是进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,任务才会进入主线程执行。

# 同步与异步

异步:

  • 计时器
  • ajax
  • 读取文件

同步程序执行完成后,执行异步程序;

console.log(1);
setTimeout(() => {
  console.log(2);
}, 1000);
setTimeout(() => {
  console.log(3);
}, 100);
setTimeout(() => {
  console.log(4);
}, 1);
console.log(5); // 1 5 4 3 2
1
2
3
4
5
6
7
8
9
10
11

# 单线程

以下列代码举例子:

for (let i = 0; i < 2000; i++) {
  console.log(1);
}
setTimeout(() => {
  console.log(2);
}, 1000);
setTimeout(() => {
  console.log(3);
}, 100);
setTimeout(() => {
  console.log(4);
}, 1);
console.log(5);
1
2
3
4
5
6
7
8
9
10
11
12
13

上述代码会先输出 2000 个 1,再输出 5 4 3 2,则计时器不准了,说明了 JS 是单线程的,一个任务完成后才能执行另一个任务;

# process.nextTick 和 setImmediate

nextTick 方法会在同步代码执行之后,异步执行之前执行。

setImmmediate 方法在异步代码执行之后执行。

# JavaScript 的事件循环(eventloop)

先执行同步操作,异步操作排在事件队列里。

  1. 先判断是同步还是异步任务,同步任务进入主线程,异步任务就进入 event table;
  2. 异步任务在 event table 中注册事件,当满足触发条件的时候,会被推入到 event queue;
  3. 同步任务进入到主线程中执行,当主线程空闲的时候,才会去 event queue 中看是否有需要执行的异步任务,如果有,就推入主线程执行。

# 宏任务与微任务

同步任务:new Promise 内为同步任务。 异步任务分为:宏任务和微任务。 宏任务包括:setTimeout、setInterval、整体代码 script 微任务包括:Promise.then()

执行顺序:先执行微任务然后执行宏任务。

# JS 执行顺序总结

  1. 同步先执行,异步后执行;
  2. 遇到 new Promise 直接执行,then 中的方法直接放入微任务队列;
  3. 遇到 setTimeout 放入宏任务队列中;
  4. 执行顺序:同步任务 -- 微任务(promise.then) -- 宏任务(setTimeout)

参考链接:

关于 async/await、promise 和 setTimeout 的执行顺序

setTimeout+Promise+Async 输出顺序?很简单呀!

setTimeout 和 setImmediate 到底谁先执行,本文让你彻底理解 Event Loop

# 扩展-浏览器的进程

JS 执行的是单线程的,而 JS 执行是在浏览器中,占用的是浏览器中的一个线程。浏览器不仅是多线程的,而且还是多进程的。

浏览器进程

JS 执行的在浏览器的渲染进程中的 JS 引擎线程完成的。渲染进程也不止一个,每个选项卡都有自己的渲染进程,这样单页页面崩溃不会导致浏览器崩溃。

渲染进程是前端主要用到的,其他包括以下进程:

# GUI 线程

GUI 线程负责渲染页面的,用来解析 HTML 和 CSS,然后将他们构建 DOM 树、CSSOM 树,还有渲染树,都是 GUI 线程负责的。

# JS 引擎线程

JS 引擎线程就是负责执行 JS 的主线程,JS 的单线程指的就是这个线程,也就是我们所说的 Chrome V8 引擎是在这个线程运行的。

JS 引擎线程和 GUI 线程是互斥。因为 JS 是可以操作 DOM 的,如果 JS 线程和 GUI 线程同时操作 DOM,就比较混乱,不知道到底渲染哪个结果,所以说如果 JS 长时间运行,GUI 线程就会被阻塞,整个页面感觉像卡死了。

# 定时器线程

JS 定时器方法有 setTimeout、setInterval,它们都运行在定时器线程,跟 JS 主线程根本不在一个地方,所以 JS 作为单线程语言,能够实现异步。

# 事件触发线程

定时器线程其实只是一个计时的作用,他并不会真正执行时间到了的回调,真正执行这个回调的还是 JS 主线程。

当时间到了定时器会将这个回调事件给到事件触发线程,然后事件触发线程将它加到事件队列里面去,最终 JS 主线程从事件队列取出这个回调执行。

事件触发线程不仅会将定时器事件放入任务队列,其他满足条件的事件也是他负责放进任务队列。

# 异步 HTTP 请求线程

这个线程负责处理异步的 ajax 请求,当请求完成后,也会通知事件触发线程,然后事件触发线程将这个事件放入事件队列给主线程执行。

总之,JS 异步的实现实际是靠的是浏览器的多线程,当他遇到异步 API 时候,就将这个任务交给对应的线程;当这个异步 API 满足回调条件时,对应的线程又通过事件触发线程将这个事件放入任务队列,然后主线程从任务队列取出事件继续执行。这里说的任务队列就是 Event Loop,目前 JS 的运行环境有两个:浏览器和 Node.js,两者 event loop 有不同的地方。

# Event Loop

# 浏览器的 Event Loop

事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,就是事件队列。各个异步线程执行完了之后,通过事件触发线程就回调事件放到事件队列,主线程每次干完手上的活儿就来看这个队列有没有新活,有的话就取出来执行。

浏览器event loop

流程如下:

  1. 主线程每次执行的时候,先看看要执行的是同步任务,还是异步的 API;
  2. 同步任务就继续执行,一直执行完;
  3. 遇到异步 API 就将他交给对应的异步线程,自己继续执行同步任务;
  4. 异步线程执行异步 API,执行完后,将异步回调时事件通过事件触发线程放入事件队列中;
  5. 主线程手上的同步任务完成后就来看事件队列里面有没有任务;
  6. 主线程发现事件队列有任务,就取出里面的任务执行;
  7. 主线程不断循环上述流程。

# 定时器不准

因为定时器是到了时间会经过事件触发线程放入事件队列中,并且等到主线程执行完之后再来执行,如果主线程被阻塞时间超过定时器时间的话,定时器就不准了。因此要主要不要过长时间占用主线程。

# 微任务

事件队列里面的事件分为两类:宏任务和微任务。其中微任务拥有更高的优先级,当事件循环遍历队列时,会先检查微任务队列,如果有任务,就全部拿来执行,执行完毕之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。

常见的宏任务和微任务有:

# 宏任务

# 浏览器 Node
I/O ✅ ✅
setTimeout ✅ ✅
setInterval ✅ ✅
setImmediate ❌ ✅
requestAnimationFrame ✅ ❌

# 微任务

# 浏览器 Node
Promise.prototype.then catch finally ✅ ✅
process.nextTick ❌ ✅
MutationObserver ✅ ❌

# 补充

# 重绘重排

浏览器的渲染进程中,主线程是用来进行 DOM 解析渲染和 JS 解析运行的,还有合成器线程,不是在主线程中的,将渲染的结果通过 IPC 传递给浏览器进程,然后传给 GPU 进行绘制。

如果进行大量的重绘重排的话,会影响性能。

此外如何避免动画卡顿,人眼在 60Hz 刷新率的情况下看不到卡顿,但是如果因为 JS 执行在主线程上的,如果 JS 阻塞了话,会影响合成器线程渲染,导致页面看上去卡顿:

  • 可以通过 requestAnimationFrame 来将 JS 切割在每帧空余的时间运行,避免影响渲染;
  • 通过 tranform 来实现动画,因为 tranform 动画不会占用主线程的绘制计算,而是在合成器线程进行的。

关于重绘和重排,可以看之前总结过的文章:浏览器渲染过程 (opens new window)

# 进程与线程

进程是程序的一次执行过程,是系统运行程序的基本单位,所以说进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

比如说,浏览器就是一个应用,每个 tab 栏创建都是多个进程共同运行的结果,前端我们主要关注的是渲染进程,其中又包括多个线程。

线程又是什么呢?线程与进程类似,但是线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同类的多个线程共享进程的堆和方法区资源。同一进程中的线程极有可能会相互影响。

比如说,在 JS 执行环境中,任务队列的事件的受到事件触发线程的影响,当满足条件时,将事件推入任务队列当中。

较为官方的术语是:

  • 进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
编辑 (opens new window)
上次更新: 2022/06/16, 10:52:37
call、apply、bind
promise

← call、apply、bind promise→

最近更新
01
一些有意思的类比
06-16
02
the-super-tiny-compiler解析
06-06
03
计算机编译原理总概
06-06
更多文章>
Theme by Vdoing | Copyright © 2021-2022
蜀ICP备2021023197号-2
Simonzhans | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜