计算机网络
1.以什么为基准去衡量什么时候使用base64
ascii码用于编码二进制数据,共128个字符,其中0到31和127为控制字符(非打印字符),无法在互联网中传输,如果要传输这32个字符数据,则需将其编码为可打印字符,base64就是这种编码技术之一,也就是将二进制数据转为base64数据
Base64,就是使用64个可打印字符来表示二进制数据的方法。Base64的索引与对应字符的关系如下表所示:0~63分别对应了唯一一个字符,比如18对应的是S。
编码过程:字符数据 --- ascii码 --- 二进制数据 --- base64
js中可使用btob()和atob()方法来编码译码base64
base64优缺点
优点:可使二进制数据在http中传输
缺点:3字节数据MAn转为base64后变成4字节TWFu,体积增大四分之一,解码编码需要额外工作量
一般图片请求的请求头会包含Request URL、Accept-Encoding、Accept-Language、Cache、Control、Connection、Cookie、Host、Pragma、Referer、User-Agent等信息,体积一般在400Bit到1000Bit之间
结合base64体积增大四分之一计算,则图片资源大小在4000B以下时,使用base64较为合理
2. 画出SSL四次握手过程
1 客户端发送client hello(ssl/tls等加密算法列表)
2 服务端接收后返回server hello(加密算法子集)、证书(公钥、颁发机构、过期时间、域名等信息)
3 客户端验证证书、取出公钥、通过公钥加密一个随机值为会话秘钥,传送该加密信息
4 服务端通过私钥解密该加密信息,取出会话秘钥,再加密该会话秘钥返回给客户端
5 客户端验证通过后,后续使用该会话秘钥传输加密内容
3. 请问SSL握手时有对称加密和非对称加密吗? 如何优化这一层?
客户端通过公钥加密会话秘钥,服务端通过秘钥解密该会话秘钥,这个过程是非对称加密
客户端再完成交换后使用会话秘钥加密内容,服务端收到后通过会话秘钥进行解密,这个加密方式是对称加密
如果将所有的HTTP连接变为HTTPS连接,则明显RSA的解密最先成为瓶颈。因此,RSA的解密能力是当前困扰HTTPS接入的主要难题。
1、CDN接入
选择使用 CDN 作为 HTTPS 接入的入口,将能够极大减少接入延时。
2、会话缓存
虽然前文提到 HTTPS 即使采用会话缓存也要至少1*RTT的延时,但是至少延时已经减少为原来的一半,明显的延时优化;同时,基于会话缓存建立的 HTTPS 连接不需要服务器使用RSA私钥解密获取 Pre-master 信息,可以省去CPU 的消耗。如果业务访问连接集中,缓存命中率高,则HTTPS的接入能力讲明显提升。
3、硬件加速
为接入服务器安装专用的SSL硬件加速卡,作用类似 GPU,释放 CPU,能够具有更高的 HTTPS 接入能力且不影响业务程序的。测试某硬件加速卡单卡可以提供35k的解密能力,相当于175核 CPU,至少相当于7台24核的服务器,考虑到接入服务器其它程序的开销,一张硬件卡可以实现接近10台服务器的接入能力。
4、远程解密
本地接入消耗过多的 CPU 资源,浪费了网卡和硬盘等资源,考虑将最消耗 CPU 资源的RSA解密计算任务转移到其它服务器,如此则可以充分发挥服务器的接入能力,充分利用带宽与网卡资源。远程解密服务器可以选择 CPU 负载较低的机器充当,实现机器资源复用,也可以是专门优化的高计算性能的服务器。当前也是 CDN 用于大规模HTTPS接入的解决方案之一。
5、SPDY/HTTP2
前面的方法分别从减少传输延时和单机负载的方法提高HTTPS 接入性能,但是方法都基于不改变HTTP 协议的基础上提出的优化方法,SPDY/HTTP2 利用TLS/SSL 带来的优势,通过修改协议的方法来提升HTTPS 的性能,提高下载速度等。
4. 一个 tcp 连接能发几个 http 请求
http1.0 一个tcp连接只能发送一个http请求, 可手动设置connection为keep-alive来使tcp保持连接
http1.1 默认设置connection为keep-alive,一个tcp连接可发送多次http请求,理论上在keep-alive-timeout设置的超时时间之内可以无限次发送http请求,所以需控制好这个超时时间,避免服务器端口被长期占用,tcp的keep-alive是用于发送心跳包,保证连接的可靠性,多个心跳包没收到,随即关闭连接
如何得知请求已接受完成,并开始超时计时呢? http1.1中使用content-length来标识内容长度,接收到请求时判断内容长度是否达到length,达到则认为接受完成,如果内容过大,有分片情况,则判断最后一个分片的长度是否为0
http1.1只能单路单用,浏览器默认tcp最大同时连接数为6个,如果页面需要同时请求10个资源,那么前6个会并行加载,剩余4个串行
http2.0中增加了多路复用,可使一个tcp连接同时并发多个http请求
5. 前端浏览器输入URL后发生什么?html文件获取,它是如何传输的?
输入url后
- 首先浏览器会查看浏览器缓存的DNS列表,如果命中直接返回对于的ip地址
- 否则会继续查看系统中的host文件和系统缓存中是否有已解析后的结果,有则返回
- 如果还没查到,则会去当地DNS服务器(LDNS)去查询,查到则返回,一般80%的域名可在该服务器查询到并解析
- 否则LDNS继续向根服务器(root)查询,root会返回顶级域名服务器ip
- LDNS去顶级域查询到二级域名服务器ip
- LDNS去二级域名服务器查询三级域名服务器ip
A记录解析域名到一个固定的IP,Cname记录解析域名到另一个域名,称为别名
修改host文件、海外DNS解析被墙、DNS劫持都可导致DNS解析到错误的IP上(DNS劫持可通过修改本机系统DNS设置、路由器设置、运营商服务器被劫持等,无法完全杜绝,可加速服务器缓存来优化)
最终返回完整ip地址,通过ip地址和端口号浏览器开始建立TCP连接,访问对应的web服务器一般为nginx,web服务器接收到请求后,开始分析路径,根据web服务器配置的router去寻找对应的服务,如果最终分析是html请求,nginx服务器会自动处理头信息和返回体并返回给客户端,客户端进行构建渲染
6. http1.0、1.1、2.0、3.0的区别
三者的差别主要体现在tcp的连接数上
http1.0在建立tcp连接后只能发送一个http请求,tcp随即关闭,后续再请求需重新建立tcp连接,属于短连接模式
http1.1在1.0的基础上增加了keep-alive模式,同时设置请求头connection来控制,close代表关闭,timeout设置超时时间,在建立tcp连接后,在超时时间范围内,客户端可发送多次http请求,属于长连接模式,同时1.1支持单独发送请求头信息到服务器,待接受到服务器返回100返回码后继续发送请求体
http2.0在1.1的基础上增加了多路复用、请求头压缩、服务器推送等功能,多路复用可使在建立完tcp连接后,多次多并发的发送http请求,1.1的并发只能同时支持浏览器限制的6个,每个都需建立新的tcp连接
HTML/CSS
1. CSS下载解析会不会阻塞DOM树渲染
css下载解析不会阻塞DOM树渲染,是两个不同的线程解析,但是会影响render树的构建,render需要等待css下载解析完
2. 有没有可能让JS下载解析不阻塞DOM树构建
可以使用在script标签中使用async 或 defer = "true",async可异步并行加载js,加载完成后就执行,无论声明顺序如何;defer同样是异步加载,但它会在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,顺序执行
JS基础
1. 什么是浅复制和深复制?有什么区别?如何实现Object的深复制
复制对象时,浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据,其中属性存储和引用指针在栈内存中,数据存在堆内存中
深拷贝就是对对象以及对象的所有子对象进行拷贝
// 递归
function copy(sourceObj) {
const targetObj = {};
Object.keys(sourceObj).forEach((key) => {
const value = sourceObj[key];
const type = Object.prototype.toString.call(value).slice(8, -1);
if (type === 'Array') {
targetObj[key] = [];
value.forEach((arrItem) => {
targetObj[key].push(arrItem);
});
} else if (type === 'Object') {
targetObj[key] = copy(value);
} else {
targetObj[key] = value;
}
});
return targetObj;
}
2. 知道什么是事件委托吗?
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件
无需绑定多个li 节约内存、优化性能,同时未来某个元素插入,不必重新绑定事件
3. new和instanceof的内部机制
new两个作用,一是将this指向自身对象,而是将自身对象的原型指向构造函数的prototype
function Test(name) {
this.name = name;
}
function new2(...params) {
const obj = Object.create(Test);
Test.apply(obj, params);
return obj;
}
new2('xiao') // Test {name: "xiao"}
new Test('xiao') // Test {name: "xiao"}
a instanceof b 判断b.prototype是否出现在a的原型链上
const xiao = new Test('xiao');
xiao instanceof Test // true
vue
1. vue的初始化,发生了什么
执行new Vue()后,vue会首先进入初始化阶段(初始到create),该阶段会初始化一些属性、事件、响应式数据,具体步骤为:
初始化实例属性(initLifecycle),比如root、$children等供外部使用的属性,以及_watcher、_isDestoryed、_isBeingDestoryed等供内部使用的属性
初始化事件(initEvents),将在父组件中注册的事件添加到子组件的事件系统中
初始化render(initRender)初始化渲染相关的属性以及方法,vnode、_c、$createElement
---beforeCreate--
初始化inject(initInjections), inject和provide一起使用,他们允许祖先组件向其所有子孙后代注入依赖
初始化状态(initState),props、methods、data、computed、watch
初始化provide(initProvide)
---created--
2. vue的模板解析,是如何进行的?如何形成AST?render函数的生成?什么是依赖收集?什么是patch?数据更新策略等
通过三种解析器(html解析器、过滤器解析器、文本解析器)对模板进行解析,其中最核心的是html解析器,通过词法分析(正则匹配)找到元素的开头、属性、内容、注释、结束等信息,找到时分别触发start、end、chars、comment等钩子函数,通过栈结构来保存AST结果对象,while循环来解析,如果在解析中存在文本变量,则需二次调用文本解析器处理,循环结束后,可生成AST树结构
在经过优化器标记静态节点后,代码生成器开始递归每一个AST节点来生成代码字符串,不同类型节点分别调用_c(元素) _v(文本) _e(注释)来生成对应的字符串,最后生成一段类似with(this) {return _c('div', {attrs: {'id': 'el'}})}的字符串作为render函数
依赖在vue中就是watcher,在vue挂载阶段会将组件(vue实例)中的watcher收集起来,包括模板编译实例化的watcher以及调用$watch方法后实例化的watcher,watcher实例化时会将自身this保存在全局某个位置并通过访问data属性,触发data的getter,该getter函数会在全局中将watcher取出在存放在dep中,而后当该数据触发setter时通知dep中的watcher执行渲染操作,这个过程称为依赖收集
patch是vnode进行对比,然后根据对比来新增、移动、删除、修改节点以及渲染为真实dom的一个过程,子节点进行优化循环对比新增、修改的节点会插入到未处理的节点之前,循环结束后未被处理的oldchildren将直接删除,通过document.createElement来创建真实dom元素节点,创建完成后通过apendChild方法来插入到dom中
3. Virtual Dom 的概念以及优势在哪里
虚拟dom就是一个虚拟节点树,在vue中是由vnode组成的一棵树,vnode其实就是一个js对象,用于描述真实dom,新生成的虚拟节点树会和上一次生成的进行对比,只渲染不同的部分;
首先虚拟dom的创建、更新、移动、删除等操作是在js引擎中完成,速度会比直接通过操作dom来说快很多,同时虚拟dom可以缓存,在渲染时只需对比新旧节点的差异,然后将少量节点做真实dom操作
至于vue2.0为何开始引入虚拟dom,是因为1.0的检测颗粒太细,模板中每一个状态都会生成一个watcher来观察变化,当节点很多时,会造成大量的内存开销和追踪依赖开销,引入虚拟dom后,检测颗粒变为中等,每一个组件(实例)模板中只会生成一个watcher来观察变化,状态变化时会通知到组件,然后组件内部再通过虚拟dom来进行对比和渲染页面
虚拟dom会触发新建vnode、对比、计算、比1.0直接状态更新触发节点更新的方式,多了更多的步骤,相当于是用时间换空间
4. 响应式即变化侦测的原理
vue通过侦测变化来实现响应式系统,侦测变化主要的是通过es5提供的object.definepropty方法来实现,该方法可以设置对象属性的getter和setter,当该属性被访问时触发get函数添加watcher依赖,当属性值修改时触发set函数通知watcher更新视图,vue会在初始阶段会通过observer对data中的所有属性递归设置getter和setter,这种方式无法追踪新增属性和删除属性,也无法侦听到数组的push、pop、reverse、splice、shift、unshift等更改数组本身的操作,对象使用delete来解决,而数组使用拦截器的方式来解决,拦截器继承自Array.prototype,修改push等方法使在原本功能不变的基础上添加其他行为,比如发送变化通知,然后修改data中为数组类型的值的原型为拦截器,不支持proto方式设置的浏览器则直接添加到数组的属性上,数组中的值也会循环丢入observer中处理为响应式数据,无法监听arr[0] = 1这种操作
6. vue.js的生命周期钩子之间的区别
初始化自定义事件
beforeCreate
初始化props、data、方法、计算属性
created
模板编译,el自动挂载或destory()
beforeDestroy
卸载依赖追踪、子组件与事件监听器
destroyed 已卸载
7. 各种API内部实现原理
8. 指令实现原理
9. v-if 和v-for为什么不能一起用? v-for中为何要加上key?
v-for比v-if拥有更高的优先级,即使部分元素被设置为v-if为false,渲染时还是会遍历整个列表,建议使用计算属性过滤掉列表后再渲染,或者将v-if添加到父元素ul中
在模板中使用key在模板解析后,在patch过程中会建立唯一key和节点索引index之间的对应关系,那么在列表更新列表触发patch对比查找时,不需要在旧子节点oldchildren中循环查找,可直接通过key获取节点,从而提高性能,在有很多子节点的列表中更明显
10. keep-alive、$nextTick
11.组件间传递值
前端性能
说下前端性能优化
首先,前端性能是一个前端中涉及很广、也很重要的一点,大概可以三类来优化,页面HTML/CSS相关、JS相关、以及网络相关;
HTML/DOM/CSS
- 非页面依赖的js文件加载放在body后执行,或者通过defer属性来异步加载,保证js的加载和执行不会阻塞到主渲染进程
- 元素的新增、删除、移动都会触发浏览器对页面dom的重排或重绘,减少页面的重排重绘,可使需要经常更改的元素脱离文档流、display:none后进行一系列操作后再display:block等方式
- 动画是个很耗性能的东西,尽量用css3来实现动画,浏览器对css3动画有优化,可通过设置transform3d的方式来开启GPU进一步提高渲染性能,如果非得用js来实现动画,可使用js的requestAnimationFrame方法来实现,该方法的第一个参数传递一个回调函数,该回调函数会在屏幕下一个刷新周期结束后执行,屏幕刷新频率(一般设置为60HZ)是根据当前系统性能来动态调整的,这样可保证动画不会掉帧、卡顿;还有svg、webGl、canvas等动画的实现
JS
631385108
避免内存占用过高,全局变量挂载的对象、定时器、事件、闭包中的变量;使用过的变量,会被浏览器通过标记清除法进行垃圾回收;内存占用过高会导致性能变差甚至应用崩溃,可通过浏览器工具来查看当前内存占用
减少作用域查找、dom元素查找、原型链查找的成本,尽量使用局部变量、当前对象上的属性访问以及存储dom元素引用到一个变量上
避免使用eval、with等可修改当前作用域的方法,作用域修改会导致浏览器作用域相关优化失效,99%的情况下会极大影响性能同时也有安全风险,另外1%可参考vue的模板编译后的with,这里的with只影响了虚拟dom的性能,对比可能出现的大量的编译器代码来说,是可以接受的
函数节流、防抖,避免触发大量操作,可使用loadash中的方法,也可以自己实现
// 防抖
function debounce(fn, wait) {
let timer = null;
function debounced() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
clearTimeout(timer);
fn();
}, wait)
}
return debounced;
}
// 节流
function throttle(fn, wait) {
let flag = false;
let timer = null;
function throttled() {
if (flag) return;
flag = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
clearTimeout(timer);
flag = false;
fn();
}, wait)
}
return throttled;
}
- 避免出现死循环、循环次数过多,对算法进行优化,递归算法尾部调用优化
网络
- http缓存,强制缓存、协商缓存
强制缓存的返回头有expires(http1.0)、Cache-control两种,expires的值是一个过期时间,在过期时间内强制缓存该请求结果, Cache-control值有no-cache(浏览器不做强制缓存检查)、no-store(浏览器和中间代理服务器都不能缓存资源)、private(资源不允许被中间代理服务器缓存)、public(资源允许被中间服务器缓存)、must-revalidate(可以缓存,但是使用之前必须先向源服务器确认)、proxy-revalidate(要求缓存服务器针对缓存资源向源服务器进行确认)、
s-maxage(缓存服务器对资源缓存的最大时间)、max-age=1000(相对最大缓存时间)等多种字段控制,Cache-Control 对缓存的控制粒度更细,包括缓存代理服务器的缓存控制,它的优先级比expires高
协商缓存是客户端和服务器通过http请求头的设置来协商是否缓存资源的方案,在返回头中主要是Last-Modified和Etag。
初次请求服务器后,浏览器会将服务器返回的Last-Modified的值和返回内容进行缓存,如果该请求没有被强制缓存,在下次请求时浏览器会将之间存储的修改时间值通过If-Modified-Since字段发送给服务器,服务器对比该值和文件最后的修改时间,如果是一样,则返回304,不一样则返回200返回码以及新的内容和最新的Last-Modified,从而实现缓存和更新
Etag的原理差不多,请求头中通过If-None-Match字段携带上一次请求返回的etag值,服务器对比该值和通过文件内容计算的Etag值,一样则返回304,否则返回200以及最新的内容和最新的Etag
缓存的一些缺点
expires的设置不稳定,用户更改系统时间,则出现误差
last-modified无法精确到秒
etag需耗费服务器资源来生成hash,在多台服务器负载均衡的情况下会失去意义
在强制缓存期间,如何得知服务器资源的更新并更新本地资源(html不缓存,其他资源加上版本号)
- 雪碧图、base64、iconfont
雪碧图将多张小图合并到一张大图上,通过设置背景位置的方式来展示图标,通过这种方式来达到减少http请求提高性能,需合成图片、维护成本较高
iconfont将svg图片通过工具转成字体文件,使用时可当做普通字体来使用,设置font-size 、color等,体积比图片小,并可以自由伸缩不会模糊,对图标有要求,需是纯色、大小在固定返回内、图形需闭合等等
base64方式是将小图片转为base64格式字符串,打包进css或者js文件中,减少http请求,达到提高性能的目的,缺点是会将原始数据体积增加1/4,适用于小于4kb左右的图片,否则得不偿失
- 代码压缩、tinypng、gzip压缩、webpack优化
代码可结合webpack或其他打包工具来压缩,webpack中使用的uglify插件
tinypng可对png图片、jpg图片进行无损压缩,可压缩80%
在nginx服务端需开启gzip压缩,并设置压缩倍数
当js文件过大并且有重复代码时webapck可进行拆包和提取,通过dllPlugin来将vue、vue-router等网页依赖的基础模块抽离出来打包成一个单独的base.js文件(利用浏览器缓存下来),通过commonChunkPlugin将多个chunk中的公共部分提取出来为common.js,最后通过webapck-bundle-analyzer插件来查看各个模块大小及其依赖关系
- nginx服务器、cdn服务器、dns服务器
多台nginx服务器使用F5进行负载均衡、使用nas磁盘同步资源更新
平安内部cdn加外部供应商cdn服务器,采用回源的方式拉取nginx文件
dns服务器由内部统一解决
- 懒加载、预加载、按需引入
懒加载在需要的时候再加载,随再随用,可减少首页压力,vue中的路由懒加载
预加载,预先加载一些资源,比如动画图片,避免在放动画时在加载,影响体验
按需引入,比如按需引入loash函数、element-ui组件等,避免整体引入
- http2.0、http3.0
http2.0在1.0的基础上增加二进制数据、多路复用、服务器推送、请求头压缩等,可共用同一个tcp连接多路并发http请求
HTTP3.0,也称作HTTP over QUIC。HTTP3.0的核心是QUIC(读音quick)协议,由Google在2015年提出的SPDY v3演化而来的新协议,传统的HTTP协议是基于传输层TCP的协议,而QUIC是基于传输层UDP上的协议,可以定义成:HTTP3.0基于UDP的安全可靠的HTTP2.0协议。
减少了TCP三次握手及TLS握手时间、多路复用丢包时的线头阻塞问题等
浏览器
webworker可开启另一个线程处理复杂计算问题,线程间通过事件消息机制相互联系,传递时使用Transferable对象可避免双倍内存,通过new Worker('worker.js')的方式开启子线程,脚本受同源限制,无法访问主进程中的dom对象,通过close方法关闭子线程
pwa
pwa翻译过来就是渐进式web应用,使用serviceWorker来拦截页面请求,并根据网络是否可用判断是否使用缓存数据或者更新缓存数据。catheStorage中存储缓存数据,它们还允许访问推送的通知和后台的API。
维护一个json文件并通过<link rel="manifest" href="./manifest.json">的方式引入,来创建桌面小图标
核心技术
Web App Manifest 通过manifest.json设置 PWA 的启动画面的图标和颜色等
Service Worker 是 PWA 中最重要的概念之一,它是一个特殊的 Web Worker,独立于浏览器的主线程运行,特殊在它可以拦截用户的网络请求,并且操作缓存,还支持 Push 和后台同步等功能。Service Worker 通过 Cache Storage 、Cache API 操作本地缓存,以及通过 fetch API 请求服务器端数据,不管是否有网络连接,或者站点发生了 404 、500,都可以让用户看到特殊定制的错误页面,而不是浏览器的默认 404 页面。
App Shell 和 App Skeleton
以 Vue 的项目举例,AppShell 包含:
入口 HTML 文件
打包好的 Vendor JS 文件
导出的 CSS 文件
如上图所示,App Shell 渲染出了 header 部分,那么正文部分在加载数据之前,都是白屏,这对于用户来说体验非常不好,有一个名词叫骨架屏(App Skeleton),在渲染出数据之前,在白屏位置占位,尽量不出现长时间的白屏。
App Skeleton 需要在最短的时间内渲染给用户,所以,一般情况下,会将 App Skeleton 编译到 HTML 里,就像下面的代码,期望浏览器在加载完 HTML 之后就能先显示骨架屏。
PWA 全称是 Progressive Web Apps,意味着是渐进式的,也就是在现有的基础上进行逐渐添加,从而改善用户体验,并不需要推倒重来,对整个站点进行改造
PWA 在 2017 年初,仅仅 Chrome 和 Firefox 支持 PWA,经过一年的发展,国内主流浏览器都已经支持 PWA,iOS 在 新发布的11.3 版本中也支持了 PWA。
离线包方案
1.Node里面的模块是什么?
node中的模块分为两类、核心模块、文件模块,其中最常用的是文件模块,包含项目中文件、node_modules中的包,核心模块由nodejs提供,例如fs模块、os模块、net模块等等,nodejs中通过require可导入其他模块、也可以通过mudule.exports导出自身
2.require的模块加载机制
文件模块和核心模块不一样
文件模块分为路径分析(相对路径、绝对路径、自定义模块路径)、文件定位(js、json、node后缀名顺序,package.json中的main,默认index.js)、编译执行几步,
node在启动时会编译核心模块保存在内存中,require时直接在内存中加载
js文件通过fs模块加载后编译,json文件通过json.parse()直接处理后赋值给模块对象的exports属性,node文件不需要编译直接通过dlopen方法加载,加载后将模块的exports对象和node模块产生联系并返回给调用者;js文件编译过程中对模块用函数进行头尾包装,隔离作用域,通过vm原生模块的runInThisContext()方法来执行,函数参数列表传递require、exports、module、__fliename、__dirname,函数执行后回传module的exports属性,使外界可访问
模块加载后都会缓存,下次require时绝对优先从缓存中获取,缓存 > 内存 > 文件定位查找 > node_modules查找
核心模块在node启动时会进行编译,js核心模块代码会被转成字符串通过c++中的数组存储在内存中,在require引入时从内存读取并添加头尾包装
c++内建模块通常不被用户调用,而是被js核心模块调用,require时无法查找,直接从内存中获取,也无需编译,直接执行,process.binding方法将内建模块内部变量和方法导出,改方法会新建一个空的exports对象,并填充对象,实现导出,js核心模块就是通过这个方式导出的
- exports和module.exports的区别
exports其实就是module.exports,exports通过形参的方式传入,直接复制形参会改变形参的引用,切断和module的引用
- node事件循环的流程
node启动后会开启一个类型于while的循环,每一次循环称为一个tick,在每一个tick中会询问观察者是否有待处理事件,如果有则取出对应回调函数执行,并进入下一个tick,直到没有事件处理退出循环,观察者在node中有多种,网络IO观察者、文件IO观察者等,node的事件循环是典型的生产者/消费者模式,网络请求就是事件的生产者,事件循环就是消费者
异步调用时,首先封装一个请求对象,对象中包含数据和回调函数,将请求对象丢入底层libuv的线程池等待执行,线程可用时执行请求对象中的IO操作,完成后将结果放在请求对象中,通知观察者,事件循环取出回调并执行
除了网络、文件IO外,setImmediate、settimout、setinterval和process.next函数也会触发异步调用,定时器会有一个专门的定时器观察者,和IO不同的是它不需要线程池的参与,定时器观察者内部会维护一颗红黑树,调用定时器时会向树中插入自身,每次tick时会从树中迭代检查定时器是否超时,超时则形成一个事件,立即执行回调函数,由于其他任务执行需要消耗时间,所以定时器的时间会不准
nextTick的方式会更轻量,直接将回调函数放入队列中,在下轮tick时取出执行
事件循环是异步的核心
5.V8的内存限制是多少,为什么这么设计
32位操作系统下是0.7G,64位操作系统是1.4G buferr等内部模块可突破限制,也可以设置这个大小
垃圾回收会引起js线程暂停执行,对1.5G内存进行一次垃圾回收需要50毫秒以上,做一次全量式垃圾回收需要1秒以上,这种花销会引起性能直线下降,所以控制内存使用,并且在浏览器的使用情况下,1.4G已经完全足够
- V8垃圾回收
V8将内存分为新生代和老生代两部分,新生代存储短时间内会被销毁的对象,老生代存储长时间存活的对象,新生代采用scavenge算法,将内存一分为二,from和to两部分,对象存入form中,将存活的对象从from中复制到to中,非存活对象直接丢弃,to和from交换,经过一个scavenge还未被回收的或者to空间的占用超过25%,对象会被放入老生代中,老生代才用标记清除算法来处理对象,将存活的对象进行标记,然后删除未被标记的对象,删除后会产生非连续空间,在空间不足时,使用mark-compact进行移动,移动完成清理边界外的内存,全量垃圾回收会时应用停顿时间过长,顾采用增量的方式步进,回收一下,应用执行一会
7.内存泄漏
应当被回收的对象没有被回收,并常驻在老生代中,称为内存泄漏
常见三种,一种是缓存,比如全局对象或闭包对象越来越大,得不到回收,一种是队列消费不及时,队列中元素的产生的速度大于消费的速度,第三种是作用域未释放(闭包,每一个模块都是);
内存泄漏导致内存占用持续增加,最终可达到超出限制,应用崩溃;
应当使用redies来缓存进程间可共享状态也可以让垃圾回收更高效;使用node-headdump来生成内存快照,然后放在谷歌浏览器中分析,通过流的方式来处理大文件