在去年底开始换工作,直到现在算是告了一个段落,断断续续的也面试了不少公司,现在回想起来,那段时间经历了被面试官手撕,被笔试题狂怼,悲伤的时候差点留下没技术的泪水。
这篇文章我打算把我找工作遇到的各种面试题(每次面试完我都会总结)和我自己复习遇到比较有意思的题目,做一份汇总,年后是跳槽高峰期,也许能帮到一些小伙伴。
先说下这些题目难度,大部分都是基础题,因为这段经历给我的感觉就是,不管你面试的是高级还是初级,基础的知识一定会问到,甚至会有一定的深度,所以基础还是非常重要的。
我将根据类型分为几篇文章来写:
面试总结:javascript 面试点汇总(万字长文)(已完成) 强烈大家看看这篇,面试中 js 是大头
面试总结:nodejs 面试点汇总(已完成)
面试总结:浏览器相关 面试点汇总(已完成)
面试总结:css 面试点汇总(已完成)
面试总结:非技术问题汇总(已完成)
我会抓紧时间把未完成的总结补全的~
这篇文章是对 框架 vue 和工程相关
相关的题目做总结,欢迎朋友们先收藏在看。
先看看目录
VUE
这部分是 vue 相关的整理。
响应式原理
响应式原理是 vue 的核心思想之一,后续打算单独整理一篇,这里简单介绍响应式的三大件
Observer
观察者,使用 Object.defineProperty 方法对对象的每一个子属性进行数据劫持/监听,在 get 方法中进行依赖收集,添加订阅者 watcher 到订阅中心。 在 set 方法中,对新的值进行收集,同时订阅中心通知订阅者们。
/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
/*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
/*如果原本对象拥有setter方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)
/*dep对象通知所有的观察者*/
dep.notify()
}
})
在setter中通知观察者更新,在getter中向Dep(调度中心)添加观察者。
watcher
扮演的角色是订阅者,他的主要作用是为观察属性提供通知函数,当被观察的值发生变化时,会接收到来自订阅中心 dep 的通知,从而触发依赖更新。
核心方法有: get() 获得getter的值并且重新进行依赖收集 addDep(dep: Dep) 添加一个依赖关系到订阅中心 Dep 集合中 update() 提供给订阅中心的通知接口,如果不是同步的(sync),那么会放到队列中,异步执行,在下一个事件循环中执行(采用 Promise、MutationObserver以及setTimeout来异步执行)
Dep
扮演的角色是调度中心,主要的作用就是收集观察者 Watcher 和通知观察者目标更新。 每一个属性都有一个 Dep 对象,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的 watch,让订阅者执行自己的 update 逻辑。
computed 为什么比 watch method 性能要好
从编码上 computed 实现的功能也可以通过普通 method 实现,但与函数相比,计算属性是基于响应式依赖进行缓存的,只有在依赖的数据发生改变是,才重新进行计算,只要依赖项没有发生变化,多次访问都只是从缓存中获取。
计算属性是基于 watcher 实现,看看源码
/*初始化computed*/
// 核心是为每个计算属性创建一个 watcher 对象
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
/*
计算属性可能是一个function,也有可能设置了get以及set的对象。
*/
let getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production') {
/*getter不存在的时候抛出warning并且给getter赋空函数*/
if (getter === undefined) {
warn(
`No getter function has been defined for computed property "${key}".`,
vm
)
getter = noop
}
}
// create internal watcher for the computed property.
/*
为每个计算属性创建一个内部的监视器Watcher,保存在vm实例的_computedWatchers中
这里的computedWatcherOptions参数传递了一个lazy为true,会使得watch实例的dirty为true
*/
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
/*组件定义的计算属性不能与 data 和 property 重复定义*/
if (!(key in vm)) {
/*定义计算属性*/
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
/*如果计算属性与已定义的data或者props中的名称冲突则发出warning*/
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
/*创建计算属性的getter*/
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/*实际是脏检查,在计算属性中的依赖发生改变的时候dirty会变成true,在get的时候重新计算计算属性的输出值
*若依赖没发生变化,直接读取 watcher.value
*/
if (watcher.dirty) {
watcher.evaluate()
}
/*依赖收集*/
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
computed 和 watch 主要区别在于使用场景,计算属性更适用于模板渲染,依赖其他对象值的变化,做重新计算在渲染,监听多个值来改变一个值。而监听属性 watch ,是用于监听某一个值的变化,进行一系列复杂的操作。监听属性可以支持异步,计算属性只能是同步。
vue 对数组的处理
在官方文档上关于数组的注意事项有这么一段
由于 JavaScript 的限制,Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
先看第二点,这是因为 Object.defineProperty 不能监听数组的长度,所以直接修改数组长度是没法被监听到的。
关于第一点,我们看看源码的实现
// vue\src\core\observer\index.js
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 在 Observer 构造函数中,对数组类型进行特殊处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
// observeArray 的实现
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // 对数组的值在进行 observe
}
}
observeArray 是对数组中的值进行监听,并不是数组下标,所以通过索引来修改值是监听不到了,假如是通过监听索引的话,那是可以实现的。那为啥不监听下标呢?在 vue issue 中好像记得尤大说是性能考虑。
因为是对数组元素做的监听,那么数组 api 造成的修改自然就没法监听到了,所以 vue 对数组的方法进行了变异,包裹了一层,本质还是执行数组的 api
// vue\src\core\observer\array.js
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// 以下这三个方法,会新增新的对象,所以需要对新增的对象进行监听
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 新对象监听
if (inserted) ob.observeArray(inserted)
// notify change 调度中心通知订阅者
ob.dep.notify()
return result
})
})
vue 中 key 的作用
有两点用处:快速节点比对和节点唯一标识
利用快速节点比对
用作于 vnode
的唯一标识,便于更快更准确的在旧节点列表中查找节点
在内部对两个节点进行比较的时候,会优先判断 key 是否一致,如下,如果 key 不一致,立马就可以得出结果
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
列表节点唯一标识
列表循环 v-for="i in dataList"
会有提示我们需要加上 key
,因为循环后的 dom 节点的结构没特殊处理的话是相同的, key 的默认值是 undefined
,那么按照上面 sameVnode
的算法,新生成的 Vnode 与 旧的节点的比较结果就是相同的,vue会对这些节点尝试就地修改/复用相同类型元素的,这种模式是高效,但是这种模式会有副作用,比如节点是带有状态的,那么就会出现异常的bug,所以这种不写 key
的默认处理只适用于不依赖其他状态的列表。
注意:在不知道哪个版本,vue 对 for 遍历中未设置 key 的情况,内部做了处理,默认生成一个 key , 所以今后就算不设置 key 也是允许的了。
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, last
for (i = 0; i < children.length; i++) {
// .... 忽略其他代码
// default key for nested array children (likely generated by v-for)
if (isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__` // set key 设置默认 key
}
res.push(c)
}
}
}
return res
}
利于节点高效查找
同一层vnode节点是以数组的方式存储,那么如果节点非常多,通过遍历查找就稍微有点慢,因此,内部将 vnode 列表转换成对象,代码如下:
/*
生成一个key与旧VNode的key对应的哈希表
比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2
结果生成{key0: 0, key1: 1, key2: 2}
*/
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
这样一来,就可以直接通过 key 查找到数组下标,利于加快查找时间。
参考文档: muyiy.cn/question/fr…
虚拟dom 与直接操作 dom 相比哪个更快?
以下是根据尤大在知乎的回答,做的总结:
- 首先是没有任何一个框架可以比纯手动优化操作 dom 快,因为框架的 dom 操作层需要应对上层API可能发生的操作,所以它的实现是普适性的,所以不可能对每个场景做优化,这就是个性能和可维护性的取舍。各大框架可以给到即使不需要手动优化,也可以提供较优秀的性能。
- 我们看看两者的重绘性能消耗:
- innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change) render Virtual DOM + diff O 显然比渲染 html string 要慢,但我们知道,这是纯 js层面的计算相比, 与 DOM 层面的操作的开销相比要小很多。 所以直接操作dom的开销就和整个页面相关,而虚拟dom的开销就是 js层面的计算和计算后的 dom 的开销,所以虚拟dom就可以保证,不管页面数据变化多少,每次计算后的重绘的性能都在可接受范围内。
- 因为机制不一样,那么比较的时候就要看场合,比如是大量数据的更新还是小量数据的更新。举个例子,如果一个非常大的列表,数据全都发生了变化,那么直接操作dom肯定是更快的,那如果只是其中的几行发生了变化,直接全量替换dom的开销可就大了,而虚拟dom在计算后,只需要替换个别dom即可
- 虚拟dom提供给开发者的价值不是性能,而是 1.为函数式的UI编程打开大门 2.扩展性强,可以渲染到 DOM 意外的其他平台
- 那如果开发中遇到特殊的情况导致虚拟dom的更新效率不满足,那么可以牺牲一定的维护性来自己手动进行优化
参考文档: www.zhihu.com/question/31…
vue 可以定义函数式组件么
函数式组件:没有状态(data),没有生命周期,只接受传递的 props ,常用于纯 UI 组件 定义:
- 通过
Vue.component
构建组件时,添加functional: true;
需要通过调用 render 函数来渲染,常用包裹组建或者构建高阶组件 - 对于单文件组件,在 template 上添加 functional
<template functional>
v-model 的实现
常用于输入框的双向数据绑定,但我知道 vue 是单向数据流的,所以 v-model
其实是个封装的指令,本质是对 input 的 @input
事件做了封装,如下代码:
<input @input="change" :obj="obj">
change(e) {
this.obj = e.target.value;
}
也可以在自定义组件上使用,简化编码,逻辑更加清晰
vue 有几种构建版本
- 有生成版本和开发版本的区分;
- 完整版和运行时版本的区分,完整版包含编译器(用于生成渲染函数);
- 构建环境的区分,支持 UMD(AMD和commonjs)、commonjs、ES Module(用于构建工具的)、ES Module(用于浏览器的)
vuex中为什么把把异步操作封装在action,把同步操作放在mutations?
mutations 同步主要是为了能用 devtools 跟踪状态的变化,每次执行完后,就可以立即得到下一个状态,这样在devtools调试工具中,就可以跟踪到状态的每一次变化,可以做时间旅行 time-travel ,那么如果是异步的话,就没法知道状态什么时候被更新,所以就有了一个 actions 用来专门处理异步函数,但要求状态的需要触发 mutations ,这样一来对于异步的更新也可以清晰看到状态的流转。
参考文档: www.zhihu.com/question/48…
vuex getter方法跟直接 state 中获取有什么区别
getter
类似于计算属性,带有缓存;当只有响应的属性发生变化才会更新缓存,相比直接获取效率更好,在设计上可以便于抽象逻辑。
vue-router 中的 link 跳转和 a 链接跳转的区别
- 判断是否有 onclick 事件,有就执行
- 阻止默认事件
- 使用 history.replace 或 history.push 修改地址栏,同时不触发页面刷新
工具相关
webpack 中的 loader 和 plugin 是干什么的
loader:对文件进行转换,比如说将 ts 编译成 js ,css预处理等等 plugin:通过监听 webpack 运行中的广播事件,从而进行自己的操作,如常用的 HtmlWebpackPlugin:创建html文件、webpack.optimize.UglifyJsPlugin:混淆压缩
webpack 的 externals
防止将外部引用的包打包到 bundle 中,而是在运行时通过模块化的方式从外部引用。 比如我们通过cdn引用 jquery ,我们不希望jq打包到 bundle 中,而且在使用时希望能通过模块化的方式引用,那么可以如下配置
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
接口怎么防刷?
被人通过工具在短时间内恶意大量请求服务端。常用优化方式如下
- referer 校验 UA 校验
- 客户端和服务端约定签名算法,由服务端校验签名
- 服务端对请求 ip 单位时间内请求数量限制
- 通过前置交互式验证手段,先验证通过在接收请求
什么是前端工程化?
关于工程化,每个人都有自己的理解,以下是我个人的理解,每个点都可以展开很多点来说,这题更多对于工作中的总结
- 协作上:
- 统一开发规范,代码、命名规范,引用语法检查工具
- 版本管理,提交规范
- 项目架构上:
- 模块化、组件化,沉淀组件库,降低编码间的耦合
- 团队统一脚手架
- 构建:
- 资源压缩、混淆
- 图片处理
- 持续集成、部署
- 减少人为参与,Jenkins CI
- 静态资源分离,cdn、静态文件服务器
- 质量跟踪:
- 持续的单元测试、mocha Istanbul
- 监控
- 用户体验:
- 性能优化,客户端、服务端、代理服务器的
小结
因为对 react 接触不多,所以基本没问 react 的问题。工具相关的问题都是由某一个知识点引申出来的,好多都想不起来了,这一块还是关乎有没有在工作中有相关的实践。