前端面试——浏览器

232 阅读15分钟

浏览器必备知识

XSS(跨站脚本攻击)

xss攻击是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如cookie等。

避免方式

  • 不使用服务端渲染
  • 对一些敏感信息进行保护,如cookie使用http-only,使得脚本无法获取
  • 对用户输入的地方和变量都进行字符的过滤

CSRF(跨站请求伪造)

CSRF攻击的本质是利用cookie会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。

避免方式

  • 添加验证码验证
  • 使用token验证
  • 限制cookie不能作为第三方使用
  • 进行同源检测

什么是进程和线程?有什么区别?

进程(process)

进程是计算机中正在运行的程序的实例,一个进程就是一个程序运行的实例。每个进程在运行时都会分配独立的内存空间,不同进程之间的内存是隔离的,一个进程的错误不会直接影响其他进程。进程间通信方式有管道,消息队列,共享内存等。进程的切换开销较大。

线程(thread)

线程是进程的子任务,一个进程可以包含多个线程。多个线程可以在同一个进程内并发执行,共享进程的资源。线程间的通信很方便,因为他们共享相同的地址空间。线程的切换开销较小,因为线程共享进程的地址空间,切换时不需要切换内存页表,速度较快。

区别

  • 进程和线程都可以实现并发执行,但进程是独立的执行实体,而线程是依赖于进程的。
  • 进程之间资源相互隔离,线程共享所属进程的资源
  • 创建和销毁线程的开销较小,而创建和销毁进程的开销较大
  • 多线程可以提高程序的执行效率

浏览器有哪些进程?

  • 主进程: 负责处理用户输入,渲染页面等主要任务
  • 渲染进程:渲染进程负责解析HTML,CSS和Javascript,并将网页渲染成可视化内容
  • GPU进程:负责处理浏览器中的GPU加速任务
  • 网络进程:网络进程负责处理浏览器中的网络请求和响应,包括下载网页和资源等
  • 插件进程:负责浏览器插件运行

协商缓存和强缓存的区别

强缓存

使用强缓存时,如果缓存资源有效,浏览器会从本地读取缓存资源并返回200,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是expires和cache-control属性。

  • expires:指定资源的过期时间。在过期时间内,改资源可以被缓存使用,不需要向浏览器发送请求。
  • cache-control:
    • private:仅浏览器可以缓存
    • public:浏览器和代理服务器都可以缓存
    • max-age=xxx:过期时间,单位为秒
    • no-cache:不进行强缓存,但会有协商缓存
    • no-store:不强缓存,也不协商缓存

当上面两种方式一起使用时,cache-control的优先级要高于expires

协商缓存

命中协商缓存的条件:

  • cache-control:no-cache
  • max-age 时间过期

使用协商缓存时,会先向服务器发送一个请求,如果资源没有发生修改,则请求返回304状态,让浏览器使用本地缓存。如果资源发生修改,则返回修改后的内容。

  • last-modified:上次修改的时间,当浏览器发起请求时,会在请求头上添加一个IF-Modified-Since属性,值为上一次资源请求的Last-Modified的值。服务器会通过这个属性和最后修改时间来进行比较,以此来判断资源是否修改。如果没有资源修改,返回304状态,使用本地缓存。如果资源修改,就返回最新资源,200状态。
  • etag:文件资源改动时,这个值也会改变。下次请求资源时,会在请求头中添加If-None-Match属性,为上一次请求的资源的Etag值。服务端会通过这个属性和资源最后一次修改时间进行对比,以此来判断资源是否修改。这种方式比Last-Modified更加准确。

区别

  • 强缓存优先级高于协商缓存
  • 协商缓存不论是否命中都会发起一次请求
  • 强缓存返回200,协商缓存命中返回304
  • ctrl+F5会强制刷新跳过所有缓存,而F5刷新跳过强缓存,但是会检查协商缓存

为什么需要浏览器缓存

  • 减少服务器的负担,提高了网站的性能
  • 加快了客户端网页的加载速度
  • 减少了多余网络数据传输

浏览器内核

  • IE:Trident内核
  • Chrome:Webkit内核,现在是Blink内核
  • Firefox:Gecko内核
  • Safari:Webkit内核
  • 360浏览器:IE+Chrome双内核

浏览器的渲染过程

  1. 浏览器自动补全协议和端口号,如果地址不合法,则会执行默认引擎

  2. 浏览器根据url地址查找本地缓存,根据缓存规则查看是否命中缓存,若命中缓存,则直接使用缓存,不再发出请求

    • 如果是https,先去找service worker,查看是否有离线缓存
    • 如果没有,再找浏览器的内存缓存(memory cache)
    • 如果还没有,再找硬盘缓存:强缓存和协商缓存
  3. 通过DNS解析找到服务器的地址

    • 递归查询有没有dns缓存,如果没有则进行迭代查询获得ip地址
    • 首先,根据dns根域名(.)服务器查询,返回顶级域名服务器ip
    • 再根据返回的ip去顶级域名(.com)服务器查询,返回权威域名服务器ip(xxx.com)
    • 再根据返回的ip去权威域名服务器查询,返回解析到的服务器ip
    • 如果此时配置了cdn的话,权威域名服务器会返回一个CName别名记录,它指向CDN网络中的负载均衡系统, 然后通过智能算法,返回最佳节点的IP
  4. 建立TCP链接

    • 三次握手,第一次客户端发送syn=1和一个初始化序列号seq=x;第二次服务端返回ACK=1,SYN=1,ack=x+1,seq=y;第三次握手,ACK=1,ack=y+1,seq=x+1
    • 如果是https,则多了一个TLS连接建立的过程:
      • 客户端给服务器发送支持的加密方法和TLS版本,以及一个随机字符串
      • 服务器返回加密方法,TLS版本号,数字证书以及一个随机字符串
      • 客户端验证数字证书,确保服务器的合法性;检查数字签名,检查证书有效期,检查证书是否撤销
      • 验证完合法性后,客户端向服务器发送另一个随机字符串,这个字符串是由服务器的公钥加密的,只有对应的私钥才可以解开
      • 服务器使用私钥解开字符串
      • 现在客户端和服务器都拥有三个随机字符串,通过相同的算法生成相同的会话密钥
      • 使用会话密钥进行双方通信
  5. 浏览器决定要附带那些cookie到请求头中

  6. 浏览器自动设置好请求头,协议版本,cookie,发出GET请求

  7. 服务器处理请求,返回一个HTTP响应报文给浏览器

  8. 浏览器根据状态码决定如何处理这一响应:

    • 1xx 信息状态码 100 继续
    • 2xx 成功状态码 200 成功 204 请求成功,但是没有资源返回 206 需要下载文件,由用户决定是否下载
    • 3xx 重定向状态 301 永久重定向 302 临时重定向 304 调用缓存,不必重新请求数据
    • 4xx 客户端错误码 400 请求的语法错误 404 服务器无法找到请求的url
    • 5xx 服务器错误码 500 服务器内部出错 503 服务器正在维护 504 请求超时
  9. 浏览器根据响应头中的Content-Type字段识别响应类型,如果是text/html,则对响应体的内容进行HTML解析

  10. dom树构建

    • html解析器对html进行解析,对字符词法分析,将字符解析成token,对token进行语法分析,转成dom节点对象并定义属性和规则
    • html解析器会维护一个解析栈,栈底为document对象,也就是dom树的根节点。然后根据根节点关系将节点一次推入栈中,形成dom树。
    • 如果解析过程中没有async或defer的script标签引用时,会暂停解析,同时加载js文件,执行相应的代码,代码执行完之后再返回渲染引擎继续渲染流程
    • 在解析过程中还会触发一系列的事件,当dom树完成后会触发domcontentloaded事件。当所有资源加载完毕后会触发load事件
  11. CSSOM规则树构建

    • 首先将CSS进行格式化成StyleSheets,然后对计算好的样式进行标准化操作
    • 样式计算规则主要有继承和层叠。计算阶段就是计算出dom树中的每个节点的位置信息,样式数据,文本节点数据,然后css和html可以同时解析,但是css会阻塞js
  12. layout布局

    • 首先创建布局树,遍历DOM树中的节点,将可见节点添加到布局树中,然后根据DOM结构和元素样式对布局树中节点的几何位置信息进行计算
  13. 分层

    • 因为脱离了文档流的对象会形成一个层叠上下文,所以有了分层的概念。就类似于ps中的图层,然后主线程为每个图层计算样式,把每一个图层的绘制拆分成很多小的绘制指令,生成绘制表。
  14. 栅格化

    • 一个图层可能很大,需要进行分块。另外渲染引擎维护了一个栅格化线程,合成线程将分割好的图块发给栅格化线程,然后分别栅格化每个图块,再将栅格化之后的图块存储在GPU内存中。
  15. 合成和显示

浏览器渲染优化

  • 优化js,js会阻塞html的解析,改变js的加载方式可以防止阻塞

    • 将js尽量放在body最后面
    • 尽量使用异步加载js资源,async,defer
  • 优化css加载

    • css样式少使用内嵌样式
    • 导入外部样式使用link,而不是@import,因为它会阻塞渲染
  • 减少重绘和回流

    • 避免频繁操作样式
    • 避免频繁操作dom
    • 复杂动画使用绝对定位的方式脱离文档流
    • 使用transform代替动画

Cokie,LocalStorage和SessionStorage的区别

Cookie

  • 大小只有4kb
  • 跨域不能共享
  • 不安全,容易被劫持
  • 只存在请求头中

SessionStorage

  • 存储在内存中,体积相对较大
  • 页面关闭,数据会消失
  • 相对Cookie安全

LocalStorage

  • 体积大,可以存储更多内容
  • 生命周期长,除非手动删除,不然会一直存在
  • 存储在硬盘中,不会像cookie一样被请求携带

同源策略

跨域问题其实就是浏览器自身的同源策略造成的。同源指的是:协议,端口号,域名必须一致。

如何解决跨域问题

  • CORS:服务器开启跨域资源共享
  • JSONP:利用script标签不存在跨域限制,只支持get请求,且不安全
  • nginx:代理跨域
  • nodejs:中间件代理跨域,通过node开启一个代理服务器

事件流

事件流分为三个阶段:捕获阶段、目标阶段、冒泡阶段 过程如下:

  1. 捕获阶段:事件从最外层的节点,也就是文档对象开始,逐级向下传播,直到事件的目标节点上。
  2. 目标阶段:事件到达目标节点,触发目标节点上的事件处理函数
  3. 冒泡阶段:事件从目标节点开始,逐级向上传播,直到到达最外层节点

冒泡和捕获的区别?

事件冒泡和事件捕获是两种不同的事件传播方式,默认是冒泡,它们的区别在于传播方向不同:

  • 事件冒泡是自下而上,从子元素冒泡到父元素,执行元素上的事件处理
  • 事件捕获是事件从文档的根元素开始,逐级向下传播到较为具体的元素(即从父元素到子元素)

如何阻止事件冒泡

  • 普通浏览器:event.stopPropagation()
  • IE浏览器:event.cancelBubble = true ;

对事件委托的理解

利用浏览器事件冒泡机制。事件在冒泡的过程中会传到父节点,并且父节点可以通过事件对象获取到目标节点,可以把子节点的监听函数定义到父节点上,由父节点的监听函数统一处理多个子元素的事件

回流和重绘

  • 回流:当DOM变化影响了元素,比如元素的大小,布局,显示隐藏等改变了,需要重写构建。每个页面至少需要一次回流,就是在页面第一次加载的时候,这个时候一定会发生回流。
  • 重绘:当一个元素的外观发生了变化,但是没有改变布局,重新渲染元素的外观。比如background-color,color

如何避免回流重绘:

  • 避免使用table布局
  • 尽可能在dom树的最末端改变class
  • 不要频繁的操作元素的样式
  • 避免设置多层内嵌样式
  • 开启GPU加速
  • 使用absolute或者fixed,脱离标准文档流

回流必将引起重绘,而重绘不一定会引起回流。

对浏览器事件循环的理解

事件循环是一种机制,它会不断的轮询任务队列,并将队列中的任务依次执行。 js的任务分为同步和异步两种:

  • 同步任务:在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务。
  • 异步任务:不进入主线程,而是放在异步队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。

宏观任务、微观任务

  • 宏任务:script全部代码,setTimeout,setInterval,I/O,UI渲染
  • 微任务:Promise.then、Process.nextTick、MutationObserver

任务队列中的任务分为宏任务和微任务,当执行栈清空后,会先检查任务队列中是否有微任务,如果有就按照先进先出的原则,压入执行栈中执行。微任务中产生了新的微任务不会推迟到下个循环中,而是在当前循环中继续执行。当执行这一轮微任务完毕之后,开启下一轮循环,执行任务队列中的宏任务。

一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。

执行顺序

  • 执行宏任务中的同步代码,遇到宏任务或微任务,分别放入对应的任务队列,等待执行
  • 当所有同步任务执行完毕之后,执行栈为空,首先执行微任务队列中的任务
  • 微任务执行完毕后,检查这次执行中是否产生新的微任务,如果存在,重复执行步骤,直到微任务执行完毕
  • 开始下一轮Event Loop,执行宏任务中的代码

Node的事件循环

Node事件循环分为6个阶段,每进入一个阶段,都会去对应的回调队列中取出函数执行。

  1. Timers阶段:执行timer(setTimeout、setInterval)的回调,由poll阶段控制
  2. I/O callbacks阶段:系统调用相关的回调
  3. idle prepare阶段:Nodejs内部执行,可以忽略
  4. poll 阶段:轮询在该阶段如果没有timer的话,会出现以下情况:
    • poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
    • poll队列为空,会出现以下两种情况
      • 如果有setImmediate回调需要执行,poll阶段会停止并且进入到check阶段执行回调
      • 如果没有setImmediate回调需要执行,就会等待回调被添加到队列中,然后立即执行。如果设置里有timer,并且poll队列为空,就会判断是否有timer超时,如果有就回到timers阶段执行回调。
  5. check阶段:执行setImmediate回调
  6. colse callbacks阶段:执行一些关闭回调,比如 socket.on(‘close’,。。。)等。

node和浏览器事件循环机制的区别

  • 浏览器事件循环会在宏任务结束后,检查微任务。而node的微任务是在两个阶段之间执行。
  • 浏览器的process.nextTick和其他微任务优先级一样,而node中要高于其他优先级。