数据如何储存
- 栈:
- Number;
- Boolean;
- String;
- null;
- undefined;
- bigint;
- symbol
- 堆:
- 所有的对象类型数据
- 为什么不都用栈呢(开销巨大)
- 对于系统栈而言,除了保存变量;
- 还有创建并切换函数上下文的功能;
V8引擎如何进行垃圾内存的回收
- 对于栈:
- 当上下文切换后,栈顶空间会被自动回收;
- 对于堆:
- 当我们构造一个对象进行赋值操作时,其实相应的内存已经被分配到堆上了;我们可以不断的构造对象,直到堆的内存上限;
- 为何要有堆的内存上限:JS的单线程和JS的垃圾回收机制
-
新生代内存:
- 由于在堆中内存是连续分配的,这样做是为了防止内存碎片--- Scavenge算法。
-
老生代内存:
- 如何晋升为老生代:
- 已经经历过一次Scavenge回收;
- 新生代的闲置内存占用超过25%;
- 老生代垃圾回收:
- 进行标记-清除:标记阶段--遍历堆内存中的所有对象并进行标记,然后将使用的变量和强引用的变量取消标记;清除阶段--将剩下标记的变量进行删除,释放内存。(依旧会产生内存碎片)
- 整理内存碎片。 清除阶段后,把存活的对象全部往一端靠拢。(非常耗时)
-
增量标记:由于老生代内存垃圾处理耗时很长,为防止阻塞。V8将标记任务分成很多小部分处理。
V8执行一段代码的过程
- 通过语法分析和词法分析生成AST(抽象语法树);
- 将AST转化为字节码(直接解析成机器码消耗内存);
- 由解释器逐行执行字节码,遇到热点代码(多次出现)由编译器生成相应的机器码,以优化执行效率。
EventLoop -- 宏任务和微任务
1.在JS中,大部分的任务都是在主线程上执行的,常见的有:
- 渲染事件;
- 用户交互事件;
- js脚本执行;
- 网络请求、文件读写完成事件等;
V8用队列去存储这些任务:宏任务(普通任务和延迟任务--setTimeout/setInterval)和微任务(Promise\Promise.then)
- 解决异步回调:
- 将异步回调进行宏任务队列的入队操作;(易造成应用卡顿)
- 将异步回调放到当前宏任务的末尾;
- 补充web worker(解决单线程导致主线程任务量过大,而渲染缓慢的问题): www.ruanyifeng.com/blog/2018/0…
2.EventLoop -- node.js
- timer阶段(执行到时间的定时器)
- I/O异常回调阶段(未立即等待到异步事件的响应)
- 空闲预备阶段(等到第二阶段结束,poll阶段未开始)
- poll(轮询)
- check阶段(执行setImmiate的回调--立即结束长时间任务)
- 关闭事件的回调阶段
3.node.js 与 浏览器关于eventloop的区别
- 浏览器的微任务是在每一次宏任务结束后执行的;
- node.js的微任务是在不同阶段之间执行的;
4.关于process.nextTick
- 独立于eventloop的任务队列;
- 在每一个eventloop阶段完成后会去检查这个队列,若有则优于微任务队列执行;
node.js的异步、非阻塞I/O是如何实现的
- 浏览器中只有一种I/O,即使用Ajax发送网络请求。 ---网络I/O
- node.js中:
- 文件I/O;
- 网络I/O
1. 非阻塞I/O和阻塞I/O
- 非阻塞I/O:调用后立即返回;
- 阻塞I/O:等到操作系统的操作全部完成后,再返回;(消耗性能)
2. 非阻塞I/O:如何通知node.js,操作系统已经完成操作了呢?
轮询:重复判断操作系统是否已经完成操作;
- 一直轮询查看I/O的状态;
- 遍历文件描述符(文件I/O时操作系统与node.js之间的文件凭证);
- epoll模式,即进入轮询时,若I/O未完成,CPU就休眠;完成后唤醒;
以上方法都非常消耗CPU。
3. 异步I/O本质(线性池)
- fs.readFile调用node.js的核心模块fs.js;
- node.js调用内建模块,创建对应文件的I/O观察者对象;
- 根据不同平台,内建对象通过libuv中间层进行系统调用;
- libuv中间层进行系统调用
- 创建请求对象;
- 推入线性池,调用返回;
- 回调通知;I/O观察者取出请求对象的存储结果,同时取出它的oncomplete_sym属性,将前者作为函数参数传递给后者,并执行。---完成回调
线性池:一个线程进行计算,剩下的进行I/O操作。I/O完成后,通知计算的线程。
异步编程的方案
1. 回调函数(高级函数)
弊端:
- 回调地狱;
内部实现机理(发布-订阅模式)
- addListener;
- removeListener;
- once;
- removeAllListener;
- emit
2. Promise(消灭了回调地狱)
亮点:
- 回调函数延迟绑定;(通过后面的then方法传入)
- 返回值穿透;(根据then中传入值创建不同类型的Promise,然后把返回的Promise穿透到外层,以供后续调用)
- 错误冒泡;(错误一直向后传递,被catch捕捉到)
为什么引入微任务:
- Promise中执行函数是同步的,但里面存在异步操作。
- 在异步操作结束后,会调用resolve方法;或执行中途出错,则调用reject方法。这两者都是作为微任务进入到EventLoop中。
如何实现链式调用:
Promise本质:有限状态机
错误捕捉及冒泡机制:
- catch方法捕捉错误
- 冒泡机制
onRejected = typeof onRejected === 'function' ? onRejected : error => {throw error}一旦其中一个PENDING状态的Promise出现错误后状态必然变为失败,然后执行onRejected函数,而这个onRejected执行又会抛错;新的Promise的状态变为失败;依次执行下去,直到catch捕捉到这个错误,才停止往下抛。
实现Promise的resolve、reject和finally
- Promise.resolve
- 传参为一个Promise,则直接返回它;
- 传参为一个thenable对象,返回的Promise会跟随这个对象,将他的最终状态作为自己的对象;
- 传参为其他对象,返回成功状态的Promise对象
- Promise.reject
- 传入的参数作为一个reason原封不动的往下传
- Promise.finally
- 无论成功还是失败,都会调用finally中的函数,并将值原封不动的往下传
实现Promise的all和race
- Promise.all
- 传入的参数为一个空的可迭代对象,则直接进行resolve;
- 如果传参中有一个promise失败,那么Promise.all返回的promise对象失败;
- 在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组;
- Promise.race
- 只要有一个promise执行完,直接resolve并停止执行;
3. co + Generator方式
特点:
- 用co库让代码一次完成,同时以同步的方法执行;
async + awaite
特点:
- 每一个async函数都返回一个Promise对象;
生成器以及协成的理解
1. 生成器执行流程
2. yield*
3. 生成器实现机制---协程
- 线程和进程
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 协程
- 协程处于线程的环境中,一个线程可以存储多个协程--> 可以将协程理解为线程的一个个任务;
- 协程不受操作系统的管理;
- 协程的运作过程(单个执行)
- A将执行权交给B(A启动了B),A为B的父协程;
- B最后
return 100,其实是将100传给了A - 对于协程,它不受操作系统的控制,完全有用户自定义切换。因此并没有线程和进程的上下文开销,因而高性能。
4. 如何让Generator的异步代码顺序执行
1. Generator如何与异步产生关系;
2. 怎么把Generator顺序执行完毕;
- thunk函数(偏函数):
- 核心逻辑:接受一定的参数,产生出定制化的函数,然后使用定制化的函数去完成功能;
- Generator和异步(核心:绑定回调函数)
- thunk版本
- Promise版本
- 采用co库
async和await的运行机制
1. async
- 概念: 一个通过异步执行并隐式返回Promise作为结果的函数;
2. await
forEach中用await会产生什么问题,怎么解决
无法保证异步代码,顺序执行
- 由于forEach的底层原理:直接执行,因此如果后面的任务用时短,就会提前执行。
for...of解决 --- 迭代器
- 解决原理:Iterator--迭代器