最近面试一个月,记录一下遇到的面试题
后续还会做一些补充更改
语义化
页面整体结构
代码结构清晰,易于阅读
利于开发和维护
有利于搜索引擎优化(SEO)
无障碍开发(盲人)
<html lang="en">
语义化的 HTML
<address>
<article>
<aside>
<footer>
<header>
label for属性
通过键盘完成所有的功能
SEO
Title:掘金 - 代码不止,掘金不停
Keywords:掘金,稀土,Vue.js,前端面试题
description: 描述内容
url输入之后
1、查找缓存
2、DNS解析
3、建立TCP连接
4、HTTP请求
5、服务器响应请求并返回结果
6、关闭TCP连接
7、浏览器渲染
8、构建DOM树
9、构建CSS规则树
10、合并生成render树
11、布局-绘制
强制缓存失效-携带缓存标识向服务器发起请求-返回304,协商缓存生效-协商缓存失效,返回200和请求结果
回流和重绘
重绘:元素外观改变,改变布局
重排/回流:重新计算元素,重新生成布局
避免回流或重构
定位\集中改变样式,不要一条一条地修改 DOM 的样式
css
Flex 布局
容器的属性:
flex-direction:决定主轴的方向 flex-direction: row|row-reverse|column|column-reverse;
flex-wrap:决定换行规则 flex-wrap: nowrap | wrap | wrap-reverse;
flex-flow: .box { flex-flow: || ; }
justify-content:对其方式,水平主轴对齐方式
align-items:对齐方式,竖直轴线方向
align-content
项目的属性(元素的属性):
order 属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为 0
flex-grow 属性:定义项目的放大比例,即使存在空间,也不会放大
flex-shrink 属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小
flex-basis 属性:定义了在分配多余的空间,项目占据的空间。
flex:是 flex-grow 和 flex-shrink、flex-basis 的简写,默认值为 0 1 auto。
align-self:允许单个项目与其他项目不一样的对齐方式,可以覆盖
align-items,默认属 性为 auto,表示继承父元素的 align-items
元素水平垂直居中
.parent {
position: relative;
}
.child {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
.parent {
display: flex;
/* 定义项目在主轴上如何对齐 */
justify-content: center;
/* 定义项目在交叉轴上如何对齐 */
align-items: center;
}
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
文本溢出
.ellipsis{
overflow: hidden; text-overflow:ellipsis;
white-space: nowrap; display: inline-block;
}
.ellipsis{
overflow: hidden; display: -webkit-box;
text-overflow:ellipsis; -webkit-line-clamp:2;
-webkit-box-orient: vertical
}
设置相对定位的容器高度,用包含省略号(…)的 元素/伪元素 模拟实现
一些开源的js库
js
es6新增语法
let 和 const、解构赋值、展开运算符、模板字⾯量、箭头函数、Promises、Generators、Iterator、
for ... in、for ... of、Map 和 Set、Proxy、Class、es module
JS中的数据类型
基本类型(值类型):Number、String、Boolean、Symbol,null,undefined 栈内存储
引用类型(复杂数据类型):Object、Function、Array 堆内存储
JS中的数据类型检测方案
typeof
instanceof
Object.prototype.toString.call()
作用域和作用域链
简单来说作用域就是变量与函数的可访问范围
1.全局作用域:代码在程序的任何地方都能被访问,window
2.函数作用域:在固定的代码片段才能被访问,function内
3.新增块级作用域,大括号内{}
一般情况下查找变量会在当前作用域先找。但是如果在当前作用域中没有查到,就会向上级作用域去查
直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
闭包
闭包是指能访问另一个函数作用域中的变量的函数, 正常就是函数嵌套函数延长外部函数的作用域
优点:模仿块级作用域、封装私有化变量、创建模块(IIFE)
缺点:一直保存在内存中,过多的闭包可能会导致内存泄漏
new运算符的实现机制
1.创建了一个对象
2.this指向这个对象
3.执行构造函数的代码
4.返回这个对象
注意的是要 判断函数定义的返回值类型(函数默认返回undefined),如果是基本数据类型,返回创建的对象。
如果是引用类型,就返回这个引用类型的对象。
原型 && 原型链
构造函数 通过new得到实例对象,内部prototype指向原型对象
实例对象 __proto__指向原型对象
原型对象 constructor指向构造函数
在对象上查找一个属性或者方法时,如果找不到就会去原型对象找,再找不到再去原型对象的原型对象,
最后找到object的原型对象为null时就表示没有
JavaScript对象是通过引用来传递的。当我们修改原型时,与之相关的对象也会改变
EventLoop
js: 先会执行栈中的内容 - 栈中的内容执行后执行微任务 - 微任务清空后再取出一个宏任务压入执行栈执行 -
再去执行微任务 - 然后在取宏任务清微任务这样不停的循环。
宏任务:ajax、定时器、一些浏览器api、script...
微任务:promise.then、mutationObersve
防抖节流
防抖:高频触发事件n 秒内函数只会执行一次, 思路:每次触发前都取消之前的延时调用方法
节流:高频事件触发, n 秒内只会执行一次, 思路:每次触发事件时都判断当前是否有等待执行的延时函数,
有等待执行的延时函数就直接return
vue
组件中的data为什么是一个函数
在Vue中组件是可以复用的,如果data是对象,当组件被多次引入后,由于对象属于引用类型,
一个组件内部的data中的属性发生改变就会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,
data必须是一个函数
key 的作用
key的作用是为了在diff运算时更快的找到相同的节点让其复用,避免大量不必要的dom操作,影响性能。
// 这里是Vue中对比两个节点是否是相同节点的代码
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)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
举个🌰,在一个列表根据不同部分条件排序时,页面结构如下,当我下次的排序条件使当前列表发生翻转,而反转后内容不会发生变化,在上述情况下我们分析当写key和不写key两种情况的dom对比。首先如果不写key,Vue内部会默认根据索引来生成一个key。
<template>
<div v-for="item in arr" class="box">
<p>标题:{{item.title}}</p>
<span>创建时间:{{item.createTime}}</span>
<span>创建人:{{item.createUser}}</span>
<div>内容:{{item.content}}</div>
</div>
</template>
<script>
export default {
data() {
return {
arr: [
{
id: 111,
title:'111111-title',
createTime:'111111-createTime',
createUser:'111111-createUser',
content:'111111-content',
},
{
id: 222,
title:'222222-title',
createTime:'222222-createTime',
createUser:'222222-createUser',
content:'222222-content',
},
{
id: 333,
title:'333333-title',
createTime:'333333-createTime',
createUser:'333333-createUser',
content:'333333-content',
},
]
};
},
}
</script>
不写key:当不写key时,Vue内部会默认根据索引来生成一个key。内部的结果就是
//旧dom数据 //新dom数据
{ {
key:1, key:1,
id: 111, id: 333,
... ...
}, },
{ {
key:2, key:2,
id: 222, id: 222,
... ...
}, },
{ {
key:3, key:3,
id: 333, id: 111,
... ...
}, },
在上述情况下,当数据发生翻转后,Vnode的key值还是不发生变化的,因为都是根据索引生成的,
所以在做虚拟dom对比的时候会判断id为111的旧dom与id为333的dom为同一个节点,
而进入内部字节点的时候发现是不同的dom,就会做dom的删除、创建、新增
写key时
//旧dom数据 //新dom数据
{ {
key:111, key:333,
id: 111, id: 333,
... ...
}, },
{ {
key:222, key:222,
id: 222, id: 222,
... ...
}, },
{ {
key:333, key:111,
id: 333, id: 111,
... ...
}, },
在写key情况下,当数据发生翻转后,Vnode的key值是不变的,所以在做虚拟dom对比的时候会判断
id为111的旧dom与id为333的dom 不是 同一个节点,而同一个节点只是位置发生了改变,只需要做dom的移动,
而不是删除、创建、添加,这样就提高了渲染的性能
vue组件的通信方式
1、props
2、$on、$emit
3、$parent、$children、$refs
4、Event Bus ==> Vue.prototype.$bus = new Vue()
5、$attrs、$listeners
6、Provide、inject
7、vuex
双向数据绑定
Vue 使用 Object.defineProperty 遍历和递归对data中的所有属性做数据劫持,把这些 属性 全部转为
getter/setter,当获取的时候会触发getter,设置的时候会触发setter
发布者-订阅者模式
监听(Observer): 遍历和递归对data中的所有属性做数据劫持,当获取的时候会触发get,设置的时候会触发set
模版编译(Compile): 解析模板指令,将模板中变量替换成数据,然后渲染初始化的页面视图。并且给所有的 变量
添加监听数据的 订阅者,一旦数据有变动,收到通知,更新试图
依赖收集(Dep): 收集者,每个变量会有一个收集者,收集变量所对应的所有的订阅者,
当数据发生变化的时候通知订阅者更新
订阅者(Watcher):数据监听 和 模版编译之间通信的桥梁,当收到通知更新时更新页面
vue1.0的订阅发布模式
1.遍历和递归对data中的所有属性做数据劫持,添加get和set,并且同时会给每个属性创建一个对应的dep
2.模版编译,将模版中所有的变量替换成数据,同时会给每个变量创建一个订阅者(Watcher),⚠️并且会将当前的
Watcher添加到当前变量所对应的dep中。添加到dep的操作就是 订阅。
具体实现方式是触发一次当前属性的get,把当前的Watcher传递过去。(这里dep有可能会对应多个Watcher)
3.往后当数据发生改变的时候就会触发每个dep的notice(),然后就是遍历dep中保存的Watcher,去执行更新,
这里就是发布
Vue2.0的订阅发布模式
在vue1.0中会给每个动态的属性添加一个Watcher,当项目体量较大时这无疑是一笔很大的开销,因为会占据很多
内存,同时,在频繁的操作dom也是对性能有了很大的消耗。所以在vue2.0引入了虚拟dom
1.遍历和递归对data中的所有属性做数据劫持,添加get和set,并且同时会给每个属性创建一个对应的dep
2.vue2.0不会给每个属性添加一个Watcher,而是给一个组件添加一个Watcher
3.当数据改变,触发dep的notice(),遍历dep中保存的Watcher,去执行更新时,不会做大量的dom操作,
而是执行虚拟dom对比,完成更新。
Vue.set
由于 Vue 会在初始化实例时对 属性 执行 getter/setter 转化,所以 属性 必须在 data
对象上存在才能让 Vue 将它转换为响应式的
弊端就是监听不到新增的属性,以及无法检测数组通过索引下标和length的修改,Vue.set就是让这些新的属性也变成响应式的数据
查看源码会发现Vue.set函数内部执行了defineReactive方法,而defineReactive方法就是Vue使用 Object.defineProperty对属性执行 getter/setter 转化的方法。
/*Vue 源码中set方法的实现(精简版) */
export function set (target, key, val) {
const ob = (target: any).__ob__
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Vue.nextTick
在下次 DOM 更新循环结束之后执行延迟回调。
因为Vue 在更新 DOM 时是异步执行的,当我们设置一个属性变更时,组件不会立即渲染,所以我们在同步代码中获取不到变更后的渲染结果。
以下是属性变更,通知Watcher的DOM更新函数,可以看出,内部也是用了nextTick方法
/*Watcher内部的更新函数 update() */
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 这里是监听数据变化通知视图更新的方法,会用异步的方式去做页面更新
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
nextTick(flushSchedulerQueue)
}
而nextTick函数内部是在用微任务去执行所有的更新函数,而我们的Vue.nextTick传入的回掉函数也会被加入到以下的callbacks数组中,也就变成了异步执行函数
// 保存当前的所有回掉函数
const callbacks = []
export function nextTick (cb, ctx) {
callbacks.push(() => {
// 将当前的回掉函数保存
cb.call(ctx)
})
// 执行函数
timerFunc()
}
let timerFunc = () => {
// 用微任务去执行保存的所有回掉函数
Promise.resolve().then(flushCallbacks)
}
function flushCallbacks () {
// 遍历执行所有的回掉函数
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
注意⚠️:Vue内部也做了兼容降级处理,依次是
Promise
- MutationObserver
- setImmediate
- setTimeout0秒
computed与watch
计算属性依赖于多个属性,有缓存。只有当依赖的属性发生变化时才会更新。监听器用于观察单个属性的变化,会立即执行。
计算属性:当模版内的表达式过于复杂时,会让模版过重且难以维护 例如: {{ message.split('').reverse().join('') }}
所以,对于复杂逻辑,应该使用计算属性
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
我们提供的函数将作为声明的计算属性的getter 函数,由于Vue知道计算属性是依赖于那些data里的属性的,因此Vue内部做了缓存,只有计算属性依赖的属性发生改变时,计算属性的getter 函数才会更新
注意⚠️:计算属性使用箭头函数时,箭头函数绑定了父级作用域的上下文, this
不会指向Vue 实例,但是会默认传入到第一个参数vm => vm.a * 2
,或者用function声明函数
监听器:观察一个属性/表达式/回掉结果,
注意⚠️:不应该使用箭头函数来定义 watcher 函数 (例如 search: newVal => this.update(newVal)
)。理由是箭头函数绑定了父级作用域的上下文,所以 this
将不会指向 Vue 实例
/*Watcher内部的更新函数 update() */
update () {
if (this.lazy) {
//这里是页面中的computed选项,只有所依赖的属性发生变化时才会惰性的做更新
this.dirty = true
} else if (this.sync) {
//这里是页面中的watch选项,表示当监听到数据变化的时候会同步执行传入的回掉函数
this.run()
} else {
// 这里是监听数据变化通知视图更新的方法,会用异步的方式去做页面更新
queueWatcher(this)
}
}
computed与watch以及dom更新在Vue中都是new 了一个Watcher,只是参数有所不同而已,在Watcher中的update更新函数中做了区分
虚拟dom
虚拟dom本质上就是一个普通的JS对象,来表示dom节点的内容
在vue中,渲染视图的时候会调用render函数,这个渲染 在组件创建时,和视图依赖的数据更新时。如果在渲染的时候,直接使用真实DOM操作的创建、更新、插入等操作会带来大量的性能损耗,这样就会极大的降低渲染效率。
因此,vue在渲染时,使用虚拟dom来替代真实dom,主要为解决渲染效率的问题。
vue中的虚拟dom是基于Snabbdom的虚拟DOM修补算法
vue3和vue2的区别
生命周期、Composition API、Teleport、响应式原理、虚拟DOM静态节点、更好的Tree-shaking
webpack
vue有哪些性能优化
对于第三方js库CDN
图片资源的压缩,icon资源使用雪碧图
开启gizp压缩
生产环境关闭SourceMap
mini-css-extract-plugin-提取CSS到单独的文件,压缩CSS
vue-router使用懒加载
合理使用watch和computed
v-for必须添加key
销毁定时器
keep-alive
Object.freeze
压缩代码
CDN加速
Tree Shaking
提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,可以⻓期缓存这些⽆需频繁变动的公共代码
webpack优化
include、exclude配置项来缩⼩loader的处理范围
多配置别名
happypack=>thread-loader(多进程loader)
cache-loader\babel-loader开启缓存
hard-source-webpack-plugin缓存中间步骤
webpack5
treeShaking机制的原理
利用es6模块的规范
ES6 Module引入进行静态分析,编译的时候判断到底加载了那些模块
判断那些模块和变量未被使用或者引用,然后删除对应代码
Loader和Plugin
webpack自身只支持js和json这两种格式的文件,对于其他文件需要通过loader将其转换。
loader,它是一个转换器,将A文件进行编译成B文件,单纯的文件转换
plugin是工具不操作文件,在webpack打包过程中,执行一些任务(打包优化、文件管理、环境注入)
常见的plugin
ProvidePlugin:自动加载模块,代替require和import
html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件
clean-wenpack-plugin 清理每次打包下没有使用的文件
DefinePlugin 编译时配置全局变量
HotModuleReplacementPlugin 热更新
optimize-css-assets-webpack-plugin 不同组件中重复的css可以快速去重
compression-webpack-plugin 生产环境可采用gzip压缩JS和CSS
webpack-bundle-analyzer:可视化Webpack输出文件的体积
常⻅的Loader
file-loader:把⽂件输出到⼀个⽂件夹中
url-loader:和 file-loader 类似,base64 的⽅式
source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码
thread-loader(多进程loader)
webpack 构建流程
1、初始化参数,合并传入的和webpack.config.js文件中的配置参数
2、注册/监听 插件,监听构建的生命周期
3、拿到配置从入口处开始构建这个AST语法树,它会根据依赖一直递归下去
4、webpack默认只能编译js和json,遇到其他文件类型会根据配置的loader进行转换
5、得到文件的结果和依赖关系后生成代码块chunk
6、输出到dist目录
webpack 热跟新
1、首先启动dev-server,webpack开始构建时会向 入口 文件注入热更新代码
2、浏览器打开的时候,浏览器与本地服务会基于Socket建立通讯
3、本地服务会监听文件的改动,然后会再次编译
4、编译完成后通过socket 发送消息告诉浏览器,(通过维护hash值和state状态);
5、浏览器再去请求新的模块替换掉之前旧的模块
webpack 的hash
1、hash: 每次webpack编译中生成的hash值
2、chunkhash: chunkhash基于入口文件及其关联的chunk形成,文件的改动只会影响与它有关联的chunk的
hash值,不会影响其他文件
3、contenthash: contenthash根据文件内容创建。当文件内容发生变化时,contenthash发生变化
网络
请求跨域问题
跨域是由浏览器的同源策略造成,(协议、域名、端口)有一个不同就视为跨域
JSONP
CORS 服务器设置Access-Control-Allow-Origin 响应头,允许跨域请求
proxy 代理 目前常用方式,通过服务器设置代理
http2
http1: 每次请求都会建立一次HTTP连接-3次握手4次挥手(每个TCP连接只能发送一个请求)
http1.1: TCP 连接默认不关闭,可以被多个请求复用,但是同一个TCP连接里面,所有的数据通信是按次序进行。
服务器只有处理完一个回应,才会进行下一个回应,前面慢的时候,后面的就需要长时间等待
http2:http2的传输是基于二进制帧的。每一个TCP连接中承载了多个双向流通的流。同一域名只需占用一个
TCP 连接,就是可以并行的发出多个请求,不会堵塞
http状态码3开头
301 永久性重定向, 可促进搜索引擎优化效果。
302 临时性重定向, 影响搜索引擎优化效果。
304 协商缓存生效。
浏览器的缓存机制
浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识,查找到就是强缓存生效
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,当协商缓存生效,返回304,
当协商缓存失效,返回200和请求结果结果