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()
通知所有Watcher,Watcher会调用patch(oldNode, Vnode)
给真实DOM打补丁,更新相应的视图。 - 通过
isSameVNode
进行判断,相同则会调用patchVNode
方法。
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实例挂载过程
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
本质就是一种优化策略,使同一时间的多次更新、多个数据更新影响到的视图,做一次更新即可。
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项目中的跨域问题
跨域的本质实际上就是浏览器的基于同源策略的一种安全手段。
所谓同源(即在同一个域)具有三个相同点:
- 协议相同(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首屏加载慢的原因:
- 网络延迟
- 资源文件体积大
- 资源重复请求加载
- 加载脚本过程中,渲染内容堵塞
解决办法
- 减少入口体积:路由懒加载
- 静态资源文件本地缓存:HTTP缓存等;
- UI框架按需加载
- 重复组件打包:
commonsChunkPlugin
中的minChunks
- 图片资源压缩
- 开启GZip压缩:
compression-webpack-plugin
- 使用SSR(服务器端渲染,常用Nuxt.js)
SSR
SSR是什么
SSR:服务器端渲染。只由服务器完成页面的HTML结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。
SSR解决了什么
- SEO:搜索引擎优先爬取页面结构,使用SSR,服务端已经生成了和业务管理的HTML,有利于SEO;
- 首屏呈现渲染:用户无需等待页面所有js加载完成后才能看到视图
缺点
- 复杂度:整个项目的复杂度
- 库的支持性,代码兼容
- 性能问题
- 服务器负载变大
所以使用SSR,需要慎重考虑:
- 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染实现;
- 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢