2024前端面试真题【Vue篇】

Vue 的虚拟DOM

虚拟DOM

虚拟DOM是真实DOM的JavaScript表示。它是一个轻量级的JavaScript对象,可以表示DOM的结构和属性,虚拟DOM与真实DOM一一对应。

在Vue中,每个Vue组件都会维护一个对应的虚拟DOM树。当组件的数据发生变化时,Vue的响应式系统会触发虚拟DOM的重新渲染过程。Vue的虚拟DOM实现(Vue 2.x使用的是snabbdom的一个分支,而Vue 3.x则完全重写了虚拟DOM的实现)会生成一个新的虚拟DOM树,并与旧的虚拟DOM树进行对比(这个过程称为diff算法)。diff算法会找出最小量的需要更新的DOM节点,然后Vue会将这些变化应用到真实的DOM上,从而更新用户界面。

虚拟DOM带来最直接的好处:性能优化。因为:直接操作DOM是昂贵的,因为DOM操作会触发浏览器的重排(reflow)和重绘(repaint)。Vue可以对抽象虚拟DOM树进行增、删、改等节点操作,经过diff算法得到一些需要修改的最小单位,再更新视图,减少了DOM操作,提高性能。

虚拟DOM最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力。以及由diff算法实现减少JS操作真实DOM带来的性能消耗。

Vue通过createElement生成VNode。每个VNode有children,children每个元素也是一个VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树。

diff 算法

diff算法是一种通过同层的树节点进行比较的高效算法。特点:

  • 比较只会在同层级比较;
  • diff比较过程中,循环从两边向中间比较

比较方式
diff算法整体策略:深度优先同层比较

原理分析

  • 当数据变化时,set()方法会调用Dep.notify()通知所有WatcherWatcher会调用patch(oldNode, Vnode)给真实DOM打补丁,更新相应的视图。
  • 通过isSameVNode进行判断,相同则会调用patchVNode方法。

Vue的组件和插件

组件(Components)是构成APP的业务模块;插件(Plugins)是对Vue功能的增强与补充。

组件

Vue中,每一个.vue文件都可以视为一个组件。优势:

  • 降低系统的耦合度
  • 调试方便,组件之间职责单一,能快速定位问题;
  • 提高可维护性
  1. 组件注册:全局注册(Vue.component())、局部注册(components

插件

插件通常是用来给全局添加功能的,一般分为:

  • 添加全局方法或属性;
  • 添加全局之源;
  • 通过全局混入添加一些组件选项;
  • 添加Vue实例方法(Vue.prototype
  • API功能库;
  1. 插件实现
    Vue插件的实现是暴露一个install方法。这个方法由构造器、可选的选项对象两个参数构成,即function(Vue, options)

  2. 插件注册Vue.use(插件名, {...options})

注意:插件注册,需要在调用new Vue()启动之前完成。Vue.use()自动阻止多次注册相同插件

Vue 生命周期

Vue的生命周期:从创建销毁的过程。
Vue的生命周期共分为8个阶段 + 2个特殊特殊阶段:

  • 创建前后:beforeCreated、created
    • 初始化Vue实例,可访问、修改属性,vm.$el未创建
    • created常用于异步数据获取
  • 载入前后:beforeMount、mounted
    • DOM初始化,$el挂载完成
    • mounted阶段创用于获取访问数据和dom
  • 更新前后:beforeUpdate、updated
  • 销毁前后:beforeDestroy、destroy
  • keep-alive缓存组件激活、停用:activated、deactivated

常见问题
数据请求放在createdmounted中有什么区别?

数据请求放在mounted阶段有可能会导致页面闪动,主要原因是由于此时页面DOM结构已经生成。所以建议在页面加载前完成请求,也就是在created阶段完成。

双向数据绑定

双向数据绑定,主要是ModelView两者的数据更新问题。Vue的双向数据绑定主要由三个部分组成:

  • Model(数据层):应用数据和业务逻辑;
  • View(视图层):UI组件等展示;
  • ViewModel(业务逻辑层):框架封装的核心,负责将数据与视图关联的控制层。

以上分层的架构方案即MVVMVM即双向数据绑定的核心功能。主要职责:

  • 数据变化后更新视图
  • 视图变化后更新数据

ViewModel

ViewModel主要由两部分组成:

  1. 监听器(Observer):对数据的属性进行监听
  2. 解析器(Compiler):对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。

实现双向绑定

Vue的双向绑定流程:

  1. new Vue()执行初始化,对data执行响应式处理,Observer执行;
  2. 同时对模板执行编译,找到动态绑定的数据,从data中获取并初始化视图,即compiler执行;
  3. 同时定义一个更新函数和Watcher,将来对应数据变化时,Watcher会调用此更新函数处理;
  4. 由于data中的某个key在一个视图中可能出现多次,所以每个key会有一个Dep管家来管理多个Watcher
  5. 数据变化,找到对应的Dep,通知所有的Watcher执行更新函数。

Vue实例挂载过程

template的解析步骤

  1. html文档片段解析成AST描述符
  2. AST描述符解析成字符串
  3. 生成render函数,挂载到vm上(render的作用是生成vNode
  4. _update方法主要调用patch,将vNode转换为真是DOM,并且更新到页面中。

Vue的挂载过程:

  • new Vue()会调用_init方法,定义$set$get等方法、事件以及生命周期钩子;
  • 调用$mount进行页面挂载,主要是通过mountComponent()方法
  • 定义updateComponent()更新函数
  • 执行render函数生成虚拟DOM
  • _update函数将虚拟DOM生成真是DOM,渲染到页面

组件通信

组件通信的本质就是信息同步,即共享到Vue中。每个组件之间都有独立的作用域,数据无法共享。但实际使用中,需要共享一些信息数据,所以组件通信的目的就是让各组件之间能进行通讯。

组件通信的8中方案:

  • props
  • $emit
  • $ref
  • EventBus:兄弟组件传值,$emit$on搭配使用
  • $parent或者$root.on().emit()
  • attrslisteners:祖先传递数据给子孙,包含了父级作用域中不作为props被识别的特性绑定。可以使用v-bind=$attrs传入内部组件
  • provideinject:组先传递数据给后代组件
  • Vuex

Vue属性

Vue的data属性

在我们定义好一个组件以后,Vue最终会通过Vue.extend()构成组件实例。

  • 根实例对象data可以是对象也可以是函数,由于根实例是单例,所以不会出现数据污染的情况。
  • 组件实例对象data必须是函数,防止多个组件实例对象之间公用一个data产生数据污染

动态给Vue2添加一个新的属性问题

Vue2是通过Object.defineProperty()实现数据响应式的。动态给Vue添加一个新的属性,无法实现响应式,因此无法触发事件属性的拦截,因此页面不会更新。

解决方案
Vue不允许在已经创建的实例上动态添加新的响应式属性,若想实现数据与视图的同步更新,可以采取以下3种方案:

  • Vue.set(target, key, vaue):内部实现即再次调用Vue.defineReactive()方法实现响应式;
  • Object.assign({}, oldObj, newObj):创建一个新的对象,合并原对象和混入对象的属性
  • $forceUpdated():强制重新渲染

v-ifv-for 的优先级

v-forv-if都是Vue模板系统中的指令。在Vue编译的时候,会先将指令系统转换为可执行的render函数。

编译渲染过程中,都是线渲染,再判断,即:v-forv-if优先级要高。

常见问题

  1. v-forv-if 不建议同时使用在同一个元素上;

由于实现都是每次渲染都是先循环再进行条件判断,会带来不必要的性能浪费。必要时候可使用<template>标签做v-if判断操作。

  1. 通常使用computed提前过滤不需要显示的项数据;

v-showv-if

v-show 原理

不论初始条件是什么,元素总是会被渲染。有transaction就执行,没有则设置display属性。仅表示css属性的切换;

v-if 原理

返回一个Node节点,render函数通过表达式的值决定是否生成DOM。

结论

v-showv-if 都能控制DOM的显示,但是 v-if 有更高的切换消耗,v-show有更好的初始渲染消耗。两者利弊取舍。

Vue 中的 key

key是每一个虚拟DOM的唯一id,也是diff算法的一种优化策略。根据key,可以更准确、快速的定位到对应的节点。

Vue中的 mixin

Mixin(混入),提供了一种非常灵活的方式来分发Vue组件中可复用的功能。

分类

  • 全局混入:Vue.mixin(mixin)
  • 局部混入:mixins: []属性赋值

注意事项
当组件存在于mixins对象相同的选项时,进行递归合并的时候,组件的选项会覆盖mixins的选项。如果相同选项为生命周期钩子的时候,会合并为一个数组,先执行mixins,再执行组件的钩子。(mergeOptions方法的使用)

Vue常见修饰符

分类

  • 表单修饰符(v-model):.lazy.trim.number
  • 事件修饰符(@xx):.stop(event.stopPropagation)、.prevent(event.preventDefault)、.self.once.native.capture.passive
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符:.sync.prop(设置自定义便签属性,避免暴露数据,防止污染HTML结构)

Vue中的 $nextTick

Vue 中的 $nextTick 是用于在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用该方法,获取更新后的DOM。

主要原因:Vue在更新DOM时是异步执行的,视图需要等待更新队列中所有数据变化完成之后,再统一更新、去重。因此,如果在数据变化之后,直接获取DOM,有可能会拿到旧的DOM。

$nextTick本质就是一种优化策略,使同一时间的多次更新、多个数据更新影响到的视图,做一次更新即可。

Vue 中的 keep-alive

keep-aliveVue的一个内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keep-alive的属性:

  • include
  • exclude
  • max

其中,keep-alive是在组件的mounted钩子函数中观测includeexclude的变化并处理的,this.cache对象对缓存的组件做存取以及移除等。

keep-alive的基本用法

<keep-alive>
	<component :is="view" />
</keep-alive>

设置了keep-alive缓存的组件,会有activateddeactivatedl两个生命周期钩子。

  • 首次进入组件:beforeRouterEnter > … > mounted > activated > … > beforeRouterLeave > deactivated
  • 再次进入组件时:beforeRouterEnter > activated > … > beforeRouterLeave > deactivated

缓存的组件如何获取新数据
解决方案:

  1. beforRouterEnter(to, from, next)
  2. activated(服务器端渲染期间,activated不可用)

Vue项目中的跨域问题

跨域的本质实际上就是浏览器的基于同源策略的一种安全手段。
所谓同源(即在同一个域)具有三个相同点:

  1. 协议相同(protocol)
  2. 主机相同(host)
  3. 端口相同(port)

反之非同源请求,即:协议、端口、主机三者其中一项不同,则会产生跨域

解决跨域的方法由很多,比如:

  • JSONP
  • CORS:由一系列传输的HTTP头组成,让服务器声明允许的访问来源(Access-Control-Allow-Origin响应头设置)。
  • Proxy:网络代理,允许客户端通过一个服务于另一个网络终端进行非直接的链接。一般认为:代理服务有利于保障网络终端的隐私和安全,防止攻击

Vue中,通过vue-cli脚手架搭建的项目,可在vue.config.js文件下配置proxy参数:

devServer: {
	...
	proxy: {
		'/api': { // 代理标识
			target: '目标服务器地址',
			changeOrigin: true, // 是否跨域
			pathRewrite: {
				'^/api': ""
			}
		}
	}
}

再通过axios配置请求的根路径:

axios.defaults.baseURL = '/api'

其它跨域解决办法:

  • nginx实现代理等

Vue的自定义指令

注册指令的方式

  • 全局注册:Vue.directive()
  • 局部注册:directive属性配置
  1. 自定义指令的钩子函数
    • bind(el, binding):指定第一次绑定到元素时调用,用于初始化配置等;
    • inserted(el, binding):被绑定元素插入到父节点时调用;
    • update(el, binding):所在组件Vnode更新时调用,可能发生在其子Vnode更新之前
    • componentUpdated(el, binding):指令所在Vnode及其子Vnode全部更新后调用;
    • unbind(el, binding):只调用一次,指令与元素解绑时

每个钩子函数都有4个参数:

  • el:绑定的元素;
  • binding:对象,包含:name、value、arg
  • vnode
  • oldVnode

示例:
防止表单重复提交

Vue.directive('throttle', {
	bind: (el, binding) => {
		let throttleTime = binding.value
		if(!throttleTime ) throttleTime  = 2000 // 节流时间默认2s
	}
	let cbFnc
	el.addEventListener('click', evt => {
		if(!cbFnc) { // 第一次执行
			cbFnc = setTimeout(_=>{
				cbFnc = null
			}, throttleTime )
		} else {
			evt && evt.stopImmediatePropagation()
		}
	}, true)
})

Vue 的过滤器(Vue3已废弃)

分类

  • 全局过滤器:Vue.filter()
  • 局部过滤器:filters

注意事项

  1. 过滤器可以接收参数,其中,表达式的值永远作为第一个参数;
  2. 一个表达式可以使用多个过滤器,用管道符**|**隔开。

原理分析
3. 在编一阶段通过parseFilters将过滤器编译为函数调用;
4. 编译后通过resolveFilter函数找到过滤器并返回结果
5. 执行结果作为参数传递给toStringtoString执行后,其结果保存在Vnode的Text属性中,渲染到视图。

插槽 slot

slot其实就是Vue组件内的一个占位符,作为承载分发内容的出口,允许用户在使用的时候自定义内容。

slot使用场景

通过插槽,可以让用户自定义拓展所需的组件,去更好的复用以及定制化处理。

slot分类

插槽可以分为3类:

  1. 默认插槽
  2. 具名插槽:子name属性+父#name
  3. 作用域插槽:子组件在作用域上绑定属性,将子组件的信息传递给组件使用。
    • 用法:#default=slotProps或者结构获取#testProps = {user, id}
    • v-slot属性只能在<template>标签上使用,但在只有默认插槽时可以在组件标签上使用。

slot原理分析

slot本质上是返回Vnode的函数。Vue中的组件要渲染到页面上,流程:template - render function - Vnode - DOM。
渲染插槽函数renderSlot_render渲染函数通过_normalizeScopedSlots得到vm.$scopedSlots

Vue的 axios

axios是一个轻量的HTTP客户端:基于XMLHTTPRequest服务来执行HTTP请求,支持Promise

手动实现 Axios

class Axios {
	constructor() {},
	request(consifg) {
		return new Promise(resolve, reject) => {
			const {url = '', method = 'get', data = {}, } = config
			const xhr = new XMLHttpRequest()
			xhr.open(method, url, true)
			xhr.onload = function() {
				resolve(xhr.responseText)
			}
			xhr.send(data)
		}
	}
}
function CreateAxios() {
	const req = new Axios()
	return req.request.bind(axios)
}
const axios = CreateAxios()
export default axios

Axios请求拦截器

axios.interceptors.request.use(
	config => {
		token && (config.headers.Authorization = token);
		return config
	},
	error => {
		return Promise.error(error)
	}
)

Axios响应拦截器

axios.interceptors.response.use(
	res=> {
		const {status, data} = res
		if(status === 200) {
			if(data.code === 511) {
				// 未授权
			} else if(data.code === 510) {
				// 未登录
			} else {
				return Promise.resolve(res)
			}
		} else {
			return Promise.reject(res)
		}
	},
	err => {
		if(err.response.status) {
			return Promise.reject(error.response)
		}
	}
)

Axios请求封装

export function httpRequest({url, type, {params = {}, data = {}}}) {
	const method = toUpperCase(type || 'get')
	return new Promise((resolve, reject) => {
		axios({
			method,
			url,
			data: data || {},
			params: params || {}
		})
			.then(res=> resolve(res))
			.catch(err => reject(err))
	})
}

Axios取消请求

  • 方法1:CancekTiken.source().cancel()
const CancelToken = axios.CancelToken;
const source = CancekTiken.source()

axios.get(url, {cancelToken: source.token})

// 取消请求
source.cancel('请求原因')
  • 方法2
const CancelToken = axios.CancelToken;
let cancel = null

axios.get(url, {
	cancelToken: new CancelToken(function exectutor(x) {
		cancel = x
	})
})

// 取消请求
cancel('请求原因')

Vue 项目中的错误处理

错误类型

  1. 接口错误
  2. 代码逻辑错误

处理办法

  1. 使用Axios响应拦截器,实现网络请求响应结果的拦截(axios.interceptors.response.use());
  2. 代码逻辑错误,设置全局错误处理函数(errorHandler:指定组件的渲染和观察期间未捕获错误的处理函数。)
Vue.config.errorHandler = (err, vm, info) => {
	// 只在v2.2.0+可用
}

Vue 项目的权限管理

前端权限控制分为4个方面:

  1. 接口权限
  2. 按钮权限
  3. 菜单权限
  4. 路由权限

接口权限

一般在请求拦截器进行token校验与拦截处理以及响应拦截处理(没有权限一般为401

路由权限

  • 方法一
    • 初始化挂载全部路由,并在路由上标记相应权限信息,由全局导航守卫拦截判断,再跳转
      • 缺点:冗余、性能消耗、修改信息需要重新编译等麻烦操作
  • 方法二
    • 初始化先挂载不需要权限控制的路由,再由登陆成功后获取的权限信息筛选可访问的路由,在全局导航守卫上进行addRoutes的调用和添加路由,按需加载
      • 缺点:每次路由跳转都得判断,修改信息需要重新编译等麻烦操作。

菜单权限

菜单权限可以理解为将页面与路由进行解耦。

实现方法

  1. 方法一:菜单由后端返回,前端定义路由信息
    • 缺点:菜单需要与路由做一一对应关系;全局导航守卫里,每次跳转都要做判断
  2. 方法二:菜单和路由都由后端返回,前端统一定义路由组件
    • 缺点:全局导航守卫里,每次跳转都需要做判断;前后端配合要求高。

按钮权限

一般采用自定义按钮权限鉴权指令实现。

Vue.directive('permit', {
	bind(el, binding, vnode) {
		let pemitArr = []
		if(binding.value) {
			permitArr = Array,of(bingding.value)
		} else {
			permitArr = vnode.context.$route.meta.permits
		}
	}
})

SPA单页面应用

SPA单页面应用,通过动态重写当前页面与用户交互,根据需要动态状态适当资源并装载到页面上,页面任何时间点都不会重新加载。
MPA多页面应用,每个页面都是一个主页面,每次访问一个页面,都需要重新加载html/css/js文件。

SPA的优缺点

优点

  1. 具有桌面应用的及时性、网站的可移植性和可访问性;
  2. 用户体验好、快,内容的改变不需要重新加载整个页面;
  3. 良好的前后端分离,分工明确;

缺点

  1. 不利于搜索引擎的抓取
  2. 首次渲染速度相对较慢

SPA的实现

  1. hash模式
    核心是通过监听URL中的hash来进行路由跳转

  2. history模式
    核心是借用HTML5 history API

    • history.pushState:添加历史记录
    • history.replaceState:修改历史记录

SPA如何做SEO

基于VueSPA实现SEO的3种方式:

  1. SSR服务端渲染:将组件或页面通过服务器生成html,再返回给浏览器
  2. 静态化
  3. 使用Phantomjs针对爬虫处理:原理是通过Nginx配置,针对来源是爬虫的请求(user-agent判断),转发大一个node server,再通过Phantomjs来解析完整的HTML,返回给爬虫。

SPA首屏加载速度慢解决办法

首屏加载时间可以通过DOMContentLoad或者performance来计算。

// 方案一
document.addEventListener('DOMContentLoad', evt => {
	console.log('first contentful painting')
})
// 方案二
performance.getEntriesByName('first-contentful-paint')[0].startTime

SPA首屏加载慢的原因:

  1. 网络延迟
  2. 资源文件体积大
  3. 资源重复请求加载
  4. 加载脚本过程中,渲染内容堵塞

解决办法

  • 减少入口体积:路由懒加载
  • 静态资源文件本地缓存:HTTP缓存等;
  • UI框架按需加载
  • 重复组件打包:commonsChunkPlugin中的minChunks
  • 图片资源压缩
  • 开启GZip压缩:compression-webpack-plugin
  • 使用SSR(服务器端渲染,常用Nuxt.js

SSR

SSR是什么

SSR:服务器端渲染。只由服务器完成页面的HTML结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。

SSR解决了什么

  1. SEO:搜索引擎优先爬取页面结构,使用SSR,服务端已经生成了和业务管理的HTML,有利于SEO;
  2. 首屏呈现渲染:用户无需等待页面所有js加载完成后才能看到视图

缺点

  • 复杂度:整个项目的复杂度
  • 库的支持性,代码兼容
  • 性能问题
  • 服务器负载变大

所以使用SSR,需要慎重考虑:

  1. 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染实现;
  2. 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢

相关推荐

  1. 2024前端面试Vue

    2024-07-11 11:58:04       18 阅读
  2. 2024前端面试【CSS

    2024-07-11 11:58:04       21 阅读
  3. 2024前端面试【手写

    2024-07-11 11:58:04       14 阅读
  4. 2024 前端高频面试Vue

    2024-07-11 11:58:04       44 阅读
  5. 前端面试

    2024-07-11 11:58:04       30 阅读
  6. 前端面试-vue

    2024-07-11 11:58:04       35 阅读
  7. 2024 前端高频面试之 浏览器原理

    2024-07-11 11:58:04       53 阅读
  8. 2024前端面试准备之Vue3

    2024-07-11 11:58:04       57 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 11:58:04       49 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 11:58:04       53 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 11:58:04       42 阅读
  4. Python语言-面向对象

    2024-07-11 11:58:04       53 阅读

热门阅读

  1. 使用semgrep做代码规范扫描

    2024-07-11 11:58:04       18 阅读
  2. L4 Persistence and Streaming

    2024-07-11 11:58:04       15 阅读
  3. DDOS 攻击原理

    2024-07-11 11:58:04       16 阅读
  4. .net 8 使用 quic 协议通讯

    2024-07-11 11:58:04       19 阅读
  5. jvm 06 补充 OOM 和具体工具使用

    2024-07-11 11:58:04       21 阅读
  6. Chameleon:动态UI框架使用详解

    2024-07-11 11:58:04       21 阅读
  7. C# 8.0 新语法的学习和使用

    2024-07-11 11:58:04       17 阅读
  8. 字符串匹配

    2024-07-11 11:58:04       19 阅读
  9. 升序到降序的类型变化

    2024-07-11 11:58:04       18 阅读
  10. 编程入门题:画矩形(C语言版)

    2024-07-11 11:58:04       17 阅读