2024前端面试真题【Vue2 + Vue3篇】

4 篇文章 0 订阅
4 篇文章 0 订阅

Vue 的设计原则

  1. 渐进式JS框架:Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
  2. 易用性:
  3. 灵活性:
  4. 高效性:

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算法是一种通过同层的树节点进行比较的高效算法。特点:

  • 同层级比较:Vue的渲染时基于组件树的,每个组件都有自己在树中的固定位置(层级),比较过程中,发现两个VNode处于相同深度且类型匹配时,就认为它们在同一层级;
  • 双向遍历:diff比较过程中,循环从两边向中间聚拢进行比较;
  • 组件和元素级别:Vue的diff算法操作有两个粒度:组件和元素两个级别。

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

  1. 头头、尾尾、头尾比较:Vue采用双指针策略比较;
  2. 双端diff算法:Vue3.x采用双端diff算法,从数组两端开始遍历,比较相邻元素是否相同,如果不同则标记为删除或添加。

原理分析

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

比较处理流程

  1. 新旧节点比较:从头开始比较新旧节点的VNode,如果相同则直接复用并执行patch操作,知道找到不同的节点为止;
  2. 双向遍历:Vue会从尾开始比较,同样的,知道找到不同的节点为止;
  3. 处理剩余节点:新的增,旧的删等;

diff算法的优化策略
4. 使用key进行节点复用;
5. 最长递增子序列:Vue3.x在处理剩余节点时,会采用最长递增子序列算法,来计算节点的移动路径,以最小化DOM的变动。

Vue的模板编译

V也中国的模板 template无法被浏览器解析并渲染,因为这不属于浏览器标准,所以需要将template转换为一个JS函数,让浏览器执行并渲染出对应的HTML函数,让视图跑起来,这一个转换过程就称为模板编译

模板编译分为3个阶段:

  • 解析parse:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST;
  • 优化optimize:遍历AST,找到其中一些静态节点进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能;
  • 生成generate:将最终的AST转换为render函数字符串。

最终生成可执行函数render

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中的某一属性的值发生变化后,会立即更新视图吗

答案:不会。
原因:V肚饿的响应式是按一定的策略进行DOM的更新的。Vue在更新DOM时是异步执行的,只要监听到数据变化,Vue将开启一个队列,缓冲在同一事件玄幻中发生的所有数据变更。

如果同一个watcher被多次触发,只会被推入到队列中一次。在下一个时间循环tick中,Vue刷新队列并执行实际工作(已去重)。

Vue中封装的数组方法

在Vue红,使用Object.defineProperty()来实现数据双向绑定,但是无法监听到数据长度、内部、截取等变化,因此需要对这些操作进行hack,让Vue监听到其中的变化:

  • push()
  • shift()
  • unshift()
  • pop()
  • splice()
  • sort()
  • reverse()

具体实现方法:重写了数组中的这些原生方法,首选获取数组的__ob__,即Observer对象。如果有新的值,则调用obserArray继续对新的值观察变化,手动调用notify,通知渲染watcher,执行update

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本质就是一种优化策略,是对EvenLoop的一种应用。使同一时间的多次更新、多个数据更新影响到的视图,做一次更新即可。

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初始化页面闪动问题

使用Vue开发时,在Vue初始化之前,由于div是不归Vue管的,因此再写的代码还没有解析的情况下容易出现花屏的现象。

看到类似于{{message}}的字样,虽然很短暂,但是还是有必要解决。

[v-clock] {
	display: none;
}

还是没解决的话就在根元素上加上style="display: none" :style="{display:'block'}"

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. 加载脚本过程中,渲染内容堵塞

SPA首屏加载慢的解决办法

  • 减少入口体积:路由懒加载
  • 静态资源文件本地缓存: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. 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢

SSR实现

对于同构开发,我们依然使用webpack打包。实现:

  1. 服务端首屏渲染:生成一个服务端bundle文件

  2. 客户端激活:生成一个客户端bundle文件

  3. 代码结构:新增两个入口,其它与之前的Vue应用相同

|- src
	|--router
		|--index.js  # 路由声明文件
	|--store
		|--router  # 全局状态
	|--main.js  # Vue实例创建
	|--entry-client.js  # 客户端入口
	|--entry-server.js  # 服务端入口
  1. 路由配置Store配置
import Vue from 'vue'
import Router from 'vue-router'

Vue.user(Router)

export function createRouter() {
	return new Router({
		mode: 'history',
		routers: [
			// 客户端没有编译器  要写成渲染函数
			{path: '/', component: {render: h=>h('div', 'index page')}},
			{path: '404', component: {render: h=>h('div', '404 page')}}
		]
	})
}
import Vue from 'vue'
import Vuex from 'vuex'

Vue.user(Vuex)

export function createStore() {
	return new Vuex.store({
		state: {},
		mutations: {},
		actions: {
			// 异步请求
		}
	})
}
  1. main.js配置:主文件是负责创建Vue实例的工厂,每次请求均会有独立的Vue实例创建。
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

// 客户端数据预取处理
Vue.mixin({
	beforeMount() {
		const { asyncData } = this.$options
		if(asyncData) {
			this.dataPromise = asyncData({
				store: this.$store,
				route: this.$route
			})
		}
	}
})

// 导出Vue实例工厂函数 为每次请求创建独立的实例(上下文用于给Vue实例传递参数)
export function createApp(context) {
	const router = createRouter()
	const app = new Vue({
		router,
		context,
		store,
		render: h=>h(App)
	})
	return {app, router}
}
  1. entry-server.js服务端入口配置
    主要任务时创建Vue实例,根据传入的URL指定首屏
import {createApp} from './main'
// 返回一个函数 接收请求上下文  返回创建的实例
export default context => {
	// 返回一个Promise 确保路由、组件准备就绪
	return new Promise((resolve, reject) => {
		const {app, router} = createApp(context)
		// 跳转到首屏地址
		router.push(context.url)
		// 路由准备就绪 返回结果
		router.onReady(_=>{
			// 获取匹配的路由组件数组
			const matchedComponents = router.getMatchedComponents()
			// 无匹配 抛出异常
			if(!matchedComponents.length) return reject({code: 404})
			// 对所有匹配的路由组件调用可能存在的异步数据获取`asyncData()`
			Promise.all(
				matchedComponents.map(com => {
					if(com.asyncData) return com.asyncData({
						store,
						router: router.currentRoute
					})
				})
			).then( _ => {
				// 所有预取钩子 resolve 后,store已经填入渲染应用所需的状态 将状态附加到上下文
				// 且 template 渲染用于 renderer 时,状态将自动序列化为 window.__INITIAL_STATE__,并注入HTML
				content.state = store.state
				resolve(app)
			}).catch(reject)
		}, reject)
	})
}
  1. entry-client.js客户端入口配置
    主要是创建Vue实例,执行挂载。即激活页面。
import { createApp } from './main'
// 创建vue、router实例
const { app, router, store } = createApp()

// 当时用template时,context.state将作为 window.__INITIAL_STATE__ 状态自动嵌入到HTML汇总
// 客户端挂载到应用程序之前  store就应该获取到状态
if(window.__INITIAL_STATE__) {
	store.replaceState(window.__INITIAL_STATE__)
}

// 路由就绪  执行挂载激活
router.onReady(_ => {
	app.$mount('#app')
})

  1. Webpack配置
  • 插件安装
yarn add webpack-node-externals loadsh.merge -D

yarn add cross-env -D
  • vue.config.js文件配置:
const VueSSRServePlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')

// 根据环境变量决定入口文件和配置项
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'

module.exports = {
	css: { extract : false },
	outputDir: `./dist/${target}`,
	configureWebpack: _ => ({
		// 入口文件配置
		entry: `./src/entry-${target}.js`,
		// 对 bundle renderer 提供 source map支持
		devtool: 'source-map',
		// target 设置为node:使webpack以Node适用的方式处理动态导入,并且还会在编译Vue组件时告知 vue-loader 输出面向服务器的代码
		target: TARGET_NODE ? 'node' : 'web',
		// 是否模拟node全局变量
		node: TARGET_NODE ? undefined : false,
		output: {
			// 适用Node峰哥导出模块
			libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
		},
		// 外置化应用程序依赖模块 提高构建速度,生成较小打包文件
		externals: TARGET_NODE ? nodeExternals({ whitelist: [/\.css$/] }) : undefined,
		optimization: { splitChunks: undefined },
		plugins: [TARGET_NODE ? new VueSSRServePlugin() : VueSSRClientPlugin]
	}),
	chainWebpack: config => {
		// cli4项目 补充添加
		if(TARGET_NODE) {
			config.optimization.delete('splitChunks')
		}
		config.module
			.rule('vue')
			.use('vue-loader')
			.tap(opt => {
				merge(opt, {optimizeSSR: false})
			})
	}
}

package.json脚本配置

"scripts": {
	"build:client": "vue-cli-service build",
	"build:server": "cros-env WEBPACK_TARGET=node vue-cli-service build",
	"build": "npm run build:server && npm run build:client"
}

index.html文件配置

服务端渲染入口位置。(不能为了好看而在后面加空格!!!

<body>
	<!--vue-ssr-outlet-->
</body>

总结

  1. 使用SSR不存在单例模式,每次用户请求都会创建一个新的Vue实例
  2. 实现SSR,需要实现服务端首屏渲染好客户端激活
  3. 服务端异步获取数据,可分为首屏异步获取和切换组件获取
    • 首屏异步获取,在服务端预渲染的时候就应该完成
    • 切换组件,通过mixin混入,在beforeMount钩子完成数据获取

Vue2和Vue3的区别

Vue3的新特性

  1. 速度更快

    • 重写了虚拟DOM的实现
    • 编译模板的优化
    • 更高效的组件初始化
    • update性能提高1.3 ~ 2倍
    • SSR舒服提高了2~3倍
  2. 体积更小
    通过Webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的能够tree-shaking,有两大好处:

    • 对开发人员,能够对Vue实现更多的功能,而不必担忧整体体积过大
    • 对使用者,打包的包体积变小了
  3. 更易维护

    • composition API,灵活的逻辑组合与复用
    • 更好的 TypeScript 支持
    • 编译器重写
  4. 更接近原生

    • 可以自动以渲染API
  5. 更易使用

    • 响应式API暴露出来,轻松识别组件重新渲染的原因

Vue3新增功能

  • framents:组件支持多个根节点
  • <teleport>任意门,使可以在组件的逻辑位置写模板代码,然后在Vue应用范围之外渲染它。
  • composition API:组合式API,通过这种形式,我们能将相同功能的变量进行一个集中式管理;
  • creteRenderer:自定义渲染器,将Vue的开发模型扩展到其它平台。

Composition API的使用

  • setup入口
  • 响应式系统API
  • 生命周期钩子
  • 依赖注入
  • refs

Vue3其它变更

  • 全局API已更改为使用应用程序实例;全局和内部API已经被重构为可tree-shakable
  • 模板指令
    • 组件上的v-model用法更改
    • <template v-for>v-for节点上的key用法已更改
    • 在同一元素上v-ifv-for的优先级已更改
    • v-bind="object"排序敏感
    • v-for中的ref不再注册ref数组
  • 组件
    • 只能使用普通函数创建功能组件
    • functional属性在单文件组件(SFC
    • 异步组件现在需要defineAsyncComponent()方法创建
  • 渲染函数
    • 渲染函数API一改变
    • $scopedSlots 属性已删除,所有插槽都通过$slots作为函数暴露
    • 自定义指令API已更改为与组件生命周期一致
    • class名调整:v-enter-formv-leave-from
    • 组件watch选项和实例方法$watch不在支持点分隔字符串路径,请改用计算函数作为参数
    • Vue2中,应用跟容易的outerHTML将替换为根组件模板,Vue3使用应用程序容易的innerHTML
  • 生命周期钩子函数
    • destoryed重命名为unmountedbeforDestroy重命名为beforeUnmount
    • beforeCreatecreated钩子在setup合并使用;
  • <template>没有特殊指令的标记会被视为普通元素,并将生成原生的<template>元素。
  • 移除的API
    • 过滤filter
    • 内联模板attribute
    • $destory实例方法(用户不应再手动管理单个Vue组件的生命周期)

Vue2和Vue3的通信方式有什么异同

共有的
- props和emits:在Vue2和Vue3中,子组件通过this.emit(Vue2)或emit(Vue3中通过defineEmits宏定义后直接使用)来触发事件
- Vuex
- Vue3引入了Composition API,为组件通信提供了更灵活的方式。通过definePropsdefineEmits宏,可以在<script setup>标签中更简洁地声明props和自定义事件,这是Vue3特有的特性。
- v-model:在Vue3中,v-model的用法得到了扩展,支持了多个修饰符和自定义组件的双向绑定。这使得父子组件之间的双向通信变得更加灵活和强大。

Vue3

Composition API 与 Vue2 使用的 Options API

Vue2 项目普遍存在的以下问题:

  1. 代码的可读性随着组件变大而变差
  2. 每一种代码复用的方式度存在缺点
  3. TypeScript支持有限

以上问题,通过使用 Composition API都解决了。

Options API

Options API:选项API,即以.vue为后缀的文件。通过methodscomputedwatchdata等属性与方法,共同处理页面逻辑。

export default {
	data: _=>({}),
	computed: {},
	watch: {},
	methods: {},
}

Composition API

Composition API中,组件根据逻辑功能来组织,一个功能所定义的所有API会放一起(高内聚,低耦合)。

Composition API 与 Options API 区别

  • 逻辑组织
    Options API将参数和实现等分离组织,对逻辑关注点存在含糊不清等问题;Composition API则是支持将某个关注点相关的代码全部放在一个函数里,修改功能时,能快速定位和实现。

  • 逻辑复用
    Vue2使用mixins实现逻辑实现复用,支持多个mixin文件使用。存在问题:

    1. 命名冲突
    2. 数据来源不清晰

而通过Composition API,编写hook函数,组件内请求使用,数据来源清晰,且命名冲突问题也会解决了。

总结

  1. 在逻辑组织和逻辑复用方面,Composition API优于Options API
  2. Composition API几乎都是函数,会有更好的类型推断
  3. Composition APItree-skaking更友好
  4. Composition API中几乎见不到this的使用,减少了this指向不明的情况
  5. 小型组件,使用Options API,比较友好

Vue3项目中,如何选择组合式API和选项式API

  1. 项目规模和复杂度:小项目或者组件逻辑相对简单的场景,选项式API是一个更快速、更直观的选择;大项目或者复杂组件,组合式API提供了更好的代码组织和复用能力,更容易管理状态、属性等;
  2. 逻辑复用性:逻辑相对独立的低复用性需求,选项式API更适合。多逻辑复用的高复用性需求,组合式APIP的setupcomposable函数将允许将逻辑提取到单文件中按需导入,非常有用。
  3. 团队习惯和个人偏好
  4. 学习和迁移成本

Vue3的设计目标是什么?做了那些优化?

Vue3的设计目标

更小更快更友好

  1. 更小:移除不常用API,引入tree-shaking,将无用模块剪辑,按需打包;
  2. 更快diff算法优化、静态提升、事件监听缓存、SSR优化等;
  3. 更友好:推出了Composition API,大大增加了代码的逻辑组织和复用能力。

示例:获取鼠标位置
utils.js

import {toRefs, reactive, onMounted, onBeforeUnmount} from 'vue'
export function getMouse() {
	const state = reactive({
		x: 0,
		y: 0
	})
	const update = e => {
		state.x = e.pageX
		state.y = e.pageY
	}
	onMounted(_ => {
		window.addEventListener('mousemove', update)
	})
	onBeforeUnmount(_ => {
		window.removeEventListener('mousemove', update)
	})
	return toRefs(state)
}

使用:

<script>
import {getMouse} from '@/utils/utils'
export function {
	setup() {
		const {x, y} = getMouse()
		return {
			x,
			y
		}
	}
}
</script>

Vue3 的优化

  1. 源码

    • Vue3的源码根据模块细分,依赖关系明确,更易阅读、理解;
    • 源码由TypeScript编写,提供了更好的类型检查,能支持复杂的类型推导
  2. 性能

    • 体积优化
    • 编译优化
    • 数据劫持优化:Object.defineProperty调整为Proxy监听整个对象。

    注意Proxy并不能监听到内部深层次的对象变化,Vue3处理方式:在getter中递归响应式(使得真正访问到的内部对象才变成响应式)。

  3. 语法API:即Composition API

Vue3 组件设计

实现一个 Modal组件,要求:

  • 遮罩层
  • 标题内容
  • 主体内容
  • 确定和取消按钮

实现流程

  1. 组件内容Modal组件一般挂载在body上,因为要在当前Vue实例外独立存在(使用Teleport传送门)。
<Teleport to='body' :disabled='!isTeleport'>
      <div v-if='modelValue' class='ui_modal'>
        <!--遮罩层-->
        <div class='mask' :style='style' @click='maskClose && !loading && handleCancel()' />
        <!--主体-->
        <div class='modal__main'>
          <!--标题栏-->
          <div class='modal__title line line--b'>
            <span>{{title || '标题'}}</span>
            <span v-if='close' title='关闭' class='close' @click='!loading && handleCcancel()'>x</span>
          </div>
          <div class='model__content'>
            <Content v-if='typeof content === "function"' :render='content' />
            <slot v-else>{{ content }}</slot>
          </div>
          <div class='model__bottom line line--t'>
            <button :disabled='loading' @click='handleConfirm()'>
              <span class='loading' v-if='loading'>...</span>确认
            </button>
            <button @click='!loading && handleCancel()'>
              取消
            </button>
          </div>
        </div>
      </div>
  </Teleport>

使用

$modal.show({
	title: 'Test',
	content(h){
		return h(
			'div',
			{
				style: 'color:red',
				onClick:($event: Event) => console.log('click', $event.target),
			},
			'hello world'
		)
	}
})

// JSX 语法
$modal.show({
	title: 'Test',
	content(){
		return (
			<div onClick={($event:Event) => console.log('click', $event.target)}>
				hellow world
			</div>
		)
})
  1. 实现API形式
    在Vue2中,我们可以借助Vue实例以及Vue.extend的方式获取组件实例,然后挂载到body上。

Vue3移除了Vue.extend()方法,但是可以通过createVNode实现:

import Modal from '@/components/Modal/Index'
const container = document.createElement('div')
const vnode = createVNode(Modal)
render(vnode, container)
const instance = vnode.component
document.body.appendChild(container)

在Vue2中,可以通过this调用全局API,但是Vue3的setup中已经没有this的概念了,需要调用app.config.globalProperties挂载到全局。

  1. 事件处理
setup(props, ctx) {
	let instance = getCurrentInstance(); // 获取当前组件实例
	onBeforeMount(_ => {
		instance._hub = {
			'on-confirm': _=>{},
			'on-cancel': _ => {}
		}
	});
	const handleConfirm = _ => {
		ctx.emit('on-confirm')
		instance._hub['on-confirm']()
	}
	const handleCancel = _ => {
		ctx.emit('on-cancel')
		ctx.emit('update:modalValue', false)
		instance._hub['on-cancel']()
	}
	return {
		handleConfirm,
		handleCancel
	}
}

_hub实现:

app.config.globalProperties.$modal = {
	show({
		onConfirm,
		onCancel
	}) {
		const {props, _hub} = instance;
		const _closeModal = _ => {
			props.modalValue = false
			constainer.parentNode!.removeChild(container)
		}
		// _hub 新增具体事件实现
		Object.assign(_hub, {
			async 'on-confirm'() {
				if(confirm) {
					const fn = onConfirm()
					if(fn && fn.then) {
						try {
							props.loading = true
							await fn;
							props.loading = false
							_closeModal()
						} catch (err) {
							console.log(err)
							props.loading = false
						}
					} else {
						_closeModal()
					}
				} else {
					_closeModal()
				}
			},
			'on-cancel'() {
				onCancel && onCancel()
				_closeModal()
			}
		})
	}
}

Vue3 性能提升主要提现在哪几方面

  1. 编译阶段
  2. 源码体积
  3. 响应式系统

编译阶段

Vue2中,每个组件实例都会对应一个 watcher 实例,在组件渲染过程中把用到的数据记录为依赖,改变会触发setter,通给值watcher,从而使关联的组件重新渲染。对于静态和动态节点共存时的变化,diff算法和遍历会浪费。

因此,Vue3在编译阶段,做了以下优化:

  1. diff算法优化:增加了静态标记
  2. 静态提升:把不参与更新的元素,做静态提升,只会被创建一次,在渲染时直接复用。
  3. 事件监听缓存:开启缓存,没有了静态标记,下次diff算法的时候可以直接使用。
  4. SSR优化:当静态内容大到一定量的时候,会用createStaticVNode方法在客户端生成一个static node,这些静态node会直接被innerHTML

源码体积

Vue3 移除了一些不常用的API,并使用Tree Shaking。任何一个函数,如:refreactive等,仅仅在用到的时候才打包,没用到的模块都会被摇掉。

响应式系统

Vue2采用Object.defineProperty来劫持整个对象,进行深度遍历属性,添加gettersetter实现响应式。

Vue3采用Proxy重写了响应式系统。由于Proxy可以对整个对象进行监听,因此不需要深度遍历,就可以监听数组的增、删、改等操作。

Vue3 为什么使用 Proxy API 替代 Object.defineProperty API?

Object.defineProperty()

**Object.defineProperty()**方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有对象,并返回此对象。

  1. 响应式实现

**Object.defineProperty()**通过gettersetter两个属性的使用,实现defineReactive()函数:

function updateVal() {
	APP.innerText = obj.foo
}
function defineReactive(obj, key, value) {
	Object.definerProperty(obj, key, {
		get() {
			return value
		},
		set(newValue) {
			if(newValue !== value) {
				value = newValue
				updateVal()
			}
		}
	})
}

defineReactive(obj, 'foo', '')

数据发生变化的时候,就会触发updateValue方法,实现数据响应。

在对象存在多个key值时,需要进行遍历、递归遍历等:

function observe(obj) {
	if(typeof obj !== 'object' || obj === null) {
		return
	}
	Object.keys(obj).forEach(key => {
		defineReactive(obj, key, obj[key])
	})
}

当给key赋值为对象时,还要再set属性中递归、并且在给一个对象进行删除、添加属性等操作时,无法劫持到、如果存在深层嵌套对象关系,需要深层的进行监听,造成很大性能问题等。

总结

  1. 检测不到对象属性的添加和删除
  2. 数组API方法无法监听到
  3. 需要对每个属性进行监听,如果嵌套对象,需要深层监听,造成性能问题。

Proxy

Proxy的监听是针对一个对象的,对对象的所有操作都会进入监听操作,这就完全可以代理所有属性了。

  1. 定义Proxy用于定义基本操作的自定义行为。

  2. 本质Proxy的修改的是程序默认行为,属于元编程Proxy用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如:属性查找、赋值等)

  3. 用法

Proxy为构造函数,用来生成Proxy实例

// target:要拦截的目标对象
// handler:通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理`P`的行为
const proxy = new Proxy(target, handler)

若在Proxy内部调用对象的默认行为,建议使用Reflect,基本特点

  • Reflect对象具有Proxy的一切代理方法,以静态方法的形式存在
  • 修改某些object方法的返回结果,让其变得更合理
  • object操作编程函数行为

Proxy的几种用法

  • get(target, key, receiver):能够对数组增删改查进行拦截;
  • set(target, key, value, receiver)
  1. 使用场景
    • 拦截和监听外部对象的访问
    • 降低函数或者类的复杂度
    • 在复杂操作前对操作进行校验或对所需资源进行管理

总结

Object.defineProperty()只能遍历对象属性进行劫持,Proxy直接可以劫持整个对象,并返回一个新对象。我们可以只操作新的对象达到响应式目的。

Vue3.0中的 Tree Shaking特性

Tree Shaking是什么

Tree Shaking:一种通过清除冗余代码方式来优化项目打包体积的技术。即:保持代码运行结果不变的前提下,清除无用代码。(eg:做蛋糕,蛋壳去掉,只留蛋白蛋黄放入搅拌)

import Vue from 'vue'

Vue2中,无论使用什么功能,最终都会出现在生成代码中,主要原因:Vue实例在项目中时单例的,捆绑程序无法检测属性是否被使用。
Vue3中,引入了Tree Shaking,将全局API进行分块。如果不使用某些功能,则不会包含在基础包中。

import {nextTick, observable}from 'vue'

Tree Shaking 如何做

Tree Shaking是基于ES6模板语法(importexports)实现的,主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。

Tree Shaking主要实现:

  1. 编译阶段利用ES Module判断哪些模块已经加载;
  2. 判断哪些模块和变量未被使用或引用,进而删除对应代码

Tree Shaking 的作用

通过Tree ShakingVue3给我们带来的好处是:

  1. 减少代码提交
  2. 减少程序执行事件
  3. 便于将来对程序架构进行优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值