JavaScript的进程和线程

一、js线程

之所以js中有事件循环,原因就是因为js是单线程的原因

1. 进程与线程的关系和区别?JS 单线程带来的好处?

一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

注:浏览器是多进程、多线程的,JS是单线程的
浏览器每个标签页是一个进程,每个进程里同时有js线程、网络线程、渲染线程等


在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。这其实也是一个单线程的好处,得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间,没有锁的问题的好处。

对于锁的问题,形象的来说就是当我读取一个数字 15 的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这个问题也不难,只需要在读取的时候加锁,直到读取完毕之前都不能进行写入操作。z这就是锁

2. 同步和异步函数

2.1 同步函数:函数返回—>拿到预期结果

Math.sqrt(2)
console.log('Hi')

2.2 异步函数:函数返回—>拿不到预期结果,还要通过一定的手段获得结果

// 这是node 里面的函数
fs.readFile('foo.txt', 'utf8', function(err, data){
    console.log(data)
}
//该函数需要在全部文件都读取完之后才能console
异步过程:
  1. 注册函数:发起异步过程
  2. 回调函数:处理结果
异步类型:
  1. 普通事件:click、resize
  2. 资源加载:load、error
  3. 定时器:setInterval、setTimeout

二、事件循环

1. 什么是执行栈

可以把执行栈认为是一个存储同步函数调用的栈结构,遵循先进后出的原则。

当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()
image.png

可以在上图清晰的看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的。

爆栈

当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题

function bar() {
  bar()
}
bar()
image.png

2. 消息队列

对于同步任务,按照顺序既可,但是根据异步函数执行时间长短不一样,所以就有了消息队列
消息指的是–>注册异步任务时添加的回调函数

js遇到异步函数时,不会一直等待其结果,而是挂起,继续执行执行栈中的任务;只要异步操作执行完成,就可以到消息队列中去排队,然后主线程空闲的时候,就可以从消息队列中获取消息并执行其回调

三、事件循环

以一个代码为例:

div.onclick = () => {
    console.log('hi!')
}
  1. js引擎解析到该段代码时,将onclick这个函数挂起
  2. 如果点击了这个div,就是异步操作执行完成,那么就将这个函数放入消息队列中
  3. 当执行栈处于闲置状态时,就从消息队列中取出该任务对应的回调放进执行栈中执行

四、 异步代码执行顺序?

再次重复一下:
当遇到异步的代码时,会被挂起并在异步操作完成时加入到 消息队列中。一旦执行栈为空,js引擎就会从 消息 队列中拿出该异步函数的回调并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

image.png

1. 异步任务的分类?

不同的任务源会被分配到不同的 消息队列中,任务源可以分为 微任务(microtask) 和 宏任务

  • 宏任务:由宿主对象发起的任务(setTimeout)
    • 宏任务包括 script, setTimeout ,setInterval ,setImmediate ,I/O,UI rendering。
  • 微任务:由js引擎发起的任务(promise)
    • 微任务包括 process.nextTick,promise ,MutationObserver。

宿主对象:
由js 宿主环境提供的对象,他们的行为完全由宿主环境决定

  • 固有(new image 创建img 元素)
  • 用户可创建的(document.createElement就可以创建Dom)

五、Event Loop (事件循环)执行顺序如下所示:

  1. 在异步事件执行完操作后会放入一个执行队列里,而根据这个异步事件的类型会被放入对应的宏任务队列或者微任务队列中
  2. 当执行栈为空时,主线程会先去执行微任务队列中对应的回调函数,再去执行宏任务队列中的任务(当然是取出放入执行栈中执行)
  3. 在一次循环中,微任务永远在宏任务之前执行

heap 堆(主要用于内存分配)

  • 对象

stack 执行栈/方法调用栈(先进后出)

  • 当JS引擎执行函数时,会把函数按照执行顺序放入stack 执行栈,并按照执行完毕的顺序从执行栈里移除。(先进后出)
  • 如果一直在调用函数而没有结束(自调用死循环),执行栈容量会达到上限,报错。 这就是 爆栈

stack queue 任务队列(异步)

  • 如果调用到异步函数,会把异步函数先放入stack queue 任务队列,继续执行stack 执行栈里的同步函数。当执行栈的函数全部执行完毕并移除,再把任务队列里的异步函数按照加入任务队列的先后顺序放入执行栈,继续执行。
  • JS中用于储存待执行回调函数的队列包含两个不同特定的列队:微队列、宏队列。

microtask 微任务(优先级高)

  • promise,process.nextTick,Object.obverse,MutationObserver

macrotask 宏任务(优先级低)

  • 定时器,setImmediate,I/O(键盘、网络),UI rending

事件循环过程

  1. 执行全局 JS 同步代码,有的是同步语句,有的是异步语句(如setTimeout等)。放入 stack 执行栈;
  2. Event Loop 事件循环 不断检查 stack 执行栈 是否为空;
  3. 为空时检查 task queue 任务队列(微队列、宏队列) 是否有异步任务, 如果有则开始执行;
  4. 在本次循环中,取出 microtask queue 微队列 中第一个 microtask 微任务,放入 stack 执行栈 中执行,完成后 microtask queue 微队列 长度减1;
  5. 继续取出 microtask queue 微队列 中第一个 microtask 微任务,放入 stack 执行栈 中执行,以此类推,直到把 microtask queue 微队列 清空;

注意:如果在执行 microtask 微任务 的过程中,又产生了新的 microtask 微任务 ,会加入到队尾,也在本次循环中执行;

  1. 取出 macrotask queue 宏队列 中第一个 macrotask 宏任务,放入 stack 执行栈 中执行;

即 所有微任务 microtask + 一个宏任务 macrotask 。(所以多个网络请求可以同时处于等待状态)

7.执行完毕后,调用栈Stack为空;
8.重复 Event Loop 事件循环 (第2-7步)

你可能感兴趣的:(JavaScript的进程和线程)