一、事件循环
在了解事件循环之前 先要了解什么是同步任务什么是异步任务。我们都知道js代码在执行的时候是按顺序执行,在上一行代码没执行完毕是不能去执行下一行代码的。这也就意味着js在无法同时做多件事情。而异步任务就是为了解决这一问题。
异步任务又分为宏任务和微任务。而事件循环指的就是宏任务和微任务的执行所产生的一个循环机制。
常见的宏任务有script(整体代码)、setTimeout、setInterval、setImmediate(nodejs独有)、requestAnimationFrame(浏览器独有)、IO、UIrender(浏览器独有)
常见的微任务有:process.nextTick(nodejs独有)、Promise.then()、Object.observe、MutationObserver
当一段js代码在执行的时候 会维护宏任务和微任务两个队列。遇到了宏任务或者微任务就会将他压入对应的对列中。等当前代码执行完毕后就会将微任务队列里得任务拿出来执行。如果此过程中产生了新的微任务 则又维护一个新的微任务队列,等当前微任务队列清空后再去执行新的微任务队列里得任务,当所有得微任务都执行完之后就会开始执行宏任务队列里得任务。如果此过程中产生了微任务则又会开始执行微任务,以此循环直到两个队列都被清空。这就是事件循环得全过程。ps:微任务会在dom渲染之前执行,因为js引擎线程和GUI线程互斥的原因,导致js在执行,就不会去渲染页面。
下面让我们看一道练习题来巩固一下知识
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1");
});
setTimeout(function () {
console.log("setTimeout2");
});
console.log(2);
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});
首先代码遇到setTimeout 将他加入宏任务队列,然后执行道第一个promise 输出其中得同步代码 promise1
然后将.then加入微任务队列。
随后又遇到了setTimeout 将其加入宏任务队列
紧接着输出2
最后又是一个promise 而这个promise里没有同步代码 所以直接将then加入微任务队列。
此时已经输出了promise1,2。
而当前得宏任务队列里有两个定时器 微任务队列里有两个promise.then
现在开始执行微任务,先输出then1 紧接着输出then3.
当前微任务队列已经空了 开始执行宏任务队列。
进入第一个定时器 输出setTimeout1,紧接着又遇到了微任务,所以又开始执行微任务。
输出then2 而这个微任务里又有一个微任务 所以接着执行他 输出then4
微任务清空了 接着去执行宏任务 输出setTimeout2.
所以最终得输出顺序应该是
promise1,2,then1,then3,setTimeout1,then2,then4,setTimeout2
二、作用域,作用域链,执行上下文
作用域指的就是一个变量被定义得区域,他决定了如何查找这个变量以及变量可被访问的权限。
作用域分为全局作用域 函数作用域 块级作用域。
在es6之前js是没有块级作用域得 所以除函数作用域外var声明得变量都会提升到全局,变成全局变量。es6引进了let和const两个关键字来声明变量。从而就有了块级作用域的概念 。在一个花括号内使用let和const声明的变量只能在这个块内被访问,外部是无法访问的。const用来声明一个不可改变的常量
js采用的是静态作用域,也就是变量得作用域在他被定义得时候就已经决定了。而与之对应得动态作用域则是在哪儿被调用 作用域就在哪儿。
执行上下文
当一个函数被调用得时候,js就会进行准备工作, 这个准备工作指的就是创建执行上下文。
一个执行上下文包括 变量对象(vo) 作用域链 this
执行上下文也分为全局执行上下文和函数执行上下文。
js有一个专门维护执行上下文得栈 全局执行上下文始终在栈得最底部 每个函数被调用时 他得执行上下文就会被压入栈中 等他执行完毕后 这个执行上下文又会被弹出。
变量对象包括函数得形参 以及函数内部所声明得所有变量。当在函数中访问一个变量时 js会现在当前函数得执行上下文得变量对象中查找,如果没有找到 则会去查找父级作用域得变量对象。一直到全局上下文得变量对象为止。这个查找得过程就是作用域链。
三、js垃圾回收机制
js大概有两种垃圾回收机制
1、标记清除
js引擎会给所有的对象打上一个标记,当这个对象被引用时 就会把标记清除掉,最后再把所有还保留这标记的变量回收 释放内存。这种方法会导致有很多内存碎片。
2、引用计数
采用计数的方式记录一个变量是否可以被回收,初始赋值的时候数值+1当变量被赋值给别人的时候 数量+1 当给变量赋值的时候数量-1,如果变量的计数为0则说明没有人引用这个变量了。则可以回收此变量。直接给变量赋值null此变量就会被回收。
四、模块化相关
模块化有利于代码的提高代码复用性和可维护性,防止命名冲突以及全局污染。
js的模块化方案大致有以下几种:
1、AMD:异步加载模块,所有的模块代码都写在回调函数中,只有当模块加载完毕才会去执行回调函数里的代码,此模式适用于浏览器端
2、CMD:可异步也可同步的模块加载方案,他和AMD不同的是执行了require就会去加载模块,支持同步也支持异步,支持浏览器也支持node
3、UMD:AMD和CMD的整合
4、Commonjs:这是nodejs的模块化方案他的每个js都是一个模块,每个js中它提供了几个核心变量:exports、module.exports、require。前两个用来暴露出变量,最后一个用来引入。
-4.1、require的加载流程:首先被加载的内容分为 node核心模块,自定义模块,文件模块。 像我们通过路劲引入的则是文件模块,而直接通过包名引入的话 commonjs首先会去查找是否是node自带的模块,如果不是则去node_modules里查找这个包,如果还没有则往父级的依赖文件夹里找 直到根目录为止。
那commonjs是如何避免循环引用和重复加载的呢?
要搞清楚这个问题得直到module和Module这两个东西,上文说了每个js文件都是一个module,这个module身上不仅有导入导出的方法,还有一个loaded属性,表示这个模块是否被加载过。Module则是nodejs在运行的时候创建的一个保存所有模块值得变量,每个模块在被加载之前都会去Module身上查找是否有缓存,没有的话就会存下。
有了这两个东西之后commonjs就可以做到避免重复加载和循环引用的问题。
5、ESM:Es Module是JS的es6规范才提出的模块化方案,是一个真正属于js自己的模块化规范
他的诞生有很多优势:
借助 Es Module 的静态导入导出的优势,实现了 tree shaking。
Es Module 还可以 import() 懒加载方式实现代码分割。
Es Module 中用 export 用来导出模块,import 用来导入模块。但是 export 配合 import 会有很多种组合情况,接下来我们逐一分析一下。
ESM的导入导出都是静态的,import会自动提升到代码顶部,并且他的语法都不能写在函数作用域和块级作用域内,也不能写在条件判断内。
正因为这种静态的语法,所有他会在代码编译阶段就确定依赖关系,也是借助这个特性,可以做摇树优化。
因为import会提升到最顶部,所以esm的执行顺序是先子后父。
下面总结一下ESM和Commonjs:
Commonjs:
1、代码运行时加载
2、导出的是值得浅拷贝,值一旦输出,在模块内到处方法是无法修改已导出的值的
3、可以动态加载,对每个导出的值都有缓存,能有效解决循环引用的问题
4、加载的时候才执行
ESM
1、代码编译时加载
2、导出的值可以被模块内的方法修改,但不支持外界主动修改
3、写法灵活,可以单个导入导出混合导入导出
4、因为import会提升,所以esm的模块会提前加载并执行