这些前端高频问题你能答出多少?

1,373 阅读16分钟

http和https的区别?

都是超文本传输协议,不过https增加了一个SSL协议加密,双方的默认端口不同,http是80,https是443。

SSL加密方式有哪些?

对称加密: 加密和解密都使用相同密钥,这个密钥的生成来浏览器和服务端各生成的一个随机数,将其混合成一个密钥,使用这一个密钥进行数据的加密传输。

非对称加密: 使用公钥和私钥两把密钥。浏览器发送加密套件列表给服务器,服务端选择一个加密套件后将公钥发给浏览器,浏览器使用公钥进行数据的加密,服务端使用私钥解密。

对称加密和非对称加密 使用随机数加公钥私钥。浏览器发送随机数client-random给服务器,服务端返回service-random随机数和公钥,浏览器生成随机数pre-master,并使用公钥加密,浏览器和服务器拥有相同的client-random和service-random及pre-master,将其生成为相同的密钥,用于对称加密传输。

以上加密方式的缺点是什么?

对称加密: 传输的随机数及合成密钥的算法是明文的,攻击者也可以自己合成密钥,进行破解。

非对称加密: 效率低,服务端使用私钥加密传输,浏览器用公钥解密,但公钥是明文的,也就无法保证发送给浏览器的数据安全。

对称加密和非对称加密: 遇到DNS劫持,浏览器不知道自己访问的是攻击者的站点。

如何解决DNS劫持的问题?

使用CA认证,让站点自己证明自己的身份。

CA的认证过程?

首先站点需要去申请自己的CA证书,然后这个证书是谁颁发的,也就中间CA机构,向上的证书链一直找到根CA为止。

如何与服务器保持长连接?

ajax轮询: ajax每隔一段时间向服务端发起请求,保持数据的同步。缺点是效率低,资源浪费。

long poll长轮询: 请求头的connection需要设置为keep-alive,客户端发送请求后,如果没有数据返回,服务端将这个请求挂起放入队列,直到有数据返回,客户端再次发起请求,以此轮询。优点是能减少无效的网络传输;缺点是无法处理高并发的场景。

iframe长连接: 在网页上嵌入一个iframe标签,它的src指向一个长连接请求。优点是消息传输及时;缺点是消耗服务器资源。

websocket: 双向通信,只需要连接一次,就可以相互传输数据,适合适时通讯,数据适时更新等场景。websoket协议与http协议没有关系,它是一个建立在tcp协议上的全新协议,为了兼容http握手规范,在握手阶段依然使用http协议,握手完成之后,数据通过tcp进行传输。优点是双向通信,不存在跨域限制,只用建立一次连接;缺点是长连接受网络限制比较大,需要处理好重连,只支持ie10以上版本。

websocket与http的区别:

  1. url以ws:或wss:开头。
  2. 状态码是101。
  3. 请求头和响应头的connection的值是upgrade,表示协议升级。
  4. 请求头和响应头会有sec-websoket字段。

webpack的构建怎么优化?

构建速度优化:首先是分析,使用speed-measure-webpack-plugin插件分析下看是哪些部分打包速度慢,可以针对性的优化。

开启多线程打包:

  1. 使用HappyPack或thread-loader开启worker多线程打包,推荐使用thread-loader,这是官方自带,HappyPack作者已经不维护。

利用缓存加速第二次构建速度:

  1. 开启babel-loader缓存,在loader的后面增加字段cacheDirectory=true开启压缩插件的cache。
  2. 开启压缩插件的缓存,推荐使用terserPlugin。
  3. 开启磁盘缓存,这个是提速最明显的,使用hard-source-webpack-plugin插件。

减少文件搜索范围:

  1. 限制babel-loader编译范围,如exclude: /node_modules/,不编译npm包。
  2. 配置别名,alias更快查找。

打包体积优化:首先还是分析,使用webpack-bundle-analyzer插件,开启可视化的打包体积展示,看是哪部分的体积大,如果polyfill太大则使用动态polyfill,使用cdn抽离公共包等。

使用压缩插件:

  1. terser-webpack-plugin

预编译分包:

  1. 使用内置插件webpack.DLLplugin,webpack内置的插件,需要单独新建一个配置文件webpack.dll.config.js,打包后会生成一个manifest.json文件,对应的是打包出来的名称和包信息,如果需要使用,还要在生产的配置里使用webpack.DllReferencePlugin进行require引用,最后再入口进行dll引用。
  2. 开启图片压缩,使用image-webpack-loader在解析图片的file-loader之后添加,让图片压缩先执行先执行

从输入浏览器网址到页面渲染经历了什么?

首先浏览器会判断输入的是否是网址,如果是关键字这会调用默认设置的搜索引擎,是网址首先会执行当前页面的beforeonload事件。然后浏览器进程将URL发送给网络进程,发起URL请求。

再发起之前还会尝试去命中缓存,首先是命中强缓存,会依次从service worker、内存、硬盘、push cache里读取缓存,如果有命中就不发起请求。没有命中则正常发起请求,首先是DNS解析将域名解析为IP地址,找到对应的主机。

然后发起请求资源的请求,如果协议是HTTPS,还需要建立TLS连接。发起HTTP请求之前需要建立TCP连接,三次握手确保双方通信的可靠性之后发起HTTP请求,这个时候会尝试去命中协商缓存,如果没命中发送HTTP的请求行、请求头、请求体,等待服务端处理并响应,如果返回的状态码是301/302,则进行重定向,重新发起导航流程。否则根据content-type的类型响应具体的内容,浏览器拿到资源后四次挥手断开TCP连接,如果有开启connection:keep-alive可保持TCP连接,浏览器进入页面渲染阶段。

简要说明下浏览器拿到响应数据后的渲染过程?

整个过程也可以称之为渲染流水线:

  1. 首先是构建DOM树,将标签解析为一个个的Token,依次压入弹出token栈确认标签的父子关系,构建为一颗树结构。

  2. 接下来是构建CSSOM,将link标签、style标签、内联的css解析为styleSheet,将属性标准化,如将em转为px、颜色关键字转为rgb值、属性关键字转为具体数值,通过继承、层叠规则遍历DOM树匹配对应css属性,让每个DOM拥有各自对应的样式属性,在浏览器的elelemt-computed里可以看到。

  3. 接下来将CSSOM和DOM进行属性的计算,重新计算为一颗只包括可见元素的布局树,隐藏的元素不参与布局计算。

  4. 接下来是分层,例如绝对定位的元素能遮罩其他元素,是因为它们都不在一个图层,需要将布局树再次计算为一颗包含了上下级关系的图层树。

  5. 再接下来是绘制,根据图层关系逐步绘制,先绘制底部再绘制上层,按照顺序组成一个待绘制列表。

  6. 不过绘制也不是整个页面全部都绘制,优先是绘制浏览器视口以内的图层,所以还有一个分块的步骤,将图层划分更小的块,先绘制视口内的小块。

  7. 接下来是光栅化,使用GPU将当前视口的图块渲染为位图,将其保存到GPU内存里。

  8. 最后光栅化完毕,通知浏览器进程,将GPU内存里的位图显示在屏幕上。

重排:通过JS或CSS更改了元素的几何位置,触发重排,从布局阶段开始重新渲染整个流水线。

重绘:通过JS更改元素外观,触发重绘,从绘制阶段开始渲染流水线,省去了布局和分层阶段。

合成:不更改布局和外观属性,例如用transform实现动画,直接在合成线程完成动画操作,CSS的will-change属性,提前告知浏览器需要进行CSS动画,为其单独准备一个独立的层,之后通过合成线程完成动画。

如何减少回流和重绘?

使用transform替代位移,使用translate3d开启GPU加速

尽量使用visibility替代display:none

不要在循环里读取节点的属性值

动画速度越快,回流次数越少

opacity:0、visibility:hidden、display:none的区别?

opacity:0。元素会隐藏,会出现在布局树里,能点击,会占据原有的空间大小,同样可以影响子元素。

visibility:hidden。元素会隐藏,会出现在布局树里,不能点击,会占据原有的空间大小,子元素会继承,也可以修改该属性的值。

display:none。元素会隐藏,不会出现在布局树里,不能点击,也不会占据原有的空间,会影响子元素。

谈下http各版本之间的区别?

http每一次版本的升级都是解决之前版本所遗留的问题。

最早是http-0.9,它的出现主要解决的问题是让文本信息可以在互联网上进行传输,主要问题是只支持HTML纯文本传输,只支持get请求,没有请求头请求体,服务端没有响应头信息;

http-1的出现增加了请求头与响应头,增加了状态码,响应资源不仅限于HTML,增加了get、post、head请求方式,主要问题依旧是无状态;

然后是http-1.1的出现,增加了connection:keep-alive,让TCP请求得以复用;增加了content-range,可以执行范围请求。依然还是有问题,TCP慢启动,多条TCP会竞争带宽,HTTP队头阻塞,一个请求没处理完,后面会被阻塞;

然后是http-2,增加二进制分帧层支持多路复用,只使用一个TCP长连接传输数据,只经历一次TCP慢启动,也避免多个TCP竞争带宽,并发请求解决队头阻塞问题;还增加了请求头与响应头压缩;服务端主动推送。也有新的问题,TCP包队头阻塞,因为只使用一条TCP的缘故,如果丢包率达到2%,性能就不及1.1。

最后就是目前最新的HTTP-3,在UDP协议基础上实现的QUIC协议,实现了TCP的传输可靠性、集成TLS加密功能、解决TCP队头阻塞、增加了快速握手。

webpack打包构建的过程?

当执行命令进行打包时,首先会找到入口文件,也就是node_modules/webpack/bin/webpakc.js,首先会检查是否有安装官方的脚手架工作,也就是webpack-cli或webpack-command其中的一个,如果一个都没有,则提示要安装,如果安装两个口也移除其中的一个。当脚手架正常时,在脚手架文件里首先需要处理将配置文件里的配置项和命令行里之后的配置进行合并,命令行的优先,合并成一个webpack内容识别的配置。将这个配置传入一个webpack实例内,返回一个compiler对象,执行这个对象的run方法开始编译。

compier对象又是继承与tapable类,这是一个类似eventEmit的事件中心库,在webpack的整个生命周期中的某些节点会触发,当需要监听某些节点的触发时,可以在插件内注册对应的钩子函数监听。

在实例化compiler对象时,首先会初始化webpack内置的插件,将它们的实例挂在到compiler对象上,将compiler对象传入到每个插件的apply方法内,然后执行run方法开发编译。执行内部的一个compie方法,实例化compilation对象,这个对象主要作用是负责整个编译过程。

执行这个对象的addEntry找到入口文件,然后是执行buildModule,执行模块构建的工厂方法,使用loader-runner对不同的文件执行配置文件里的loader,使用acorn将loader处理的文件生成为AST并遍历它,遇到require依赖时,将其加入到依赖数组,递归的对其依赖执行这个过程,当处理完毕之后执 行seal方法。

然后是执行optimization对代码进行优化,最后将生成好的代码保存到cimpilation.assets中,通过emitAssets将最终的文件输出到output的path中。

谈谈vuex原理?

执行Vue.use时会执行vuex的install方法,会往全局混入一个全局的mixin,只有一个属性beforeCreate,它的作用是让每个组件可以访问到this.$store属性。

执行new Vuex.Store时会将传入的配置进行格式化处理,会递归的注册每个module的state、getters、mutation、actions属性,将每个module的getter、action、mutations放入一个对象里,对应的key前面会加上模块名,而state会放入一个有上下级关系的对象里。

内部会重写commit和dispatch,再当前模块触发状态变更时会自动在要触发的commit和dispatch前面加上模块名。 最后会提供一些map开头的语法糖使用。

谈谈vue-router原理?

执行Vue.use安装router时会向全局混入一个mixin,这个mixin只有一个属性beforeCreate,它的作用是让每个组件拥有this.$routerthis.$route属性,执行beforeCreate钩子时会执行router实例的init方法,然后会注册全局组件router-view和router-link。

执行new VueRouter时会将router的配置构建为路由组件映射表,path对应的组件还有name对应的组件,然后内部出初始化一个history属性,有一个History类,又会有三个继承自它的子类,mode的不同histiry实例化的子类不同,这个类有路径跳转transitionTo、push、replace等方法。

注册完毕之后开始执行全局注入的beforeCreate钩子,内部会执行router实例的init方法,内部会执行transitionTo方法,这个方法主要处理三件事,导航守卫的执行,url的变化、和router-view的渲染。 首先处理导航守卫,计算出新的路径,然后按照导航执行的顺序将所有的导航放入一个队列里面,例如首先是组件的失活钩子、接下来全局的before钩子和路由更新钩子等,执行内部的runQueue方法,每个钩子的内部需要执行next才会执行下一个导航钩子。

接着是更新url,将现有路径#之后的内容替换成目标fullPath生成一个新的url,然后调用浏览器的pushState,将这条记录压入历史栈。

最后是router-view的渲染,因为router-view存在嵌套使用的原因,所以首先是计算的嵌套的深度,如果一个组件的父组件是由router-view渲染的,深度值就会+1,一个路径会按照从父到子放入matched数组里,从而根据深度值能准确的渲染对应的组件,最后是执行render函数的h方法进行组件的渲染。

webpack的loader为什么是从右往左执行?

函数组合的形式采用的是compose形式,先会执行右侧的函数,然后将结果依次给到左侧。而每个loader其实就是一个函数,输入的是loader正则匹配的文件,输出的是一个webpack模块,而这个模块会传个下一个loader继续处理。

compose = (f, g) => (...args) => f(g(...args))

如何编写一个webpack的loader?

loader简单理解就是一个函数,接受的参数是test匹配的文件源码,还有loader设置时的options对象传入的值,这个函数做的事情就是对传入的文件源码进行处理,然后导出一个js模块,传给之后的loader继续使用。导出可以在源码前加上export default code,或者使用callback(null, code)的形式。

如何编写一个webpack的插件?

插件是作用是在整理构建生命周期中都可以被使用到,自己编写插件时需要导出一个class类,需要有一个apply方法,这个方法的第一个参数就是compiler的对象,这个对象是继承自tabpad的,然后是监听构建过程中不同的hooks来确定执行的时机,例如文件的写入监听emit钩子,将文件设置到compilation.assets对象上,还有其他的make/run等钩子。

说说JS的垃圾回收机制?

这个问题首先要从变量的类型开始说起,如果是值类型的变量,它们是直接存储在调用栈里,当调用栈弹出时,变量占据的内存随之回收。如果是引用类型的话,调用栈里存储的是指向堆里的指针,当调用栈弹出的时候,栈空间会清空回收,而堆里占用的空间就需要使用垃圾回收机制。

堆里面的内存分为新生代和老生代两个区域,新生代里存放的是新加入的对象,使用副垃圾回收器。老生代里存放的是生存时间久的对象,使用主垃圾回收器。回收器的回收机制是标记活动对象和非活动对象,标记完成后首先回收非活动对象的内存。

副垃圾回收器:使用scavenge算法,又分为对象区域和和空闲区域,当对象区域快满的时候就会执行一次垃圾清理操作,清除非活动对象,然后将活动对象整体有序复制到空闲区域,也完成了内存整体的操作。复制完成后两个区域也就进行了角色翻转,对象区域变成了空闲区域,空闲区域变成了对象区域。如果一个对象经历两次翻转还是存货状态就会被移到老生区里去。

主垃圾回收器:主垃圾回收器采用的标记-清除的算法进行垃圾回收(引用计数不是主流回收机制),标记阶段是遍历调用栈,能够访问到的对象就是活动对象对象,访问不到的就是垃圾数据会被清除,

XSS如何防范?

跨站脚本,主要危害是以下四种:

  1. 主要危害是窃取cookie。
  2. 监听用户的键盘事件。
  3. 修改DOM伪造登录窗口,获取输入的用户名和密码。
  4. 生成页面浮窗广告。

攻击方式分为存储型、反射型、基于DOM型三种方式注入恶意脚本:

存储型:将恶意脚本以输入的形式(名称/评论等)提交到数据库,用户读取到恶意脚本时会请求远方的脚本,然后将用户的信息发往恶意的服务器。

反射型:模拟用户的输入,将恶意的脚本地址拼接到url的query参数里,转而在用户的url上执行。攻击方式是诱导用户去点击一些已经拼接好的恶意链接。

DOM型:通过WiFi劫持、本地软件等方式修改web页面数据。

防范方式:

  1. 过滤关键字,如左尖括号<转为&lt,右尖括号转为&gt。
  2. 敏感的cookie信息设置为httpOnly。
  3. 使用CSP内容安全策略,限制其他域下的资源文件;禁止向第三方域提交数据;禁止内联脚本和未授权脚本的执行;提供上报机制,尽快发现XSS漏洞。

CSRF如何防范?

跨站请求伪造,利用用户的登录状态,去发起请求。

攻击方式:

  1. 自动发起get请求:如利用img标签的src属性,仿造是一次图片的资源请求,其实发起的一次get请求。
  2. 自动发起post请求:构建一个隐藏的form表单,用户访问自动提交表单,可能表单是一次转账操作。

防范方式:

  1. 设置cookie的SameSite属性,设置cookie的携带,Strict只允许同源携带;Lax为get请求会携带;None为任何时候都会携带。
  2. 验证请求的来源站点,验证Referer和Origin属性。
  3. CSRF Token,请求需要携带上CSRF Token,从第三方发起的请求没有这个从而被拒绝。

如何做优化性能?

主要是从两个方面着手:

一、加载更快:

  1. 减少请求的次数:路由懒加载、图片懒加载、雪碧图、小图片转base64
  2. 减少RTT(TCP请求数据包往返延迟,一个包是14k)次数,首屏将css或js该为内联的形式,静态资源使用CDN,缩短RTT时长。
  3. 减少单次请求时间:代码压缩;使用import/export语法,让tree-shaking生效;使用image-webpack-loader对图片进行压缩。
  4. 使用HtmlWebpackTagsPlugin提取公共资源包以CDN形式引入。
  5. 合理使用浏览器的强缓存和协商缓存。

二、渲染更快:

  1. css放文件头部、js放底部,合理使用异步加载js,防止渲染阻塞。
  2. 合并DOM操作,减少回流和重绘的触发,位移操作使用transition3d,开启硬件加速。
  3. 对频繁触发的事件进行节流或防抖。
  4. 将脚本的计算任务分配给Web Workers。
  5. 避免频繁的创建临时引用类型变量,减少垃圾回收次数。

前端的性能指标有哪些?

  1. FP(First Paint):浏览器绘制的第一帧非背景内容。
  2. FCP(First Contentful Paint): 浏览器绘制的第一帧文本、图片等内容的时间。
  3. LCP(Speed Index):首屏时间。
  4. FI(Fisrt CPU Idle):首次CPU空闲时间,页面最小可交互时间。
  5. TTI(Time to Interactive):完全可交互时间。
  6. MPFID(Max Potential First Input Delay):页面加载最繁忙时,能响应用户输入所需的时间。

如何优化以上指标?

FP:在解析HTML之前渲染进程生成一个空白的页面,创建空白页面的节点称之为FP。如果该指标时间过久,说明HTML文件由于网络原因导致加载过久。

FCP:在关键资源加载完毕之后,会执行渲染流水线,当页面中绘制了第一个像素时,这个时间点就是FCP。如果该指标不达标,可能是资源加载太慢,或脚本的执行效率问题。

LCP:当首页内容完全绘制完成时,这个时间时间点又被称为LCP。优化和FCP一样,加载导致的过慢。

FI:优化方式和LCP一样。

TTI:推迟执行一些和生成页面无关的脚本工作。

MPFID:计算任务扔给WebWorker,减少主线程压力;重构CSS,减少层级。

DOMContentLoad:DOM生成完毕之后会触发该事件。

onload:当所有资源都加载结束之后,在触发onload事件。

以上提到的优化很多是加载导致的,那加载的性能指标有哪些?

在network面板可以查看每个请求具体信息,其中的timing就是请求花费的时间:

  1. Queued:发起一个请求之后排队的时间,HTTP2之前的流程只能同时建立6个TCP连接,如果都出于忙碌也就有处于排队状态,升级到HTTP2解决。
  2. stalled:停滞。
  3. Initial connection/SSL:与服务器建立连接,包括TCP、SSL握手时间。
  4. Request send:建立连接后发送出数据的时间。
  5. TTFB:服务端响应第一个字节的时间。性能原因可能是服务端生成数据过慢、或网络延迟、或请求头多余信息太多,如cookie等。
  6. Content Download:从第一字节到全部响应数据接收完毕。性能过慢的原因可能是响应数据太大,压缩代码去注释等方法解决。

前端如何进行异常/性能监控?

性能监控可以查看preformance或audis/lighthouse,在用户实时使用的过程中,监听到性能。

异常捕获:

  1. try-catch:局部可疑代码错误捕获
  2. window.onerror:可以捕获到同步错误,无法捕获静态资源异常和js代码错误
  3. 资源错误捕获:在资源文件上加上onerror事件
  4. window.addEventListener('error', error=> {...}):在事件捕获阶段进行错误捕捉
  5. unhandledrejection事件:捕获promise错误
  6. Vue.config.errorHandler = (err, vm, info) => {...}:vue异常