2022面试总结(持续更新中...)

27,614 阅读6分钟

HTML

1. 重绘与重排

一个页面从加载到完成,首先生成DOM树,然后根据DOM节点的几何属性生成render树(渲染树),当渲染树构建完成,页面开始根据DOM树布局,渲染树也会根据设置的样式渲染节点

重排: 当我们删除或修改元素大小时,页面会重新布局,DOM树发生变化,引起渲染树重新渲染,这个过程叫做回流(重排一定造成重绘

引发重排的事件

  • 改变试图窗口的大小
  • 改变文字的大小
  • 改变元素边距
  • 元素的位置重新定位(脱离文档流)
  • 操作DOM
  • 添加/删除样式表,操作class属性,设置style属性
  • CSS动画属性

重绘: 当修改元素的颜色等,渲染树会根据新规则重新渲染,这个过程叫做重绘(重绘不一定造成重排

引发重绘的事件

  • 改变元素外观属性,如修改文字颜色/图片背景色等
  • 所有的重排事件都会引发重绘

如何减少重排

  1. 对DOM进行多次添加删除操作时,使用documentFragment对象(在该对象中对DOM进行操作,完成后append到文档中,即可只进行一次回流)
    function addDivs(element) {
      var div;
      // Creates a new empty DocumentFragment.
      var fragment = document.createDocumentFragment();
      for (var i = 0; i < 20; i ++) {
        div = document.createElement('a');
        div.innerHTML = 'Heya!';
        fragment.appendChild(div);
      }
      element.appendChild(fragment);
    }
    
  2. 操作元素时使用定位(absolute, fixed)脱离文档流或display:none;隐藏该元素
  3. 避免逐项更改样式,将样式列表定义为class并一次性更改class属性
  4. 避免循环读取offsetLeft等属性,在循环之前把它们缓存起来。
  5. 避免使用table布局

浏览器的优化机制 浏览器会维护一个队列,将重绘,重排的事件放到此队列中,当一定数量或时间后执行此次队列,完成一次重绘/重排操作

2. 获取点击元素的下标

------ html-------
<ul>
    <li>Coffee</li>
    <li>Milk</li>
    <li>Soda</li>
</ul>
  1. 原生js
let lis = document.getElementsByTagName('li')
for (let i=0; i<lis.length; i++) {
  lis[i].index = i
}
document.getElementsByTagName('ul')[0].addEventListener('click', handle, false)
function handle (event) {
  if (event.target.tagName === 'LI') {
    console.log(event.target.index)
  }
}
  1. jquery的index()方法
$('li').click(function () {
  console.log($(this).index())
})

CSS

👉 详情

JS、ES6相关

👉 详情

webpack

webpack是模块打包工具,对js模块和扩展语言进行打包供浏览器识别运行

  • webpack只能处理js代码,其他格式需要通过loader进行转换
  • 可对浏览器不能识别的规范和静态文件进行分析、压缩、合并、打包成可供浏览器支持的代码

1. webpack和Grunt以及Gulp相比有什么区别

2. webpack 的 loader 和 plugin 区别,举几个常用的 loader 和 plugin 并说出作用

  • loader用于对模块代码进行转换,可将不同格式语言转换为JavaScript,或将图像转换为Data Url,由于webpack只能够识别JavaScript,所以不同类型的模块需要对应的loader进行转换
  • plugin是webpack的扩展插件,可完成loader无法完成的复杂功能,可控制webpack每个打包环节的流程,极大丰富了webpack的扩展性

3. webpack打包过程

  • 读取文件,分析模块的依赖
  • 对模块进行解析执行(深度遍历)
  • 针对不同的模块使用不同的 loader
  • 编译模块,生成抽象语法树(AST)
  • 遍历 AST,输出 JS

4. webpack优化

webpack优化的两大指标: 构建的体积,打包的速度

  1. 减少loader匹配无用的资源

    • 使用exclude: /(node_modules)/ 可跳过对node_modules文件夹内资源的转译
  2. 设置loader的缓存

    • loader: 'babel-loader?cacheDirectory=true' 将babel-loader转译结果缓存,babel-loader效率增加两倍
    • 下次loader时直接读取缓存,未找到则重新loader
  3. Happypack插件将loader转为多线程

    • webpack转移是单线程进行,使用该插件可设置一个线程池,将loader放入线程池中进行多线程打包
  4. DLL打包

    • DllPlugin插件会将第三方库打包成依赖库文件,这个依赖库文件不会跟随业务代码一起重新打包,只有当依赖关系发生改变时才会重新打包
  5. Tree-shaking(树摇)

    • 基于ES6 module语法(export/import),树摇可以分析出模块的依赖关系,在打包时删除掉未被引用的依赖模块
    • 更适用于模块级的冗余代码处理,更小颗粒度建议配合使用UglifyJsPlugin
  6. UglifyJsPlugin

    • 可将注释,console语句等删除
  7. gzip压缩

  8. 性能分析 webpack-bundle-analyzer

5. 热更新(HMR-Hot Module Replacement)

定义:无需刷新网页,就能完成模块的更新与替换

优点:提高开发效率,优化开发体验

原理请点击👉

Vue

1. 兄弟组件如何通信,无嵌套,回调触发

方法一、通过父组件通信

此方法需要保证兄弟组件A、B都在同一个父组件下;

父组件通过接受子组件A传递过来的事件消息,并调用子组件B

子组件A
this.$emit('transmit', 'msg')
父组件
<ChildA @transmit="transmit"></ChildA>
<ChildB :msg="msg"></ChildB>

transmit (data) => {
    this.msg = data 
}
子组件B、需要使用watch来监听父组件props穿过来的数据变化
watch (new, old) {
    数据操作... 
}

方法二、eventBus

通过创建Bus.js注册一个全局实例,通讯组件通过调用实例的方法达到通讯目的

  1. 创建Bus.js
// eventBus 共享vue实例,用于兄弟组件数据传递
import Vue from 'vue'
const Bus = new Vue({})
export {Bus}
  1. 组件A导入Bus.js 并emit消息
import {Bus} from './Bus.js'

Bus.$emit('transmit', 'msg')
  1. 组件B导入Bus.js并在mounted中检测数据变化
import {Bus} from './Bus.js'

mounted () {
    Bus.$on('transmit', (data) => {
        操作...
    })
}

由于$on事件无法主动销毁,所以需要根据业务手动进行销毁

在组件销毁前方法中销毁
beforeDestory () {
    Bus.$off('transmit')
}

或者在使用前进行销毁
mounted () {
    Bus.$off('transmit')
    Bus.$on('transmit', (data) => {
        操作...
    })
}

方法二、provide/inject

允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效

  • provide 选项应该是一个对象或返回一个对象的函数
  • 绑定的数据是非响应式的,但绑定的是响应式对象则是响应式

2. vueRouter的工作原理

Vue Router 是路由管理器,可以改变url而不向服务器发送请求,页面无需刷新

有hash和history两种路由模式

hash模式

  • #后的hash值改变,会触发onhashchange事件
  • hash的变化会被浏览器记录(历史访问栈)下来,可以前进、后退、刷新而不向服务器发送请求
  • 通过匹配#后面的hash,与vueRouter配置文件里的path对应做路由跳转

history模式

  • 基于浏览器history的pushState()、replaceState()、popState()来实现,可以读取、修改浏览器历史记录栈
  • 可以前进、后退不发往服务器发送请求,但刷新会向服务器发送请求
  • 若匹配不到资源,则返回404,因此需要后台支持,重定向到根目录主页

动态路由匹配中会复用同一组件,这就导致再次访问组件不被重新渲染,声明周期钩子不会执行,这就需要我们用watch去监听路由的变化

watch: {
    $route(to, from) {
        ······
    }
}

router和route的区别

  1. router是vueRouter的实例对象,全局存在,包含全部的路由信息,可进行路由的跳转
  2. route是当前所处路由的对象,每一个路由都有自己的route对象,局部存在,包含query,params,name,path等属性,可对当前路由进行操作

vue Router 详解

3. Vuex的理解

Vuex是全局状态管理中心

Vuex包含五个属性:state、getter、mutation、action、module

  • state:存储数据,存储状态;注册了Store实例后,可通过this.$store.XX来访问;对应组件内的data,数据为响应式,组件通过store来读取vuex的数据,若数据改变,则组件的数据也会改变
  • getter:store的计算属性,它的返回值会被依赖缓存起来,当依赖值发生变化时才会被重新计算
  • mutation:更改Vuex的store中状态的唯一方法是提交mutation
  • action:包含任意异步操作,通过提交 mutation 间接更变状态
  • module:将 store 分割成模块,每个模块都具有state、mutation、action、getter、甚至是嵌套子模块
  • 如果设置nameSpace,则getter,mutation,action访问需要增加子模块名称,state不受影响

当组件进行数据修改的时候,通过调用dispatch来触发actions里面的方法,actions里每个方法都有commit方法,通过执行commit方法来触发mutation里的方法进行数据修改,由于mutation里每个函数都有一个state参数,进而可对state进行修改,当数据修改完毕后,会传导给页面。页面的数据也会发生改变。

4. vue 虚拟DOM *

传统的DOM渲染

  1. 构建DOM树:解析HTML构建成DOM树
  2. 生成样式表:解析CSS样式生成样式表
  3. 生成Render树:结合DOM树和样式表,构建成Render树
  4. 确定几何位置:根据Render树的结构,确定每个节点的几何位置
  5. 渲染页面:根据Render树的结构和元素的几何位置,通过painting方法渲染到页面上

缺点:若更新多个节点,则dom会渲染多次

vue虚拟DOM

  1. 通过render函数生成虚拟dom
  2. 当数据发生改变时,利用diff算法对节点进行对比更新
  3. diff算法会对比两个DOM树同一层级的节点,进行新增,删除,更新
  4. diff之后分别更新真实dom,在全部遍历完成之后,将真实dom插入到body上
  5. 只有元素节点,注释节点,文本节点才会转化成真实dom

渲染列表为什么加key?

key值确定元素的唯一性,当利用diff算法对比虚拟dom树变化时,会通过key字段去做对比,有key值则节省diff算法对比的效率

5. watch用法

watch用来监听并响应数据的变化

  • 可以直接监听基本数据类型数据
  • 若是监听对象,则需要开启deep(深度监听)
  • 若是监听数组,则不需要开启deep监听
  • immediate 初始化绑定值时即执行监听
  • watch首次初始化绑定不执行,但监听的值发生变化时则执行监听
data () {
    return {
        age: 20,
        obj: {
            age: 24
        },
        arr: [1,2,3]
    }
}
1. 监听基本类型
watch: {
    age (newd, oldd) {
        ...
    }
}

2. 监听对象
watch: {
    obj: {
        handler (newd, oldd) {
            ...
        },
        deep: true, // 开启深度监听
        immediate: true // 首次即执行
    }
}
3. 监听对象某个属性

*** 采用字符串
watch: {
    'obj.age': {
        handler (newd, oldd) {
            ...
        }
    }
}

*** 利用computed计算属性
computed: {
    age () {
        return this.obj.age
    }
}
watch: {
    age (newd, oldd) {
        ...
    }
    // 也可写为
    age: {
        handler (newd, oldd) {
            ...
        }
    }
}
4. 监听的对象属性为引用类型

6. vue 生命周期有哪些?

  • beforeCreate ----创建前
  • created ---- 成功创建
  • beforeMount ---- 挂载前
  • mounted ---- 成功挂载
  • beforeUpdate ---- 更新前
  • updated ---- 成功更新
  • beforeDestroy ---- 销毁前
  • destroyed ---- 成功销毁

👉传送门,Vue声明周期详解

7. vue 响应式数据是如何实现的?

绑定响应式的数据都有observe属性

  1. 通过walk方法遍历data里的每个属性值,并调用defineReactive方法
  2. defineReative里实例化dep对象,并通过Object.definePropetry的set和get进行转换
  3. 在get中通过dep.target触发dep.depend()收集依赖,在set中通过dep.notify()触发依赖
  4. depend方法触发Watcher中的addDep,将watcher放入dep的依赖池中
  5. notify遍历依赖池,触发Watcher的update方法更新视图
  6. update方法产生异步队列,批量更新视图
  • 由于Object.observe的废除,导致无法检测对象/数组属性的新增和删除
  • 无法检测对象/数组属性的添加/删除,可通过Vue.set(this.obj, key,value)来向对象动态添加响应式属性
  • 或者通过Object.assign({}, obj) 来创建一个新对象来触发渲染

8. vue nextTick

异步更新队列

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的Promise.then和MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData='newvalue',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新

nextTick

由于DOM是异步执行更新的,有时我们修改完数据等待DOM更新后进行操作,则此刻可使用Vue.nextTick(callback)

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '没有更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '更新完成'
      console.log(this.$el.textContent) // => '没有更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '更新完成'
      })
    }
  }
})

因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的ES2016async/await语法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = 'updated'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

网络

👉 详情

优化

1. 图片优化

图片大致分为png、jpg、gif三种

  • gif体积小,很好的压缩效果,支持动画,支持透明效果;但色彩效果最低
  • png压缩效果好,支持透明效果,色彩不如jpg,最为通用

优化方案:

  1. 雪碧图
    • 即css sprites, 就是把很多小图片制作成一个大图,然后作为背景图片使用,定位即可
    • 优点: 体积小,引入一张图片即可,减少http请求
  2. 图片压缩
    • 使用canvas的drawImage()进行压缩
    • 上传至七牛云或阿里云进行压缩
  3. base64
    • 利用编码存储图片,减少http请求
  4. 响应式图片
    • 利用srcset 和 sizes 属性做响应式图片
  5. 延迟加载
    • 当图片很多时,全部加载耗时过多也很可能造成卡顿
    • 仅加载可视区域内的图片,非可视区域使用通用图片占位,当页面滚动进入可视区域后,再加载此区域内的图片

当加载图片过大时,会出现局部逐步加载情况,用户体验差;可通过哟图片的onload方法来判断图片是否加载完成,未加载完前先display:none;进行隐藏,当onload后在进行加载。

2. 缓存优化相关

👉缓存详解

其他

1. 轮流向圆形桌子放硬币,如果保证能赢,谁先放?

假设桌子小到只能放下一枚硬币,那么先放者赢;若不确定桌子大小,则首先在圆心处放硬币,然后在对手放完后的关于圆心的对称点处再放硬币,确保先放者赢

2. 随机抽样一致性算法

详解