gzl的博客

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

JS全面梳理EventLoop

发表于 2019-09-11 更新于 2019-12-15 分类于 JavaScript

https://juejin.im/post/5d5b4c2df265da03dd3d73e5

CPU、进程、线程之间的关系

计算机的核心是CPU,它承担了所有的计算任务。

它就像一座工厂,时刻在运行。

假定工厂的电力有限,一次只能供给一个车间使用。 也就是说,一个车间开工的时候,其他车间都必须停工。 背后的含义就是,单个CPU一次只能运行一个任务。

进程就好比工厂的车间,它代表CPU所能处理的单个任务。 进程之间相互独立,任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 CPU使用时间片轮转进度算法来实现同时运行多个进程。

一个车间里,可以有很多工人,共享车间所有的资源,他们协同完成一个任务。

线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源。

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
  • 不同进程之间也可以通信,不过代价较大
  • 单线程与多线程,都是指在一个进程内的单和多

浏览器是多进程的

对于计算机来说,每一个应用程序都是一个进程, 而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现的。 对于这种子进程的扩展方式,我们可以称这个应用程序是多进程的。

而对于浏览器来说,浏览器就是多进程的,在Chrome浏览器中打开多个tab,然后打开windows控制管理器可以看到一个Chrome浏览器启动了好多个进程。

总结一下:

  • 浏览器是多进程的
  • 每一个Tab页,就是一个独立的进程

浏览器包含了哪些进程

主进程

  • 协调控制其他子进程(创建、销毁)
  • 浏览器界面显示,用户交互,前进、后退、收藏
  • 将渲染进程得到的内存中的Bitmap,绘制到用户界面上
  • 处理不可见操作,网络请求,文件访问等

第三方插件进程

  • 每种类型的插件对应一个进程,仅当使用该插件时才创建

GPU进程

  • 用于3D绘制等

渲染进程,就是我们说的浏览器内核

  • 负责页面渲染,脚本执行,事件处理等
  • 每个tab页一个渲染进程

那么浏览器中包含了这么多的进程,那么对于普通的前端操作来说,最重要的是什么呢?

答案是渲染进程,也就是我们常说的浏览器内核

浏览器内核(渲染进程)

进程和线程是一对多的关系,也就是说一个进程包含了多条线程。

而对于渲染进程来说,它当然也是多线程的了,接下来我们来看一下渲染进程包含哪些线程。

  • GUI渲染线程

    • 负责渲染页面,布局和绘制
    • 页面需要重绘和回流时,该线程就会执行
    • 与js引擎线程互斥,防止渲染结果不可预期
  • JS引擎线程

    • 负责处理解析和执行javascript脚本程序
    • 只有一个JS引擎线程(单线程)
    • 与GUI渲染线程互斥,防止渲染结果不可预期
  • 事件触发线程

    • 用来控制事件循环(鼠标点击、setTimeout、ajax等)
    • 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
  • 定时触发器线程

    • setInterval与setTimeout所在的线程
    • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
    • 计时完毕后,通知事件触发线程
  • 异步http请求线程
    • 浏览器有一个单独的线程用于处理AJAX请求
    • 当请求完成时,若有回调函数,通知事件触发线程

为什么 javascript 是单线程的

首先是历史原因,在创建 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。

其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。

而且,如果同时操作 DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期。

为什么 GUI 渲染线程与 JS 引擎线程互斥

这是由于 JS 是可以操作 DOM 的,如果同时修改元素属性并同时渲染界面(即 JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就可能不一致了。

因此,为了防止渲染出现不可预期的结果,浏览器设定 GUI渲染线程和JS引擎线程为互斥关系, 当JS引擎线程执行时GUI渲染线程会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行。

宏任务、微任务

宏任务

我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。

我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

1
// 宏任务-->渲染-->宏任务-->渲染-->渲染...

主代码块,setTimeout,setInterval等,都属于宏任务

例一

1
2
3
4
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';

会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。

这里存在的疑问就是:这里的DOM操作是属于宏任务还是微任务?经过思考加上读了 JavaScript 忍者秘籍,DOM 操作应该是微任务,如果要是宏任务,执行一个宏任务就渲染一次,上面的代码就会变四次颜色了。

例二

1
2
3
4
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)

会看到,页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。

微任务

我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。

也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。

Promise,process.nextTick等,属于微任务。

例一

1
2
3
4
5
6
7
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);

控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行,所以 2 最后输出

页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了黑色,然后才执行的渲染

例二

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)

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

// print : 1 3 2

上面代码共包含两个 setTimeout ,也就是说除主代码块外,共有两个宏任务, 其中第一个宏任务执行中,输出 1 ,并且创建了微任务队列,所以在下一个宏任务队列执行前, 先执行微任务,在微任务执行中,输出 3 ,微任务执行后,执行下一次宏任务,执行中输出 2

总结

执行一个宏任务(栈中没有就从事件队列中获取)

执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

task2

RN-《深入浅出nodejs》

发表于 2019-09-10 更新于 2020-02-06 分类于 读书笔记

第一章 Node简介

异步I/O

在Node中,绝大多数的操作都以异步的方式进行调用,从文件读取到网络请求。这样的意义在于,在Node中,我们可以从语言层面很自然地进行并行I/O操作。每个调用之间无须等待之前的I/O调用结束。

下面的两个文件读取任务的耗时取决于最慢的那个文件读取的耗时:

1
2
3
4
5
6
fs.readFile('/path1', function (err, file) {
console.log('读取文件1完成');
});
fs.readFile('/path2', function (err, file) {
console.log('读取文件2完成');
});

而对于同步I/O而言,它们的耗时是两个任务的耗时之和。

事件与回调函数

事件的编程方式具有轻量级、松耦合、只关注事务点等优势。

纵观下来,回调函数也是最好的接受异步调用返回数据的方式。

第二章 模块机制

CommonJS模块规范

主要分为模块引用、模块定义、模块标识3个部分。

模块引用

1
let math = require('math')

模块定义

1
2
3
exports.add = function () {
console.log("add函数");
}

模块标识

模块标识其实就是传递给 require() 方法的参数,它必须是符合小驼峰命名的字符串,或者以 . .. 开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。

Node的模块实现

在Node中引入模块,需要经历如下3个步骤。

(1)路径分析 (2)文件定位 (3)编译执行

在Node中,模块分为两类,一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。

优先从缓存加载

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的,不同之处在于核心模块的缓存检查先于文件模块的缓存检查。

路径分析和文件定位

模块标识符分析

核心模块

文件定位

目录分析和包

JS深入prototype

发表于 2019-09-10 分类于 JavaScript

直接上图

prototype

下面是两个不太注意的点

constructor

1
2
3
4
5
function Person() {

}
let person = new Person();
console.log(person.constructor === Person); // true

当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

1
person.constructor === Person.prototype.constructor

__proto__

其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

总结

摘自评论区

1
2
3
4
5
6
7
8
9
10
11
function Person() {

}

let person = new Person();

console.log(person.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
console.log(Object.constructor === Function); // true

实例对象的隐式原型始终指向构造函数的显式原型;

只有函数才拥有prototype属性,每一个对象都拥有__proto__属性;

每一个原型对象都有一个constructor属性指向它们的构造函数;

原型链的链接依赖__proto__这个指针,每一级的原型对象都依靠这个指针指向上一级原型对象;

原型链的尽头始终是null;

原型对象充当两面派,既是构造函数函数,也是对象;

参考

JavaScript深入之从原型到原型链

1…121314…32

gzl

96 日志
14 分类
37 标签
© 2020 gzl
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Pisces v7.2.0