Vue 的设计原则
- 渐进式JS框架:Vue被设计为可以自底向上逐层应用。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算法是一种通过同层的树节点进行比较的高效算法。特点:
- 同层级比较:Vue的渲染时基于组件树的,每个组件都有自己在树中的固定位置(层级),比较过程中,发现两个VNode处于相同深度且类型匹配时,就认为它们在同一层级;
- 双向遍历:diff比较过程中,循环从两边向中间聚拢进行比较;
- 组件和元素级别:Vue的diff算法操作有两个粒度:组件和元素两个级别。
比较方式
diff算法整体策略:深度优先,同层比较。
- 头头、尾尾、头尾比较:Vue采用双指针策略比较;
- 双端diff算法:Vue3.x采用双端diff算法,从数组两端开始遍历,比较相邻元素是否相同,如果不同则标记为删除或添加。
原理分析
- 当数据变化时,
set()
方法会调用Dep.notify()
通知所有Watcher,Watcher会调用patch(oldNode, Vnode)
给真实DOM打补丁,更新相应的视图。 - 通过
isSameVNode
进行判断,相同则会调用patchVNode
方法。
比较处理流程
- 新旧节点比较:从头开始比较新旧节点的VNode,如果相同则直接复用并执行patch操作,知道找到不同的节点为止;
- 双向遍历:Vue会从尾开始比较,同样的,知道找到不同的节点为止;
- 处理剩余节点:新的增,旧的删等;
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
文件都可以视为一个组件。优势:
- 降低系统的耦合度;
- 调试方便,组件之间职责单一,能快速定位问题;
- 提高可维护性
- 组件注册:全局注册(
Vue.component()
)、局部注册(components
)
插件
插件通常是用来给全局添加功能的,一般分为:
- 添加全局方法或属性;
- 添加全局之源;
- 通过全局混入添加一些组件选项;
- 添加Vue实例方法(
Vue.prototype
) - API功能库;
-
插件实现
Vue插件的实现是暴露一个install
方法。这个方法由构造器、可选的选项对象两个参数构成,即function(Vue, options)
-
插件注册:
Vue.use(插件名, {...options})
注意:插件注册,需要在调用new Vue()
启动之前完成。Vue.use()
会自动阻止多次注册相同插件。
Vue 生命周期
Vue的生命周期:从创建到销毁的过程。
Vue的生命周期共分为8个阶段 + 2个特殊特殊阶段:
- 创建前后:beforeCreated、created
- 初始化Vue实例,可访问、修改属性,
vm.$el
未创建 created
常用于异步数据获取
- 初始化Vue实例,可访问、修改属性,
- 载入前后:beforeMount、mounted
- DOM初始化,
$el
挂载完成 mounted
阶段创用于获取访问数据和dom
- DOM初始化,
- 更新前后:beforeUpdate、updated
- 销毁前后:beforeDestroy、destroy
- keep-alive缓存组件激活、停用:activated、deactivated
常见问题
数据请求放在created
和mounted
中有什么区别?
数据请求放在
mounted
阶段有可能会导致页面闪动,主要原因是由于此时页面DOM结构已经生成。所以建议在页面加载前完成请求,也就是在created
阶段完成。
双向数据绑定
双向数据绑定,主要是Model和View两者的数据更新问题。Vue的双向数据绑定主要由三个部分组成:
- Model(数据层):应用数据和业务逻辑;
- View(视图层):UI组件等展示;
- ViewModel(业务逻辑层):框架封装的核心,负责将数据与视图关联的控制层。
以上分层的架构方案即MVVM,VM即双向数据绑定的核心功能。主要职责:
- 数据变化后更新视图
- 视图变化后更新数据
ViewModel
ViewModel主要由两部分组成:
- 监听器(Observer):对数据的属性进行监听
- 解析器(Compiler):对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
实现双向绑定
Vue的双向绑定流程:
new Vue()
执行初始化,对data
执行响应式处理,Observer执行;- 同时对模板执行编译,找到动态绑定的数据,从
data
中获取并初始化视图,即compiler执行; - 同时定义一个更新函数和Watcher,将来对应数据变化时,Watcher会调用此更新函数处理;
- 由于
data
中的某个key
在一个视图中可能出现多次,所以每个key
会有一个Dep管家来管理多个Watcher; - 数据变化,找到对应的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的解析步骤
- 将html文档片段解析成AST描述符
- 将AST描述符解析成字符串
- 生成render函数,挂载到vm上(render的作用是生成
vNode
) _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()
attrs
与listeners
:祖先传递数据给子孙,包含了父级作用域中不作为props
被识别的特性绑定。可以使用v-bind=$attrs
传入内部组件provide
与inject
:组先传递数据给后代组件- 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-if
和 v-for
的优先级
v-for
与v-if
都是Vue模板系统中的指令。在Vue编译的时候,会先将指令系统转换为可执行的render
函数。
编译渲染过程中,都是线渲染,再判断,即:v-for
比v-if
优先级要高。
常见问题
v-for
和v-if
不建议同时使用在同一个元素上;
由于实现都是每次渲染都是先循环再进行条件判断,会带来不必要的性能浪费。必要时候可使用
<template>
标签做v-if
判断操作。
- 通常使用
computed
提前过滤不需要显示的项数据;
v-show
和 v-if
v-show
原理
不论初始条件是什么,元素总是会被渲染。有transaction
就执行,没有则设置display
属性。仅表示css属性的切换;
v-if
原理
返回一个Node节点,render
函数通过表达式的值决定是否生成DOM。
结论
v-show
和 v-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-alive
是Vue的一个内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive
的属性:
include
exclude
max
其中,keep-alive
是在组件的mounted
钩子函数中观测include
和exclude
的变化并处理的,this.cache
对象对缓存的组件做存取以及移除等。
keep-alive
的基本用法
<keep-alive>
<component :is="view" />
</keep-alive>
设置了keep-alive
缓存的组件,会有activated
和deactivated
l两个生命周期钩子。
- 首次进入组件:beforeRouterEnter > … > mounted > activated > … > beforeRouterLeave > deactivated
- 再次进入组件时:beforeRouterEnter > activated > … > beforeRouterLeave > deactivated
缓存的组件如何获取新数据
解决方案:
beforRouterEnter(to, from, next)
activated
(服务器端渲染期间,activated
不可用)
Vue初始化页面闪动问题
使用Vue开发时,在Vue初始化之前,由于div是不归Vue管的,因此再写的代码还没有解析的情况下容易出现花屏的现象。
看到类似于{{message}}的字样,虽然很短暂,但是还是有必要解决。
[v-clock] {
display: none;
}
还是没解决的话就在根元素上加上style="display: none" :style="{display:'block'}"
Vue项目中的跨域问题
跨域的本质实际上就是浏览器的基于同源策略的一种安全手段。
所谓同源(即在同一个域)具有三个相同点:
- 协议相同(protocol)
- 主机相同(host)
- 端口相同(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
属性配置
- 自定义指令的钩子函数
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
注意事项
- 过滤器可以接收参数,其中,表达式的值永远作为第一个参数;
- 一个表达式可以使用多个过滤器,用管道符**|**隔开。
原理分析
3. 在编一阶段通过parseFilters
将过滤器编译为函数调用;
4. 编译后通过resolveFilter
函数找到过滤器并返回结果
5. 执行结果作为参数传递给toString
,toString
执行后,其结果保存在Vnode的Text属性中,渲染到视图。
插槽 slot
slot其实就是Vue组件内的一个占位符,作为承载分发内容的出口,允许用户在使用的时候自定义内容。
slot使用场景
通过插槽,可以让用户自定义拓展所需的组件,去更好的复用以及定制化处理。
slot分类
插槽可以分为3类:
- 默认插槽
- 具名插槽:子
name
属性+父#name
。 - 作用域插槽:子组件在作用域上绑定属性,将子组件的信息传递给组件使用。
- 用法:
#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 项目中的错误处理
错误类型
- 接口错误
- 代码逻辑错误
处理办法
- 使用Axios响应拦截器,实现网络请求响应结果的拦截(
axios.interceptors.response.use()
); - 代码逻辑错误,设置全局错误处理函数(
errorHandler
:指定组件的渲染和观察期间未捕获错误的处理函数。)
Vue.config.errorHandler = (err, vm, info) => {
// 只在v2.2.0+可用
}
Vue 项目的权限管理
前端权限控制分为4个方面:
- 接口权限
- 按钮权限
- 菜单权限
- 路由权限
接口权限
一般在请求拦截器进行token校验与拦截处理以及响应拦截处理(没有权限一般为401)
路由权限
- 方法一
- 初始化挂载全部路由,并在路由上标记相应权限信息,由全局导航守卫拦截判断,再跳转
- 缺点:冗余、性能消耗、修改信息需要重新编译等麻烦操作
- 初始化挂载全部路由,并在路由上标记相应权限信息,由全局导航守卫拦截判断,再跳转
- 方法二
- 初始化先挂载不需要权限控制的路由,再由登陆成功后获取的权限信息筛选可访问的路由,在全局导航守卫上进行addRoutes的调用和添加路由,按需加载
- 缺点:每次路由跳转都得判断,修改信息需要重新编译等麻烦操作。
- 初始化先挂载不需要权限控制的路由,再由登陆成功后获取的权限信息筛选可访问的路由,在全局导航守卫上进行addRoutes的调用和添加路由,按需加载
菜单权限
菜单权限可以理解为将页面与路由进行解耦。
实现方法
- 方法一:菜单由后端返回,前端定义路由信息
- 缺点:菜单需要与路由做一一对应关系;全局导航守卫里,每次跳转都要做判断
- 方法二:菜单和路由都由后端返回,前端统一定义路由组件
- 缺点:全局导航守卫里,每次跳转都需要做判断;前后端配合要求高。
按钮权限
一般采用自定义按钮权限鉴权指令实现。
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的优缺点
优点
- 具有桌面应用的及时性、网站的可移植性和可访问性;
- 用户体验好、快,内容的改变不需要重新加载整个页面;
- 良好的前后端分离,分工明确;
缺点
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
SPA的实现
-
hash模式
核心是通过监听URL中的hash来进行路由跳转 -
history模式
核心是借用HTML5 history API:history.pushState
:添加历史记录history.replaceState
:修改历史记录
SPA如何做SEO
基于Vue的SPA实现SEO的3种方式:
- SSR服务端渲染:将组件或页面通过服务器生成html,再返回给浏览器
- 静态化
- 使用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首屏加载慢的原因
- 网络延迟
- 资源文件体积大
- 资源重复请求加载
- 加载脚本过程中,渲染内容堵塞
SPA首屏加载慢的解决办法
- 减少入口体积:路由懒加载
- 静态资源文件本地缓存:HTTP缓存等;
- UI框架按需加载
- 重复组件打包:
commonsChunkPlugin
中的minChunks
- 图片资源压缩
- 开启GZip压缩:
compression-webpack-plugin
- 使用SSR(服务器端渲染,常用Nuxt.js)
SSR
SSR是什么
SSR:服务器端渲染。只由服务器完成页面的HTML结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。
SSR解决了什么
- SEO:搜索引擎优先爬取页面结构,使用SSR,服务端已经生成了和业务管理的HTML,有利于SEO;
- 首屏呈现渲染:用户无需等待页面所有js加载完成后才能看到视图
缺点
- 复杂度:整个项目的复杂度
- 库的支持性,代码兼容
- 性能问题
- 服务器负载变大
所以使用SSR,需要慎重考虑:
- 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染实现;
- 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢
SSR实现
对于同构开发,我们依然使用webpack打包。实现:
-
服务端首屏渲染:生成一个服务端bundle文件
-
客户端激活:生成一个客户端bundle文件
-
代码结构:新增两个入口,其它与之前的Vue应用相同
|- src
|--router
|--index.js # 路由声明文件
|--store
|--router # 全局状态
|--main.js # Vue实例创建
|--entry-client.js # 客户端入口
|--entry-server.js # 服务端入口
- 路由配置、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: {
// 异步请求
}
})
}
- 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}
}
- 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)
})
}
- 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')
})
- 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>
总结
- 使用SSR不存在单例模式,每次用户请求都会创建一个新的Vue实例
- 实现SSR,需要实现服务端首屏渲染好客户端激活
- 服务端异步获取数据,可分为首屏异步获取和切换组件获取
- 首屏异步获取,在服务端预渲染的时候就应该完成
- 切换组件,通过mixin混入,在
beforeMount
钩子完成数据获取
Vue2和Vue3的区别
Vue3的新特性
-
速度更快
- 重写了虚拟DOM的实现
- 编译模板的优化
- 更高效的组件初始化
update
性能提高1.3 ~ 2倍- SSR舒服提高了2~3倍
-
体积更小
通过Webpack的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的能够tree-shaking
,有两大好处:- 对开发人员,能够对Vue实现更多的功能,而不必担忧整体体积过大
- 对使用者,打包的包体积变小了
-
更易维护
- composition API,灵活的逻辑组合与复用
- 更好的 TypeScript 支持
- 编译器重写
-
更接近原生
- 可以自动以渲染API
-
更易使用
- 响应式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-if
和v-for
的优先级已更改 v-bind="object"
排序敏感v-for
中的ref
不再注册ref
数组
- 组件上的
- 组件
- 只能使用普通函数创建功能组件
functional
属性在单文件组件(SFC)- 异步组件现在需要
defineAsyncComponent()
方法创建
- 渲染函数
- 渲染函数API一改变
$scopedSlots
属性已删除,所有插槽都通过$slots
作为函数暴露- 自定义指令API已更改为与组件生命周期一致
class
名调整:v-enter-form
、v-leave-from
- 组件
watch
选项和实例方法$watch
不在支持点分隔字符串路径,请改用计算函数作为参数 - Vue2中,应用跟容易的
outerHTML
将替换为根组件模板,Vue3使用应用程序容易的innerHTML
。
- 生命周期钩子函数
destoryed
重命名为unmounted
;beforDestroy
重命名为beforeUnmount
beforeCreate
和created
钩子在setup
合并使用;
<template>
没有特殊指令的标记会被视为普通元素,并将生成原生的<template>
元素。- 移除的API
- 过滤
filter
- 内联模板
attribute
$destory
实例方法(用户不应再手动管理单个Vue组件的生命周期)
- 过滤
Vue2和Vue3的通信方式有什么异同
共有的
- props和emits:在Vue2和Vue3中,子组件通过this.emit
(Vue2)或emit
(Vue3中通过defineEmits
宏定义后直接使用)来触发事件
- Vuex
- Vue3引入了Composition API,为组件通信提供了更灵活的方式。通过defineProps
和defineEmits
宏,可以在<script setup>
标签中更简洁地声明props和自定义事件,这是Vue3特有的特性。
- v-model
:在Vue3中,v-model
的用法得到了扩展,支持了多个修饰符和自定义组件的双向绑定。这使得父子组件之间的双向通信变得更加灵活和强大。
Vue3
Composition API 与 Vue2 使用的 Options API
Vue2 项目普遍存在的以下问题:
- 代码的可读性随着组件变大而变差
- 每一种代码复用的方式度存在缺点
- TypeScript支持有限
以上问题,通过使用 Composition API都解决了。
Options API
Options API:选项API,即以.vue
为后缀的文件。通过methods
、computed
、watch
、data
等属性与方法,共同处理页面逻辑。
export default {
data: _=>({}),
computed: {},
watch: {},
methods: {},
}
Composition API
Composition API中,组件根据逻辑功能来组织,一个功能所定义的所有API会放一起(高内聚,低耦合)。
Composition API 与 Options API 区别
-
逻辑组织
Options API将参数和实现等分离组织,对逻辑关注点存在含糊不清等问题;Composition API则是支持将某个关注点相关的代码全部放在一个函数里,修改功能时,能快速定位和实现。 -
逻辑复用
Vue2使用mixins
实现逻辑实现复用,支持多个mixin
文件使用。存在问题:- 命名冲突
- 数据来源不清晰
而通过Composition API,编写hook函数,组件内请求使用,数据来源清晰,且命名冲突问题也会解决了。
总结
- 在逻辑组织和逻辑复用方面,Composition API优于Options API;
- Composition API几乎都是函数,会有更好的类型推断
- Composition API对
tree-skaking
更友好 - Composition API中几乎见不到
this
的使用,减少了this
指向不明的情况 - 小型组件,使用Options API,比较友好
Vue3项目中,如何选择组合式API和选项式API
- 项目规模和复杂度:小项目或者组件逻辑相对简单的场景,选项式API是一个更快速、更直观的选择;大项目或者复杂组件,组合式API提供了更好的代码组织和复用能力,更容易管理状态、属性等;
- 逻辑复用性:逻辑相对独立的低复用性需求,选项式API更适合。多逻辑复用的高复用性需求,组合式APIP的
setup
和composable
函数将允许将逻辑提取到单文件中按需导入,非常有用。 - 团队习惯和个人偏好
- 学习和迁移成本
Vue3的设计目标是什么?做了那些优化?
Vue3的设计目标
更小、更快、更友好。
- 更小:移除不常用API,引入
tree-shaking
,将无用模块剪辑,按需打包; - 更快:diff算法优化、静态提升、事件监听缓存、SSR优化等;
- 更友好:推出了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 的优化
-
源码:
- Vue3的源码根据模块细分,依赖关系明确,更易阅读、理解;
- 源码由TypeScript编写,提供了更好的类型检查,能支持复杂的类型推导
-
性能
- 体积优化
- 编译优化
- 数据劫持优化:
Object.defineProperty
调整为Proxy监听整个对象。
注意:Proxy并不能监听到内部深层次的对象变化,Vue3处理方式:在
getter
中递归响应式(使得真正访问到的内部对象才变成响应式)。 -
语法API:即Composition API
Vue3 组件设计
实现一个 Modal组件,要求:
- 遮罩层
- 标题内容
- 主体内容
- 确定和取消按钮
实现流程
- 组件内容: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>
)
})
- 实现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
挂载到全局。
- 事件处理
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 性能提升主要提现在哪几方面
- 编译阶段
- 源码体积
- 响应式系统
编译阶段
Vue2中,每个组件实例都会对应一个 watcher
实例,在组件渲染过程中把用到的数据记录为依赖,改变会触发setter
,通给值watcher
,从而使关联的组件重新渲染。对于静态和动态节点共存时的变化,diff算法和遍历会浪费。
因此,Vue3在编译阶段,做了以下优化:
- diff算法优化:增加了静态标记
- 静态提升:把不参与更新的元素,做静态提升,只会被创建一次,在渲染时直接复用。
- 事件监听缓存:开启缓存,没有了静态标记,下次diff算法的时候可以直接使用。
- SSR优化:当静态内容大到一定量的时候,会用
createStaticVNode
方法在客户端生成一个static node,这些静态node会直接被innerHTML
。
源码体积
Vue3 移除了一些不常用的API,并使用Tree Shaking
。任何一个函数,如:ref
、reactive
等,仅仅在用到的时候才打包,没用到的模块都会被摇掉。
响应式系统
Vue2采用Object.defineProperty
来劫持整个对象,进行深度遍历属性,添加getter
和setter
实现响应式。
Vue3采用Proxy
重写了响应式系统。由于Proxy
可以对整个对象进行监听,因此不需要深度遍历,就可以监听数组的增、删、改等操作。
Vue3 为什么使用 Proxy API 替代 Object.defineProperty API?
Object.defineProperty()
**Object.defineProperty()**方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有对象,并返回此对象。
- 响应式实现
**Object.defineProperty()**通过getter
和setter
两个属性的使用,实现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
属性中递归、并且在给一个对象进行删除、添加属性等操作时,无法劫持到、如果存在深层嵌套对象关系,需要深层的进行监听,造成很大性能问题等。
总结
- 检测不到对象属性的添加和删除
- 数组API方法无法监听到
- 需要对每个属性进行监听,如果嵌套对象,需要深层监听,造成性能问题。
Proxy
Proxy的监听是针对一个对象的,对对象的所有操作都会进入监听操作,这就完全可以代理所有属性了。
-
定义:Proxy用于定义基本操作的自定义行为。
-
本质:Proxy的修改的是程序默认行为,属于元编程。Proxy用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如:属性查找、赋值等)
-
用法
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)
- 使用场景
- 拦截和监听外部对象的访问
- 降低函数或者类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
总结
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模板语法(import
、exports
)实现的,主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。
Tree Shaking主要实现:
- 编译阶段利用ES Module判断哪些模块已经加载;
- 判断哪些模块和变量未被使用或引用,进而删除对应代码
Tree Shaking 的作用
通过Tree Shaking,Vue3给我们带来的好处是:
- 减少代码提交
- 减少程序执行事件
- 便于将来对程序架构进行优化