node 基础与 Event Loop

node 的特色

  • 主线程是单线程
  • 异步非阻塞(非阻塞I/O)
  • 事件驱动

单线程javascript

  • 进程和线程

进程是操做系统分配资源和调度任务的基本单位,线程是创建在进程上的一次程序运行单位,一个进程上能够有多个线程。java

  • 什么是单线程?

一个进程中只有一个线程,程序顺序执行,前面的执行完成后才会执行后面的程序。node

有点儿像点菜同样, 顾客来了。服务员接单,服务员接完单后立刻告诉厨房去作菜吧,此时服务员不会等待菜作好而是立刻会被释放出来继续服务下一个客户。 一直都是一个服务员在工做,等菜作好了,服务员在回去取菜给客户吃。ajax

单线程特色是节约内存,而且不须要再切换执行上下文,并且单线程不须要考虑锁的问题。数据库

以下图api

image

异步非阻塞promise

  • 同步和异步

同步和异步关注的是消息通知机制,指代的是被调用方浏览器

同步就是发出调用后,没有获得结果前,该调用不返回,一旦调用返回,就获得返回值bash

当一个异步过程调用发出后,调用者不会马上获得结果,而是调用发出后,被调用者经过状态、通知或者回调函数处理这个调用。服务器

  • 阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)的状态,针对的是调用者

阻塞调用是指调用结果返回以前,当前线程会被挂起。调用线程只有在获得结果后才会返回

非阻塞调用指在不能马上获得结果前,该调用不会阻塞当前线程

a>  同步阻塞
    调用者:我喜欢你 
    被调用者:我也喜欢你
    
b>  异步阻塞
    调用者:  我喜欢你
    被调用者:我和我妈商量下。回头回复你
    调用者:  不挂断电话,我等你回复
    
c>  异步非阻塞
    调用者: 我喜欢你
    被调用者: 我和我妈商量下。回头回复你
    调用者:  挂断电话,不等你了。联系了另一个妹子
    
d>  同步非阻塞
    调用者:给 A 打电话 我喜欢你
    被调用者(A):正准备回复
    调用者:不挂断电话 A 转身又给 B 打电话 我喜欢你
复制代码
  • I/O 操做

    • 访问服务器的静态资源

    • 读取数据,读取文件

node 在处理高并发, I/O密集场景有明显优点。高并发是指在同一时间并发访问服务器,I/O密集知道是文件操做、网络操做、数据库。相对的有 CPU 密集,CPU 密集值的是逻辑处理运算、压缩、解压、加密、解密等。 但是菜作好了,如何告诉服务员呢? ==回调==

事件驱动

谈谈浏览器中的Event Loop

image

  • 渲染引擎

渲染引擎内部是多线程的,包括 UI 线程和 JS 线程。注意 UI 线程和 JS 线程是互斥的,由于 JS 运行结果会影响到 UI 线程的结果。 UI 更新会被保存在任务队列中等 JS 线程空闲时候当即被执行。

  • JS 单线程(主线程)

JS 在最初为何被设计成了单线程,而不是多线程呢?若是多个线程同时操做 DOM 那岂不是会很混乱?

  • 其余线程

    • 浏览器事件触发线程(用来控制事件循环、存放seTimeout、浏览器事件、ajax回调)
    • 定时触发器线程(setTimeout)
    • 异步 HTTP 请求线程(ajax请求线程)

浏览器中的 Event Loop

来个经典的图

image

  • 全部同步任务都在主线程上执行,造成一个执行栈
  • 主线程以外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中放一个事件
  • 一旦执行栈中的全部同步任务执行完毕,系统就会读取任务队列,将任务队列中的事件放到执行栈中依次执行
  • 主线程从任务队列中读取事件,这个过程是循环不断的

栈内存 / 堆内存

javascript 中的变量分为基本类型和引用类型。基本类型是保存在栈内存中,引用类型则指的是保存在堆内存中的对象

image

任务队列

  • 任务队列 先进先出

宏观任务(MacroTask):

setTimeout
setInterval
setImmediate(只兼容IE)
MessageChannel
requestAnimationFrame
I/O
UI rendering 
复制代码

微观任务(MicroTask):

process.nextTick
Promise
Object.observe(已废弃)
MutationObserver
复制代码
  • 栈 先进后出

好比: 函数的执行栈,做用域的释放顺序。

放进去的顺序: 全局做用域 <= one <= two <= three

函数 three 没有执行完,函数 one 是不会被释放的。函数销毁的顺序则是 three => two => one => 全局

image

function one () {
  let a = 1;
  two();
  function two() {
    console.log(a);
    let b = 2;
    function three () {
      debugger;
      console.log(b);
    }
    three();
  }
}

one();
复制代码

断点调试下以下:进入的顺序和出去的顺序是相反的。

image

例子-1:

// 栈中的代码执行完毕后,会调用队列中的代码,此过程不挺的循环
// 当 1000 毫秒到达的时候 setTimeout 才会被放到任务队列里去
console.log(1);
setTimeout(() => {
  console.log(2);
}, 1000)

setTimeout(() => {
  console.log(3);
}, 500)

// 1 3 2
复制代码

例子-2:

console.log(1);
setTimeout(() => {
  console.log(2);
}, 1000)
while(true) {}
setTimeout(() => {
  console.log(3);
}, 500)
复制代码

此时不会输出 2 和 3。是由于 while 是个死循环。当时间到达时,要看栈中是否已经执行完了,若是没有执行完,就不会调用队列中的内容

例子-3:

console.log('global')
for (var i = 1;i <= 5;i ++) {
  setTimeout(function() {
    console.log('setTimeout1:', i)
  },i*1000)
  console.log(i)
}

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

setTimeout(function () {
  console.log('timeout2')
  new Promise(function (resolve) {
    console.log('timeout2_promise')
    resolve()
  }).then(function () {
    console.log('timeout2_then')
  })
}, 1000)
// 输出结果
// global
// 1 
// 2
// 3
// 4
// 5
// promise1
// then1
// setTimeout1: 6
// timeout2
// timeout2_pormise 
// timeout2_then 
// setTimeout1: 6 
// setTimeout1: 6 
// setTimeout1: 6 
// setTimeout1: 6
// setTimeout1: 6 

复制代码

先执行主线程中的任务输出 global 和 for 循环中的 i。setTimeout 属于宏观任务,时间到了会放到宏任务队列中,setTimeout1 会根据 i * 1000 依次放入到宏任务队列中。Promise 构造函数中的执行器属于同步任务,会先输出 promise1, 调用 resolve 后改变了 Promise 状态,调用 then 方法会将任务放入微任务队列中。此时 setTimeout2 时间到会被放入宏任务队列中。timeout2_promise 也会根据promise1的执行过程进入到微任务队列。

每次 Event Loop 触发执行的过程是:

A> 执行主线程中的任务,调用栈为空

B> 取出==全部== micro-task 任务队列 => 执行

C> 取出==一个== macro-task 任务 => 执行

D> 取出==全部== micro-task 任务队列 => 执行

E> 重复 C 和 D

node 系统中的 Event Loop

image

  • js 代码会交给 V8 引擎进行处理
  • 代码中用到的 node api 会交给 libuv 库处理
  • libuv 经过阻塞 i/o 和多线程实现异步 io
  • 经过事件驱动方式,将结果放到事件队列中,最终交给个人应用
相关文章
相关标签/搜索