JS基础总结(4)—— JS执行机制与EventLoop
前言
农历2019已经过去,趁着上班前这段时间,整理了一下javascript的基础知识,在此给大家做下分享,喜欢的大佬们可以给个小赞。本文在github也做了收录。
本人github: github.com/Michael-lzg
JS基础总结(1)——数据类型
JS基础总结(2)——原型与原型链
JS基础总结(3)——作用域和闭包
JS基础总结(4)——this指向及call/apply/bind
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
(整体代码)、setTimeout
、setInterval
、I/O、UI 交互事件、postMessage
、MessageChannel
、setImmediate(Node.js 环境)
micro-task(微任务):
可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。
microtask 主要包含:Promise
.then
、MutaionObserver
、process.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 读写
5、模块加载
javascript 有个特点,就是原生没提供包引用的 API 一次性把要加载的东西全执行一遍,这里就要看各位闭包的功力了。所用东西都在一起,没有分而治之,搞的特别没有逻辑性和复用性。如果页面简单或网站当然我们可以通过一些 AMD、CMD 的 js 库(比如 requireJS 和 seaJS)搞定事实上很多大型网站都是这么干的。
nodeJS 中提供了 CMD 的模块加载的 API,如果你用过 seaJS,那么应该上手很快。node 还提供了 npm 这种包管理工具,能更有效方便的管理我们引用的库
推荐文章
从零开始构建一个webpack项目
总结几个webpack打包优化的方法
总结前端性能优化的方法
几种常见的JS递归算法
搭建一个vue-cli的移动端H5开发模板
封装一个toast和dialog组件并发布到npm
一文读尽前端路由、后端路由、单页面应用、多页面应用
关于几个移动端软键盘的坑及其解决方案
浅谈JavaScript的防抖与节流