Vue常见面试题
# Vue 常见面试题
# Vue 面试题
# new Vue() 发生了什么
- 内部执行了根实例的初始化过程
options
合并- 属性初始化
- 自定义事件处理
- 数据响应式处理
- 初始化生命周期钩子
# Vue 数据响应式原理
- defineReactive 把数据定义成响应式的
- 给属性增加一个 dep,用来收集对应的那些 watcher
- 等数据变化进行更新
# Vue 生命周期
# beforeCreate
这是第一个生命周期函数,此时,组件的 data
和 methods
以及页面 DOM
结构都还没有初始化,所以这个阶段,什么都做不了
# created
这是组件创建阶段第二个生命周期函数,此时,组件的 data
和 methods
已经可以用了,但是页面还没有渲染出来;在这个生命周期函数中,我们经常会发起 Ajax
请求
# beforeMount
当模板在内存中编译完成,会立即执行实例创建阶段的第三个生命周期;此时内存中的模板结构,还没有真正渲染到页面上,此时页面上看不到真实的数据( 用户看到的只是一个模板页面 )
# mounted
这个是组件创建阶段最后一个生命周期函数,此时,页面已经真正的渲染好了,用户已经可以看到真实的页面数据了;当这个生命周期函数执行完,组件就离开了创建阶段,进入到了运行中的阶段
- 如果用到了第三方的
UI
插件,而且这个插件还需要被初始化,那么,必须在mounted
及之后来初始化插件(echarts 图表)
# beforeUpdate
当执行 beforeUpdate
生命周期函数的时候, 数据肯定时最新的,但是页面上呈现的数据,还是旧的
# updated
页面已经完成了更新,此时,data
数据是最新的,同时,页面上呈现的数据,也是最新的
# beforeDestory
当执行 beforeDestory
的时候,组件即将被销毁,但是还没有真正开始销毁,此时组件还是正常可用的,data
、methods
等数据或方法,依然可以被正常访问
# destoryed
组件已经完成了销毁,此时组件已经废了,data
、methods
都不可用了
# SPA 优缺点
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。⼀旦⻚⾯加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
# 优点
- 前后端分离
- 减轻服务器的负担
- 良好的交互体验 - ajax
# 缺点
- 不利于 SEO
- 首屏渲染时间长
- 前进、后退难以管理
# 说说 vue 的事件管理
待更新
# vue 如何防止数据被再次 observe
待更新
# 数据变化,派发更新的时候,做了何优化
将变更放入队列,在下一个
nexttick
之后更新
# Vue.$nextTick
原理
在下次 DOM
更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
- 言外之意就是在主线程执行代码完成之后,立刻执行
- 也就是在微任务队列中(也可能是宏任务队列,视运行环境而定)
- 内部实现依次使用
Promise
->MutationObserver
->setImmediate
->setTimeout
做兼容 - Vue 源码
src/core/util/next-tick.js
文件
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// Use MutationObserver where native Promise is not available,
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true,
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 作用
- 视图更新之后,获取最新的
dom
节点 data
数据修改后,立即拿到最新数据
# CommonJS 和 esModule 的区别
# CommonJS
- module.exports 默认值为 {}
- exports 是 module.exports 的引用(exports 默认指向 module.exports 的内存空间)
- require() 返回的是 module.exports 而不是 exports
//foo.js
exports = {
foo: 'foo',
} // 给 exports 重新赋值,断开了 exports 指向 module.exports 的地址,重新指向一个新地址
//bar.js
const { foo } = require('./foo.js')
//reuqire 返回的是 module.exports 对象, 默认为 {}
2
3
4
5
6
7
8
# esModule
- 浏览器加载 ES6 模块,也使用
<script>
标签,但是要加入type="module"
属性。
<script type="module" src="./foo.js"></script>
# 差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的 require() 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段。
# CommonJS 模块加载 ES6 模块
使用 import() 方法加载
;(async () => {
await import('./foo.mjs')
})()
2
3
# ES6 模块加载 CommonJS 模块
ES6 模块的 import 命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。
// 正确
import packageMain from 'commonjs-package'
// 报错
import { method } from 'commonjs-package'
2
3
4
5
# CommonJS 模块循环引用问题
# 父组件监听子组件的生命周期
- 子组件生命周期中
emit
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit('mounted')
}
2
3
4
5
6
7
- hook
// Parent.vue
<Child @hook:mounted="doSomething"/>
2
# Vue-Router 面试题
# 原理
# 前端路由
hash 模式
- 类似于服务端路由,前端路由实现起来其实也很简单,就是匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容
- 这种
#
后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面 - 另外每次 hash 值的变化,还会触发 hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化
- 然后我们便可以监听 hashchange 来实现更新页面部分内容的操作
window.addEventListener('hashchange', () => {
// do something
})
2
3
history 模式
- HTML5 新增的API: pushState 和 replaceState,通过这两个 API 可以改变 url 地址且不会发送请求
- 但因为没有
#
号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求 - 为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
// 当活动历史记录条目更改时,将触发popstate事件
// 调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)
window.addEventListener('popstate', () => {
// do something
})
2
3
4
5
# history
和 hash
模式区别
hash
模式路径上有#
, 用window.location.hash
读取。而history
路由没有会好看一点hash
路由支持低版本的浏览器,而history
路由是HTML5
新增的API
- 回车刷新操作时,
hash
路由会加载到地址栏对应的页面,而history
路由一般 404 报错 history
模式需要后台配置支持。当我们刷新页面或者直接访问路径的时候就会返回 404,那是因为在 history 模式下,只是动态的通过 js 操作 window.history 来改变浏览器地址栏里的路径,并没有发起 http 请求,但是当我直接在浏览器里输入这个地址的时候,就一定要对服务器发起 http 请求,但是这个目标在服务器上又不存在,所以会返回 404
# vue-router 钩子执行顺序
# 导航钩子分类
- 全局守卫
- 路由独享守卫
- 组件守卫
# 全局守卫
router.beforeEach((to, from, next) => {
console.log('全局 - beforeEach')
next() // 必须要有调用 next
})
router.afterEach(() => {
console.log('全局 - afterEach')
})
2
3
4
5
6
7
8
# 路由独享守卫
{
path: '/home',
name: 'Home',
component: () => import('../views/Home.vue'),
beforeEnter (to, from , next) {
next() // 必须要有调用 next
console.log('路由 - beforeEnter ---> Home')
}
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
beforeEnter (to, from, next) {
next() // 必须要有调用 next
console.log('路由 - beforeEnter ---> About')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 组件守卫
// Home 组件
beforeRouteEnter(to, from, next) {
console.log('组件 - beforeRouteEnter ---> Home')
next() // 必须要有调用 next
},
beforeRouteLeave(to, from, next) {
console.log('组件 - beforeRouteLeave ---> Home')
next() // 必须要有调用 next
},
// About 组件
beforeRouteEnter(to, from, next) {
console.log('组件 - beforeRouteEnter ---> About')
next()
},
beforeRouteLeave(to, from, next) {
next()
console.log('组件 - beforeRouteLeave ---> About')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 顺序
从 Home
组件 跳转到 About
组件 依次输出
- 组件 - beforeRouteLeave ---> Home
- 全局 - beforeEach
- 路由 - beforeEnter ---> About
- 组件 - beforeRouteEnter ---> About
- 全局 - afterEach
2
3
4
5
假设,一打开页面就进入 Home
页面,此时的钩子顺序 如下
- 全局 - beforeEach
- 路由 - beforeEnter ---> Home
- 组件 - beforeRouteEnter ---> Home
- 全局 - afterEach
2
3
4
综合上诉两种情况,我们可以得出如下结论
- 如果是第一次进入页面,依次执行
全局的进入前置钩子
->路由的进入钩子
->组件的进入钩子
->全局的进入后置钩子
- 如果从组件内离开,将会优先执行组件的
beforeRouteLeave
钩子,此后依次执行全局的进入前置钩子
->路由的进入钩子
->组件的进入钩子
->全局的进入后置钩子
- 顺序依次为: 组件离开(可有可无) -> 全局 -> 路由 -> 组件 -> 全局
# Vuex 面试题
# vuex 判断修改state 是直接修改还是 提交commit
在严格模式下,直接修改 state 会报错,
this.$store.state.xxx = xxx
# Ajax 面试题
# fetch 和 xhr 有什么区别
# xhr
- 浏览器内置 api
- 使用起来也比较繁琐,需要设置很多值。
- 使用回调函数
# fetch
- 浏览器内置 api
- Fetch 提供了对 Request 和 Response (以及其他与网络请求有关的)对象的通用定义
- 返回一个 Promise 对象
# 差异
- fetch 使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁。
- fetch 采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。
- fetch 通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来。
# cros 的简单实现
跨域 是 浏览器存在的一种安全策略,在非浏览器环境中不存在 跨域 问题
- 本地另起一个 node 服务,将 浏览器 发起的跨域请求代理到本地
- 在 node 环境中根据请求的路径 发起真实请求,等到请求返回了数据,再 response 给浏览器
# 浏览器 面试题
# 协商缓存和强缓存
# 协商缓存
提示
让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的 Etag 和 Last-Modified 通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。
- Last-Modified/Etag
- Last-Modified: 文件最后更改时间,Etag:存储的是文件的特殊标识(一般都是 hash 生成的)
# 强缓存
提示
服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
- Cache-Control(http1.1)和Expires(http1.0)
- Cache-Control:max-age > 0(直接从游览器缓存中 提取),max-age <= 0(向server 发送http 请求确认 ,该资源是否有修改)
# Cookie 有哪些字段
- domain:指定浏览器发出 HTTP 请求时,哪些 域名 要附带这个 Cookie
- path:指定浏览器发出 HTTP 请求时,哪些 路径 要附带这个 Cookie
- HttpOnly:指定该 Cookie 无法通过 JavaScript 脚本拿到
- secure:标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端
- SameSite:允许服务器要求某个 cookie 在跨站请求时不会被发送
- Expires:指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie;不设置该属性,或者设为null,Cookie 只在当前会话(session)有效
- Max-Age:指定从现在开始 Cookie 存在的秒数,过了这个时间以后,浏览器就不再保留这个 Cookie
# XSS(跨站脚本攻击)
攻击者想尽一切办法将可以执行的代码注入到网页中
# 存储型
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
# 反射型
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
# DOM 型
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
预防:
- 对数据进行严格的输出编码:如HTML元素的编码,JS编码,CSS编码,URL编码等等
- CSP HTTP Header,即 Content-Security-Policy、X-XSS-Protection
- 输入验证:比如一些常见的数字、URL、电话号码、邮箱地址等等做校验判断
- 开启浏览器XSS防御:Http Only cookie
- 验证码
# CSRF(跨站请求伪造)
攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
- 受害者登录 a.com,并保留了登录凭证(Cookie)
- 攻击者引诱受害者访问了b.com
- b.com 向 a.com 发送了一个请求:a.com/act=xx浏览器会默认携带a.com的Cookie
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
- a.com以受害者的名义执行了act=xx
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作
预防:
- 同源检测:通过Header中的Origin Header 、Referer Header 确定
- CSRF Token 校验:将CSRF Token输出到页面中(通常保存在Session中),页面提交的请求携带这个Token,服务器验证Token是否正确
- Samesite Cookie属性
# nginx 和 tomcat 区别
nginx
- 静态服务器,静态资源,处理静态资源速度快
- 反向代理服务器 ,且支持负载均衡
- 并发性比较好,CPU内存占用低
tomcat
- 是应用(Java)服务器
- 动态解析容器,处理动态请求
总结: Nginx有动态分离机制,静态请求直接就可以通过Nginx处理,动态请求才转发请求到后台交由Tomcat进行处理