JS基础总结(5)—— JS执行机制与EventLoop

4,363 阅读8分钟

前言

农历2019已经过去,趁着上班前这段时间,整理了一下javascript的基础知识,在此给大家做下分享,喜欢的大佬们可以给个小赞。 本人github: github.com/Michael-lzg

相关文章:

js 是一门单线程语言。 js 引擎有一个主线程(main thread)用来解释和执行 js 程序,实际上还存在其他的线程。例如:处理 ajax 请求的线程、处理 DOM 事件的线程、定时器线程、读写文件的线程(例如在 node.js 中)等等。这些线程可能存在于 js 引擎之内,也可能存在于 js 引擎之外,在此我们不做区分。不妨叫它们工作线程。

JS 执行上下文

当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。

例子 1: 变量提升

foo // undefined
var foo = function() {
  console.log('foo1')
}

foo() // foo1,foo赋值

var foo = function() {
  console.log('foo2')
}

foo() // foo2,foo重新赋值

例子 2:函数提升

foo() // foo2
function foo() {
  console.log('foo1')
}

foo() // foo2

function foo() {
  console.log('foo2')
}

foo() // foo2

例子 3:声明优先级,函数 > 变量

foo() // foo2
var foo = function() {
  console.log('foo1')
}

foo() // foo1,foo重新赋值

function foo() {
  console.log('foo2')
}

foo() // foo1

运行环境

在 JavaScript 的世界里,运行环境有三种,分别是:

  1. 全局环境:代码首先进入的环境
  2. 函数环境:函数被调用时执行的环境
  3. eval 函数:(不常用)

执行上下文特点

  1. 单线程,在主进程上运行
  2. 同步执行,从上往下按顺序执行
  3. 全局上下文只有一个,浏览器关闭时会被弹出栈
  4. 函数的执行上下文没有数目限制
  5. 函数每被调用一次,都会产生一个新的执行上下文环境

执行上下文栈

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。

其实这是一个压栈出栈的过程——执行上下文栈。

var // 1.进入全局上下文环境
  a = 10,
  fn,
  bar = function(x) {
    var b = 20
    fn(x + b) // 3.进入fn上下文环境
  }

fn = function(y) {
  var c = 20
  console.log(y + c)
}

bar(5) // 2.进入bar上下文环境

执行上下文生命周期

1、创建阶段

  • 生成变量对象
  • 建立作用域链
  • 确定 this 指向

2、执行阶段

  • 变量赋值
  • 函数引用
  • 执行其他代码

3、销毁阶段

  • 执行完毕出栈,等待回收被销毁

javascript 事件循环

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop(事件循环)。

同步任务和异步任务,我们对任务有更精细的定义:

macro-task(宏任务):

可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得 JS 内部(macro)task与 DOM 任务能够有序执行,会在一个(macro)task执行结束后,在下一个(macro)task执行开始前,对页面进行重新渲染。

(macro)task 主要包含:script(整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、postMessageMessageChannel、setImmediate(Node.js 环境)

micro-task(微任务):

可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。

microtask 主要包含:Promise.thenMutaionObserverprocess.nextTick(Node.js 环境)

举个例子

我们来分析一段较复杂的代码,看看你是否真的掌握了 js 的执行机制:

console.log('1')

setTimeout(function() {
  console.log('2')
  process.nextTick(function() {
    console.log('3')
  })
  new Promise(function(resolve) {
    console.log('4')
    resolve()
  }).then(function() {
    console.log('5')
  })
})
process.nextTick(function() {
  console.log('6')
})
new Promise(function(resolve) {
  console.log('7')
  resolve()
}).then(function() {
  console.log('8')
})

setTimeout(function() {
  console.log('9')
  process.nextTick(function() {
    console.log('10')
  })
  new Promise(function(resolve) {
    console.log('11')
    resolve()
  }).then(function() {
    console.log('12')
  })
})

// 1,7,8,2,4,5,6,3,9,11,12,10

再来一段

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')

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

解决异步问题的方法

  1. 回调函数
ajax('XXX1', () => {
  // callback 函数体
  ajax('XXX2', () => {
    // callback 函数体
    ajax('XXX3', () => {
      // callback 函数体
    })
  })
})
  • 优点:解决了同步的问题
  • 缺点:回调地狱,不能用 try catch 捕获错误,不能 return

2、Promise 为了解决 callback 的问题而产生。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装。

  • 优点:解决了回调地狱的问题
  • 缺点:无法取消 Promise ,错误需要通过回调函数来捕获

3、Async/await

  • 优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
  • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

总结

  • javascript 是一门单线程语言
  • Event Loop 是 javascript 的执行机制

node 环境和浏览器的区别

1、全局环境下 this 的指向

  • node 中 this 指向 global
  • 浏览器中 this 指向 window
  • 这就是为什么 underscore 中一上来就定义了一 root;

浏览器中的 window 下封装了不少的 API 比如 alert 、document、location、history 等等还有很多, 我门就不能在 node 环境中 xxx();或 window.xxx();了。因为这些 API 是浏览器级别的封装,存 javascript 中是没有的。当然 node 中也提供了不少 node 特有的 API。

2、js 引擎

  • 在浏览器中不同的浏览器厂商提供了不同的浏览器内核,浏览器依赖这些内核解释折我们编写的 js。但是考虑到不同内核的少量差异,我们需要对应兼容性好在有一些优秀的库帮助我们处理这个问题。比如 jquery、underscore 等等。

  • nodejs 是基于 Chrome's JavaScript runtime,也就是说,实际上它是对 GoogleV8 引擎(应用于 Google Chrome 浏览器)进行了封装。V8 引 擎执行 Javascript 的速度非常快,性能非常好。

3、DOM 操作

  • 浏览器中的 js 大多数情况下是在直接或间接(一些虚拟 DOM 的库和框架)的操作 DOM。因为浏览器中的代码主要是在表现层工作。
  • node 是一门服务端技术。没有一个前台页面,所以我们不会再 node 中操作 DOM。

4、I/O 读写

与浏览器不同,我们需要像其他服务端技术一样读写文件,nodejs 提供了比较方便的组件。而浏览器(确保兼容性的)想在页面中直接打开一个本地的图片就麻烦了好多(别和我说这还不简单,相对路径。。。。。。试试就知道了要么找个库要么二进制流,要么上传上去有了网络地址在显示。不然人家为什么要搞一个 js 库呢),而这一切 node 都用一个组件搞定了。

5、模块加载

  • javascript 有个特点,就是原生没提供包引用的 API 一次性把要加载的东西全执行一遍,这里就要看各位闭包的功力了。所用东西都在一起,没有分而治之,搞的特别没有逻辑性和复用性。如果页面简单或网站当然我们可以通过一些 AMD、CMD 的 js 库(比如 requireJS 和 seaJS)搞定事实上很多大型网站都是这么干的。

  • nodeJS 中提供了 CMD 的模块加载的 API,如果你用过 seaJS,那么应该上手很快。node 还提供了 npm 这种包管理工具,能更有效方便的管理我们饮用的库

推荐文章