在去年底开始换工作,直到现在算是告了一个段落,断断续续的也面试了不少公司,现在回想起来,那段时间经历了被面试官手撕,被笔试题狂怼,悲伤的时候差点留下没技术的泪水。
这篇文章我打算把我找工作遇到的各种面试题(每次面试完我都会总结)和我自己复习遇到比较有意思的题目,做一份汇总,年后是跳槽高峰期,也许能帮到一些小伙伴。
先说下这些题目难度,大部分都是基础题,因为这段经历给我的感觉就是,不管你面试的是高级还是初级,基础的知识一定会问到,甚至会有一定的深度,所以基础还是非常重要的。
我将根据类型分为几篇文章来写:
面试总结:javascript 面试点汇总(万字长文)(已完成) 强烈大家看看这篇,面试中 js 是大头
面试总结:nodejs 面试点汇总(已完成)
面试总结:浏览器相关 面试点汇总(已完成)
面试总结:css 面试点汇总(已完成)
面试总结:框架 vue 和工程相关的面试点汇总(已完成)
面试总结:面试技巧篇(已完成)
六篇文章都已经更新完啦~
这篇文章是对 框架 vue 和工程相关
相关的题目做总结,欢迎朋友们先收藏在看。
先看看目录
这部分是 vue 相关的整理。
响应式原理是 vue 的核心思想之一,后续打算单独整理一篇,这里简单介绍响应式的三大件
观察者,使用 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中向Dep(调度中心)添加观察者,在getter中通知观察者更新。
扮演的角色是订阅者,他的主要作用是为观察属性提供通知函数,当被观察的值发生变化时,会接收到来自订阅中心 dep 的通知,从而触发依赖更新。
核心方法有:
get() 获得getter的值并且重新进行依赖收集
addDep(dep: Dep) 添加一个依赖关系到订阅中心 Dep 集合中
update() 提供给订阅中心的通知接口,如果不是同步的(sync),那么会放到队列中,异步执行,在下一个事件循环中执行(采用 Promise、MutationObserver以及setTimeout来异步执行)
扮演的角色是调度中心,主要的作用就是收集观察者 Watcher 和通知观察者目标更新。
每一个属性都有一个 Dep 对象,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的 watch,让订阅者执行自己的 update 逻辑。
从编码上 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 ,是用于监听某一个值的变化,进行一系列复杂的操作。监听属性可以支持异步,计算属性只能是同步。
在官方文档上关于数组的注意事项有这么一段
由于 JavaScript 的限制,Vue 不能检测以下数组的变动:
先看第二点,这是因为 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
})
})
有两点用处:快速节点比对和节点唯一标识
用作于 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 查找到数组下标,利于加快查找时间。
参考文档:
https://muyiy.cn/question/frame/1.html
以下是根据尤大在知乎的回答,做的总结:
参考文档:
https://www.zhihu.com/question/31809713/answer/53544875
函数式组件:没有状态(data),没有生命周期,只接受传递的 props ,常用于纯 UI 组件
定义:
Vue.component
构建组件时,添加 functional: true;
需要通过调用 render 函数来渲染,常用包裹组建或者构建高阶组件
常用于输入框的双向数据绑定,但我知道 vue 是单向数据流的,所以 v-model
其实是个封装的指令,本质是对 input 的 @input
事件做了封装,如下代码:
<input @input="change" :obj="obj">
change(e) {
this.obj = e.target.value;
}
也可以在自定义组件上使用,简化编码,逻辑更加清晰
mutations 同步主要是为了能用 devtools 跟踪状态的变化,每次执行完后,就可以立即得到下一个状态,这样在devtools调试工具中,就可以跟踪到状态的每一次变化,可以做时间旅行 time-travel ,那么如果是异步的话,就没法知道状态什么时候被更新,所以就有了一个 actions 用来专门处理异步函数,但要求状态的需要触发 mutations ,这样一来对于异步的更新也可以清晰看到状态的流转。
参考文档:
https://www.zhihu.com/question/48759748/answer/112823337
getter
类似于计算属性,带有缓存;当只有响应的属性发生变化才会更新缓存,相比直接获取效率更好,在设计上可以便于抽象逻辑。
loader:对文件进行转换,比如说将 ts 编译成 js ,css预处理等等
plugin:通过监听 webpack 运行中的广播事件,从而进行自己的操作,如常用的 HtmlWebpackPlugin:创建html文件、webpack.optimize.UglifyJsPlugin:混淆压缩
防止将外部引用的包打包到 bundle 中,而是在运行时通过模块化的方式从外部引用。
比如我们通过cdn引用 jquery ,我们不希望jq打包到 bundle 中,而且在使用时希望能通过模块化的方式引用,那么可以如下配置
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
被人通过工具在短时间内恶意大量请求服务端。常用优化方式如下
关于工程化,每个人都有自己的理解,以下是我个人的理解,每个点都可以展开很多点来说,这题更多对于工作中的总结
因为对 react 接触不多,所以基本没问 react 的问题。工具相关的问题都是由某一个知识点引申出来的,好多都想不起来了,这一块还是关乎有没有在工作中有相关的实践。
如果你喜欢探讨技术或者有疑问,欢迎添加我微信一起学习交流,大家都是同行,非常期待能与大伙交朋友,聊技术、聊爱好。
下面是我的微信二维码,可扫码添加