web 前端面试大概念问题

128 阅读16分钟

web 前端面试大概念问题

还是bitnara

  • 缘起:

web 前端面试有一些基本的必考问题,所以在这里记录一下,做一个总结;

  • 浏览器的渲染

耗时的角度看,渲染加载界面的耗时主要体现在: DNS查询 TCP连接 HTTP请求及响应 服务器响应 客户端渲染

客户端渲染的基本步骤是 : 处理HTML构建dom树, 处理css 创建cssom树,将dom树与cssom树合并为一个渲染树,根据渲染树来布局,计算每个节点的几何信息;最后将每个节点绘制在屏幕上;

css 会阻塞dom 树的渲染; 所以在使用css 和 js 文件的时候,最好把css文件放到文档的首页 , js 文件放到末尾的位置;

css对象模型(cssom) , 是树形形式的所有的css选择器和每个选择器的相关属性的映射,具有树的根节点 , 同级、后代、子级和其他关系; 直到所有的css 加载完成之后,cssom 树才完整的创建,因为css 会被重载和覆盖; css 的rule tree 是匹配并将css rule 附加在rendering tree 上面; 还要补充一点,css 和 dom 合并称为frame , 之后的layout 和 paint 都是对frame 进行的操作;

dom绘制过程.jpg

cssom 和 dom 创建之后,浏览器需要计算布局和绘制,calculate layout and paint ; 计算的过程可以成为layout 和 reflow;

repaint:是颜色发生变化,不需要重新计算位置; reflow: 是尺寸发生变化, 要重新计算位置;

注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。 所以相对来说 , 使用visibility 会对性能有着更好的发挥;

这里说一下减少reflow / repaint 的方法:

减少把dom 变脸作为变量在js 代码中进行处理; 尽量修改层级比较低的dom; 不要使用table 布局 , 使用table 布局的话,修改一点,会触发整个dom 的重绘操作;

借鉴了左耳朵耗子的文章 , 关于浏览器渲染原理介绍的,coolshell.cn/articles/96… , 被左耗子老师的身深厚功夫打动;

  • 浏览器缓存相关的知识: 输入一个url 之后 , 这个过程应用了哪些缓存,如何优化:

首先是url 解析为 ip 地址 , DNS server ,之后是TCP 三次握手,建立连接; 之后是发送http 请求 , 服务端相应http 请求并返回http 响应报文 , 浏览器进行layout ,TCP四次挥手结束连接

这上面考察的知识点会非常多,首先各个环节优化的问题 , dns 解析ip 地址,本地dns缓存是一种优化手段,http 请求里面涉及到很多的缓存知识, 包括协商缓存和强缓存,

github.com/amandakelak…

首先说一下基本的原理:

浏览器在请求资源的时候,根据请求头的expires 和 cache-control 来判断是否命中强缓存,是的话直接从缓存中读取资源,不会发送请求到服务器; 如果没有命中,那么浏览器一定会发送一个请求到server , 通过last-modified 和 etags 验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回资源,依然是从缓存中读取资源;

都没有命中的情况,才是从server 中读取资源;

expires 是http 1.0 时期的header , 表示的是一个绝对时间,是由服务器返回的;cache-control 是http 1.1 时期的header , 优先级是高于expires , 表示的是相对时间;

cache-control : max-ag = 315360000

cache-control 常见的值:

cache-control: no-cache;
cache-control: no-store;
cache-control: public;
cache-control: private;

no-cache 也是会存储数据到浏览器的,只是在验证的时候,不会使用浏览器缓存;

no-store 才是不会缓存的;

public: 可以被多用户共享 , 包括终端和中间的cdn 节点;

private: 只能被浏览器缓存,不允许中间的节点缓存;

这个是不同的 cache-control 作用的流程图

协商缓存是使用[last-modified , If-Modified-Since] 和 [ETag , If-None-Match] 这两对header 进行缓存的;

Last-modified 表示本地文件的最后修改时间,浏览器会在request header 加上If-Modified-Since , 询问server 在该日期后资源是否有更新,有更新的话就会在新的资源发送回来;

Etag 类似于指纹 ,文件发生变化都会导致Etag 变化 ; 所以Etag 可以保证每个资源是唯一的; If-None-Match 的header 会将上次返回的Etag 发送给server , 询问该资源的Etag 是否有更新 , 有更新的话 , 会发送新的资源回来;

流程图:

Etag.png

Etag 的优先级比Last-Modofied 高;

因为Last-Modofied 精确的时间只是到秒的级别 , unix 服务器的精确时间也是到秒的级别;所以使用Etag 是很有必要的;

延申:

cookie , session 的区别:

事实上,cookie 和 session 一个在浏览器上,一个在server 上,是没有比较的必要的,这里进行比较的想法是在于对比记忆:

cookie 大小是4KB ,常见的配置如下:

Expires :cookie最长有效期
Max-Age:在 cookie 失效之前需要经过的秒数。(当Expires和Max-Age同时存在时,文档中给出的是已Max-Age为准,可是我自己用Chrome实验的结果是取二者中最长有效期的值)
Domain:指定 cookie 可以送达的主机名。
Path:指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部
Secure:一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器。
HttpOnly:设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)。 [zoom面试中考察了web 安全的问题,自己没有回答上来。。。]

session 的机制:
当程序需要为某个客户端的请求创建一个session时,

服务器首先检查这个客户端的请求里是否已包含了一个session标识------------称为session id,如果已包含则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。

Storage: LocalStorage 和 sessionStorage 的区别:

sessionStorage 临时存储在session中 , 浏览器Tab 页关闭,数据消失; 不同页面不可以共享sessionStorage;

indexDB: 是web 数据库;

  • script 标签和 和 link 标签对加载的影响:

分析的角度: GUI 进程和JS 进程是互斥的,所以标签的位置是会影响加载顺序的; script 标签是阻塞dom 的解析,阻塞dom 的解析不是页面不渲染,而是会触发paint 操作;

标签不会阻塞dom 解析但会阻塞dom 渲染; 标签会阻塞js 脚本的运行;

总结一下: script 标签会阻塞dom 的解析和渲染; link 标签不会阻塞dom 解析,但是会阻塞渲染;link 还会阻塞script 标签的执行;

  • async 和 defer 的区别:

async : 异步 , defer :同步

看到一篇文章的解释:

<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

<script async src="script.js"></script>

有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

<script defer src="myscript.js"></script>

有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

这里在总结一下,async 是js 脚本加载完之后立即执行的, defer 是表示在元素解析完成之后执行的 , 在DOMContentLoaded 事件触发之前完成;

计算机网络相关的知识点

桃谷绘里香

  • 解释TCP三次握手和四次挥手:

    TCP三次握手.jpg

    上图是TCP 三次握手的示意图,在这里解释一下:

第一次握手 : 客户端打算建立连接时,向服务器发出连接请求报文段,
此时首部中的同步位 SYN = 1 ,同时选择一个初始需要 seq = x 。 
TCP 规定,SYN = 1 的报文段 不能携带数据,但要消耗掉一个序号。
这时,TCP 客户进程进入 SYN-SENT (同步已发送)状态。
第二次握手:服务器收到连接请求报文段后,如果同意建立连接,则向客户端发送确认。
在确认报文段中应把 SYN 位 和 ACK 位 都置 1 ,
确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。
(这个报文段也不能携带数据,但同样要消耗掉一个序号。)这时 TCP 服务器
进程进入 SYN-RCVD (同步收到) 状态。
第三次握手:客户端收到服务器的确认后,
还要向服务器给出确认。确认报文段的 ACK 置为 1 ,
确认号 ack = y + 1,而自己的序号 seq = x + 1 。
这时, TCP 连接已经建立,客户端进入 ESTABLISHED (已建立连接) 状态。
当 服务器 收到 客户端 的确认后,也进入 ESTABLISHED (已建立连接) 状态。

Tcp 四次挥手的操作:

tcp四次挥手.jpg

第一次握手 : 客户端 的应用进程先向其 TCP 发出连接释放报文段,并停止在发送数据,主动关闭 TCP 连接。客户端把连接释放报文段首部的终止控制位 FIN 置 1,其序号 seq = u ,它等于前面已传过的数据的最后一个字节的序号加 1 。这时 客户端 进入 FIN-WAIT-1 (终止等待1) 状态,等待 服务器 的确认。( FIN 报文段即使不携带数据,它也消耗掉一个序号。)第二次握手:服务器收到连接释放报文段后即发出确认,确认号 ack = u + 1 ,而这个报文段 自己的序号是 v ,等于 服务器 前面已经传过的数据的最后一个字节的序号加 1 。然后 服务器 就进入 CLOSEWAIT(关闭等待)状态。TCP 服务器进程这时应通知高层应用进程(不确定自己是否还有数据要发送给 客户端(所以是四次不是三次)),因而从 客户端 到 服务器 这个方向的连接就释放了,这时的 TCP 连接处于 半关闭(Half-close)状态,即 客户端 已经没有数据要发送了,但 服务器 若发送数据,客户端仍要接收。也就是说, 服务器 到 客户端 这个方向的连接并未关闭,这个状态可能会持续一段时间。客户端 收到来自 服务器 的确认后,就进入 FIN-WAIT-2(终止等待2) 状态,等待 服务器 发出的连接释放报文段。第三次握手:若 服务器 已经没有要向 客户端 发送的数据,其应用进程就通知 TCP 释放连接。这时 服务器发出的连接使用报文段必须使用 FIN = 1。现假设 服务器 的序号为 w(在半关闭状态 服务器 可能又发送了一些数据)。服务器还必须重复上次已发送过的确认号 ack = u + 1。这时 服务器 就进入 LAST-ACK (最后确认) 状态,等待 客户端 的确认。第四次握手:客户端 在收到 服务器 的连接释放报文段后,必须对此发出确认。在确认报文段中把 ACK 置 1,确认号 ack = w + 1,而自己的序号是 seq = u + 1(根据 TCP 标准,前面发送过的 FIN 报文段要消耗一个序号)。然后进入到 TIME-WAIT(时间等待) 状态。请注意,TCP 连接现在还没有释放掉。必须经过 时间等待计时器(TIME-WAIT)设置的时间 2MSL 后,客户端 才进入到 CLOSED 状态。时间 MSL 叫做 最长报文段寿命,RFC 793 建议设为 2 分钟。

还有一个问题: TCP 为什么要经过4次挥手才可以结束连接:最后客户端需要等待一个2MSL的时间,是因为网络包延时的原因会进行重传,在2msl 的时间内,客户端没有接受到server 的包,说明是断开链接成功了;

那为什么是需要三次握手:三次握手是在于由于网络延时造成包传递到server 端,server 端误认为client 又发送了一个包,所以server 会一直等待client 的包 , 这样造成了server 的资源浪费;client 的三次握手可以解决这一个问题;

  • 跨域问题以及常见的解决方案:
  1. 设置代理:vue.config.js
const webpack = require('webpack');

module.exports = {
  devServer: {
    open: true, // 在 DevServer 启动, 且第一次构建完时自动打开网页
    port: 8080, // 端口号
    proxy: { // 设置代理
      '/api': { // 网关地址
        target: 'https://test.com', // 接口的域名
        ws: true, // 启用 websocket
        secure: false, // 如果是 https 协议,需要配置这个参数
        // 开启代理:在本地会创建一个虚拟服务端,然后发送请求的数据,并同时接收请求的数据,
        // 这样服务端和服务端进行数据的交互就不会有跨域问题
        changOrigin: true, // 跨域需开启
        pathRewrite: { // 重写地址
          '^/api': '' // 将前缀 '/api' 转为 '/'
        }
      }
    }
  }
}

上述只是是应用于开发阶段 , webpack proxy 的原理是类似于nginx proxy , webpack 实现上是利用http-proxy-middle 中间件转发;

2.jsonp : jsonp 只能应用于get 请求; 封装的代码 , 注意一下 reduce 的用法;

const request = ({url, data}) => {
  return new Promise((resolve, reject) => {
    // 处理传参成xx=yy&aa=bb的形式
    const handleData = (data) => {
      const keys = Object.keys(data)
      const keysLen = keys.length
      return keys.reduce((pre, cur, index) => {
        const value = data[cur]
        const flag = index !== keysLen - 1 ? '&' : ''
        return `${pre}${cur}=${value}${flag}`
      }, '')
    }
    // 动态创建script标签
    const script = document.createElement('script')
    // 接口返回的数据获取
    window.jsonpCb = (res) => {
      document.body.removeChild(script)
      delete window.jsonpCb
      resolve(res)
    }
    script.src = `${url}?${handleData(data)}&cb=jsonpCb`
    document.body.appendChild(script)
  })
}
// 使用方式
request({
  url: 'http://localhost:9871/api/jsonp',
  data: {
    // 传参
    msg: 'helloJsonp'
  }
}).then(res => {
  console.log(res)
});

上述代码是将jsonp 通过request 进行封装;

  1. 空iframe 加form
const requestPost = ({url, data}) => {
  // 首先创建一个用来发送数据的iframe.
  const iframe = document.createElement('iframe')
  iframe.name = 'iframePost'
  iframe.style.display = 'none'
  document.body.appendChild(iframe)
  const form = document.createElement('form')
  const node = document.createElement('input')
  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener('load', function () {
    console.log('post success')
  })

  form.action = url
  // 在指定的iframe中执行form
  form.target = iframe.name
  form.method = 'post'
  for (let name in data) {
    node.name = name
    node.value = data[name].toString()
    form.appendChild(node.cloneNode())
  }
  // 表单元素需要添加到主文档中.
  form.style.display = 'none'
  document.body.appendChild(form)
  form.submit()

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form)
}
// 使用方式
requestPost({
  url: 'http://localhost:9871/api/iframePost',
  data: {
    msg: 'helloIframePost'
  }
});

为什么iframe 加 form 就可以实现跨域呢: 原因是在于通过iframe 把form 引入页面中,form 表单中我们通常可以使用action 传 url , 在submit 来实现接口请求,所以这里的道理就是这样;

  1. cors :

cors 全称是: 跨域资源共享 , cross-origin resource sharing;cors 允许浏览器向跨源服务器发出xmlhttprequest请求; 实现cors 的关键是server , server 要实现cors 接口;

分为简单请求和 非简单请求: 简单请求: 需要满足两大条件: 1.

head
get
post

2.HTTP的头信息不超出以下几种字段

Accept
Accept-Language
Content-Language
Last-event-ID
Content-Type

非简单请求: 请求方法是 put / delete , 或者content-type : application/json ; 和 jsonp 相比,显然cors 更强大 , 但是jsonp 主要是用来支持老式的浏览器;

  • http 报文的格式:

    http报文格式: 状态行(包括状态码) , 响应头部 , 响应包体;

    http 响应

上图是http 响应的格式;

算法部分

大桥未久

  • 数组去重所有的算法:
  1. 使用set ;
  2. 使用Object.keys;
let arrrrr = ['n', 'i', 'o', '1', 1, '2', 2];
arrrrr.forEach(element => {
  heihei[element] = true;
  hehe[element] = false;
});
console.log(Object.keys(heihei));
  1. 使用暴力算法;
  2. 使用indexOf;
  3. 使用filter
arr = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 213, 132, 123, 11, 11, 22, 2, 22, 23];
newArr = arr.filter((element, index) => {
  return arr.indexOf(element) === index;
});
  1. 使用reduce
var arr = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 213, 132, 123, 11, 11, 22, 2, 22, 23];
var newArr = arr.reduce((cur, next) => {
  cur.indexOf(next) === -1 ? cur.push(next) : '';
  return cur;
}, []);
  • 微信红包是如何实现的: 使用class封装红包的样式,并写动作函数,触发动作的时候,执行函数,弹出一个Dialog , 在Dialog (容器) 里面,封装关于钱的业务逻辑;

抢红包的操作: 强的操作是在cache 层完成 , 通过原子减进行红包递减;拆红包是在数据库完成的操作;入账是异步操作;

  • 给定一组数 , 求和函数是带延时的网络请求的函数 , 如何在最快的时间内计算这组数据的和 封装一个适配器模式 , 监听延时的时间,超过指定的时间,使用自己写的求和函数进行实现;监听的方式可以通过eventEmitter 进行实现;

前端工程化

美竹玲

webpack 阮一峰写了一本书: xbhub.gitee.io/wiki/webpac…

  • webpack 如何拆分大文件: 通过配置webpack 文件名称(hash)可以实现拆分大文件;

  • webpack的基本配置:

可以使用如下经验来配置webpack:

想让源文件加入到构建流程中去被 Webpack 控制,配置 entry。 想自定义输出文件的位置和名称,配置 output。 想自定义寻找依赖模块时的策略,配置 resolve。 想自定义解析和转换文件的策略,配置 module,通常是配置 module.rules 里的 Loader。 其它的大部分需求可能要通过 Plugin 去实现,配置 plugin。

  • webpack devServer 的原理: 我们知道,devServer 是可以主动刷新界面的,那么实现的原理是什么呢: 通过 DevServer 启动的 Webpack 会开启监听模式,当发生变化时重新执行完构建后通知 DevServer。 DevServer 会让 Webpack 在构建出的 JavaScript 代码里注入一个代理客户端用于控制网页,网页和 DevServer 之间通过 WebSocket 协议通信, 以方便 DevServer 主动向客户端发送命令。 DevServer 在收到来自 Webpack 的文件变化通知时通过注入的客户端控制网页刷新。

本人毕业一年半 , 目前正建立完整的前端知识树体系,少不了的,最近在看机会,金三银四,有需要的欢迎联系;wechat : Yingbin192