JS执行机制与Event Loop
前言
整理了一下 javascript 的基础知识,在此给大家做下分享,喜欢的大佬们可以给个赞。
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 () {
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 (整体代码)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
micro-task(微任务)
可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前的 task 任务后,下一个 task 之前,在渲染之前。所以他的响应熟读比 setTimeout 会更快,因为无需等渲染。也就是说,在摸一个 macrotask 执行完后,就会将在它执行期间产生的所有 mocrotask 都执行完毕(在渲染前)。
macrotask 主要包括:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
举个例子
我们来分析一段比较复杂的代码,看看你是否真的掌握了 js 的执行机制
consoloe.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('sacript end')
// script start
// async1 start
// async2
// promise1
// script end
// async end
// promise2
// setTimeout
解决异步的方法
1、回调函数
ajax('x1', () => {
// callback 函数体
ajax('x2', () => {
// callback 函数体
ajax('x3', () => {
// callback 函数体
})
})
})
优点:解决了同步的问题
缺点:回调地狱,不能用 try catch 捕捉错误,不能 return
2、Promise 为了解决 callback 的问题而产生
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新的 Promise,如果我们在 then 中 return,return 的结果会被 Promise.reolve() 包装
优点:解决了回调地狱
缺点:无法取消 Promise,错误需要通过回调函数来捕获
3、Async/await
优点是:代码清晰,不用 Promise 写一大堆 then 链,处理了回调地狱问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低
总结
javascript 是一门单线程语言
Event Loop 是 javascript 的执行机制