面试之复盘(二)

1,634 阅读8分钟

性能优化

网页性能优化可以从哪几方面入手?

提高访问和响应速度->提高用户体验!

第一类是页面级别的优化;第二类则是代码级别的优化。

  • 代码优化
    • DOM渲染优化
      • 事件委托
      • 使用文档碎片减少DOM交互次数
      • 使用innerHTML
      • css硬件加速(GPU加速)
      • 缓存DOM节点
      • 采用基于vue/react数据影响视图的模式
      • 分离读写操作
      • 动画效果应用到position属性值为absolute或fix的元素上(脱离文档流)。
    • 慎用with(增加了查找作用域的消耗时间)
    • 避免使用 eval和 Function(将源代码转换成可执行代码很消耗资源)
    • 算法优化
    • js和css的引用位置
      • 将外部脚本置底(防止阻塞其他资源的加载)
      • css引用放在HEAD中(加快渲染,减少页面空白时间)
  • 浏览器
    • 减少HTTP请求
      • 使用缓存
      • 请求资源的合并
      • 避免重定向(增加多余请求)
    • 缩短请求时间
      • 减少DNS查找
      • 减少重复代码
      • 请求资源的压缩
    • 网络安全
  • 资源加载
    • 懒加载(按需加载)
    • 资源压缩(减小体积)
    • CDN(本质仍然是缓存)

For of 和 for (let i = 0; i < ...) 哪种写法性能更高?为什么?

for > for-of > for-in

for-in循环除了遍历数组元素以外,还会遍历自定义属性。

for-of循环不会循环对象的key,只会循环出数组的value,因此for-of不能循环遍历普通对象。

for-of和for-in的key是String类型,有转换过程,开销比较大,但是for循环的i是Number类型,开销较小。

不过for-of语法在内存占用上也有一定的优势。

模块化

了解过js的模块化吗?node如何实现模块化?ES6的模块化又是什么?

模块化就是把单独的一个功能封装到一个模块(文件)中,模块之间相互隔离,但是可以通过特定的接口公开内部成员,也可以依赖别的模块。

Nodejs通过 CommonJs 实现模块化。在 CommonJs 的模块化规范中,每一个文件就是一个模块,拥有自己独立的作用域、变量、以及方法等,对其他的模块都不可见。每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。require 方法用于加载模块。

ES6语法规范中,在语言层面上定义了ES6模块化规范,是浏览器与服务端通用的模块化开发规范。ES6模块化中每个js文件都是一个独立的模块,导入模块成员使用import关键字,暴露模块成员使用export关键字。

webpack工作流

  • 初始化参数

    从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

  • 开始编译

    用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;

  • 确定入口

    根据配置中的 entry 找出所有的入口文件;

  • 编译模块

    从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

  • 完成模块编译

    在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

  • 输出资源

    根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

  • 输出完成

    在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

微前端

讲讲什么是微前端,微前端解决什么问题?

  • 什么是微前端 微前端就是将不同的功能按照不同的维度拆分成多个子应用,通过主应用来加载这些子应用。

核心在于拆,拆完后再合。

  • 解决什么问题

    • 不同团队间开发同一个应用技术栈
    • 每个团队都可以独立开发,独立部署
    • 项目中还需要应用老的代码

    将一个应用划分成若干个子应用,将子应用打包成一个个的lib。当路径切换时加载不同的子应用,这样每个子应用都是独立的,技术栈也不用做限制,从而解决了前端协同开发的问题。

  • 与iframe区别

    如果使用iframe, iframe中的子应用切换路由时用户刷新页面,路由状态丢失,无法实现很多功能。

浏览器机制

浏览器执行js的时候是否会阻塞html的解析?为什么?

在构建DOM树的时候,当遇到JS元素时,HTML解析器就会将控制权转让给JavaScript引擎线程,该线程会阻断HTML解析器的运行,当js加载并且执行完毕后,JavaScript引擎线程会将控制权还给HTML解析器,让其去继续构建dom树。

现在有十个相同大小的css文件,加载一个要n秒,加载10个要多少秒?js呢?

Chrome 中所有 link 一开始会同时建立连接,但每次只并行加载 6 个资源,其余资源会被延迟加载。所以应该是2n秒。

JS 都是串行加载,所以需要10n秒。

浏览器多线程机制

  • GUI渲染线程

负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

  • JS引擎线程

也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

JS引擎线程负责解析Javascript脚本,运行代码。

JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序。

  • 事件触发线程

归属于浏览器而不是JS引擎,用来控制事件循环。(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)

当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。

  • 定时触发器线程

setInterval与setTimeout所在线程。

浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确) 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)

  • 异步http请求线程

XMLHttpRequest在连接后是通过浏览器新开一个线程请求。 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

浏览器缓存如何实现

  • 服务器首先产生ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
  • 304是HTTP状态码,服务器用来标识这个文件没修改,不返回内容,浏览器在接收到个状态码后,会使用浏览器已缓存的文件。

Vue

Vue的生命周期

四个阶段

  • 初始化阶段 new Vue()到created阶段,在Vue.js实例上初始化一些属性、事件以及响应式数据。
  • 模版编译阶段 created到beforeMount阶段,将模版编译为渲染函数。
  • 挂载阶段 beforeMount到mounted阶段,Vue.js会将模版渲染到指定的dom元素中。在挂载的过程中,Vue.js会开启watcher来持续追踪依赖的变化。
  • 卸载阶段 调用$destory()后,Vue.js将自身从父组件中移除,取消实例上所有依赖的追踪并且移除事件监听器。

执行异步队列之前为什么要根据id排序呢?

flushSchedulerQueue中首先会将队列中所有的 watcher 按照 id 进行排序,之后再遍历队列依次执行其中的 watcher,排序的原因是要保证 watcher 按照正确的顺序执行(watcher 之间的数据是可能存在依赖关系的,所以有执行的先后顺序,可以看下 watcher 的初始化顺序)。此时的 flushSchedulerQueue 已经通过 nextTick(flushSchedulerQueue) 变成了异步执行,这样做的目的是在一个事件循环(clickHandler)中让 flushSchedulerQueue 只执行一次,避免多次执行、渲染。