v-show 和 v-if 区别
v-show
通过 CSS display 控制显示和隐藏v-if
通过判断组件真实渲染和销毁,而不是显示和隐藏- 频繁切换显示状态用
v-show
,否则用v-if
v-if
- 当
v-if
与v-for
一起使用时,v-for
具有比v-if
更高的优先级,意味着:v-if
将分别重复运行于每个v-for
循环中,会造成性能问题。所以,不推荐v-if
和v-for
同时使用
1 | const compiler = require('vue-template-compiler') |
v-show
1 | const compiler = require('vue-template-compiler') |
为何在 v-for 中用 key
- 必须用
key
,且不能是index
和random
diff
算法中通过tag
和key
来判断,是否是sameNode
- 减少渲染次数,提升渲染性能
描述 Vue 组件生命周期(父子组件)
beforeCreate
在初始化事件生命周期之后,数据被观测(observer)之前调用created
实例已经创建完成之后被调用可以进行一些数据、资源请求。在这个阶段无法与 DOM 进行交互,如果非想要,可以通过
$nextTick
访问beforeMount
在 DOM 挂载之前被调用,相关的render
函数首次被调用(如果有template
会转换成render
函数)在此时也可以对数据进行更改,不会触发
updated
mounted
创建vm.$el
并替换el
,并在挂载之后调用该钩子可以访问到 DOM 节点,使用
$refs
属性对 DOM 进行操作,也可以像后台发送请求,拿到返回数据beforeUpdate
数据更新时调用,发生在虚拟 DOM 重新渲染和和打补丁之前可以在这个钩子中进一步地更改状态,这不会触发附加的重新渲染
updated
由于数据更改导致虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子避免在此期间更改状态,可能会导致更新无限循环
beforeDestroy
实例销毁之前调用destroyed
Vue 实例销毁后调用,调用后,Vue 实例所有东西都会解绑定,所有事件监听会被移除,所有子实例也会被销毁可以执行一些优化操作,清除定时器,解除绑定事件
注意:除了 beforeCreate
和 created
钩子之外,其他钩子均在服务器端渲染期间不调用
1 | Vue.prototype._init = function (options?: Object) { |
mounted
(渲染完成)执行顺序是先子后父
1 | function patch(oldVnode, vnode, hydrating, removeOnly) { |
$destroy
(销毁完成)执行顺序是先子后父
1 | Vue.prototype.$destroy = function() { |
Vue 组件如何通讯
- 父 -> 子通过
props
,子 -> 父通过$on $emit
- 在父组件中提供数据子组件进行消费
provide
、inject
ref
获取实例的方式调用组件的属性或方法- 自定义事件
event.$on
、event.$off
、event.$emit
vuex
状态管理实现通信
描述组件渲染和更新过程
- 生成
render
函数,其生成一个vnode
,它会touch
触发getter
进行收集依赖 - 在模板中哪个被引用了就会将其用
Watcher
观察起来,发生了setter
也会将其Watcher
起来 - 如果之前已经被
Watcher
观察起来,发生更新进行重新渲染
双向数据绑定 v-model 的实现原理
v-model
本质上是语法糖,v-model
在内部为不同的输入元素使用不同的属性并抛出不同的事件
text
和textarea
元素使用 value 属性和 input 事件checkbox
和radio
使用 checked 属性和 change 事件select
字段将 value 作为 prop 并将 change 作为事件
1 | const compiler = require('vue-template-compiler') |
对 MVVM 的理解
Model
:代表数据模型,也可以在Model
中定义数据修改和操作的业务逻辑。我们可以把Model
称为数据层,因为它仅仅关注数据本身,不关心任何行为View
:用户操作界面。当ViewModel
对Model
进行更新的时候,会通过数据绑定更新到View
ViewModel
:业务逻辑层,View
需要什么数据,ViewModel
要提供这个数据;View
有某些操作,ViewModel
就要响应这些操作
总结: MVVM
模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM
在使用当中,利用双向绑定技术,使得 Model
变化时,ViewModel
会自动更新,而 ViewModel
变化时,View
也会自动变化
computed 和 watch 的区别
computed
:
computed
具有缓存性,computed
的值在getter
执行后是会缓存的,只有它依赖的属性值改变之后,下一次获取computed
的值时才会重新调用对应的getter
来计算computed
适用于比较消耗性能的计算场景,可以提高性能
watch
:
- 更多的是观察作用,类似于数据监听的回调函数,用于观察
props
、$emit
或本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
computed
和 watch
都支持对象的写法
1 | vm.$watch('obj', { |
为何组件 data 必须是一个函数
- 一个组件被复用多次的话,也就是创建多个实例。本质上,这些实例用的都是同一个构造函数,如果
data
是对象的话(引用数据类型),会影响到所有实例 - 为了组件不同实例
data
不冲突,data
必须是一个函数
自定义 v-model
v-model
可以看成是 value + input
方法的语法糖
- 自定义:自己写
model
属性,里面放上prop
和event
1 | <template> |
相同逻辑如何抽离
Vue.mixin
,给组件每个生命周期、函数都混入一些公共逻辑mixin
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项时也会有选择性进行合并
何时使用异步组件
核心就是把组件变成一个函数,依赖 import()
语法,可以实现文件的分割加载
- 加载大组件
- 路由异步加载
何时使用 keep-alive
常用的两个属性:include
、exclude
,允许组件有条件的进行缓存
两个生命周期:activated
、deactivated
,用来得知当前组件是否处于活跃状态
- 缓存组件实例,用于保留组件状态或避免重复渲染
- 多个静态 Tab 页的切换时,来优化性能
何时需要使用 beforeDestory
- 解绑自定义事件
event.$off
- 清除定时器
- 解绑自定义的 DOM 事件,如:
window
、scroll
等
action 和 mutation 有何区别
action
中可以处理异步,mutation
中不可以mutation
做的是原子操作,action
可以整合多个mutation
vue-router 常用的路由模式
hash 路由
hash
变化会触发网页跳转,即浏览器的前进、后退hash
变化不会刷新页面,SPA
必需的特点hash
永远不会提交到server
端(前端自生自灭)
history 路由
- 用
url
规范的路由,但跳转时不刷新页面 - 需要
server
端配合,可参考:后端配置例子 pushState
不会触发hashchange
事件,popstate
事件只会在浏览器某些行为下触发,比如点击后退、前进按钮
vnode 描述一个 DOM 结构
- Vue 中的真实 DOM
1 | <div id="div1" class="container"> |
- Vue 中的虚拟 DOM
1 | { |
数据响应式原理
核心 API:Object.defineProperty
存在一些问题,Vue 3.0 启动
Proxy
Proxy
可以原生支持监听数组变化但是
Proxy
兼容性不好,且无法polyfill
问题
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性(
Vue.set
、Vue.delete
) - 不能监听数组变化(重新定义原型,重写
push
、pop
等方法)
简单实现 Vue 中的 defineReactive
1 | // 触发更新视图 |
diff 算法
diff 算法过程:
- 同级元素进行比较,再比较子节点
- 先判断一方有子节点,一方没有子节点情况(如果新的
children
没有子节点,将旧的子节点移除) - 之后比较都有子节点的情况(核心
diff
),递归比较子节点
正常 diff
两个树的时间复杂度是 O(n^3)
,但实际情况我们很少会跨级移动 DOM。所以,只有当新旧 children
都为多个子节点时才需要核心的 diff
算法进行同层级比较
- Vue2 核心
diff
算法采用了双端比较的算法,同时从新旧children
的两端开始比较,借助key
值找到可复用的节点,再进行相关操作。相比 React 的diff
算法,同样情况可以减少移动节点次数,减少不必要的性能损耗 - Vue3 核心
diff
算法采用了最长递增子序列
双端比较算法
- 使用 旧列表 的头一个节点
oldStartNode
与 新列表 的头一个节点newStartNode
对比 - 使用 旧列表 的最后一个节点
oldEndNode
与 新列表 的最后一个节点newEndNode
对比 - 使用 旧列表 的头一个节点
oldStartNode
与 新列表 的最后一个节点newEndNode
对比 - 使用 旧列表 的最后一个节点
oldEndNode
与 新列表 的头一个节点newStartNode
对比
树 diff 的时间复杂度 O(n^3)
对于旧树上的点 E 来说,它要和新树上的所有点比较,复杂度为
O(n)
点 E 在新树上没有找到,点 E 会被删除,然后遍历新树上的所有点找到对应点(X)去填空,复杂度增加到
O(n^2)
这样的操作会在旧树的每个点进行,最终复杂度为
O(n^3)
1000 个节点,要计算 1 亿次,算法不可用
优化时间复杂度到 O(n)
- 只比较同一层级,不跨级比较
tag
不相同,则直接删掉重建,不再深度比较tag
和key
,两者都相同,则认为是相同节点,不再深度比较
简述 diff 算法过程
在 Vue 中,主要是 patch()
、patchVnode()
和 updateChildren
这三个方法来实现 Diff
的
当 Vue 中的响应式数据发生变化时,就会触发
updateCompoent()
updateComponent()
会调用patch()
方法,在该方法中进行比较,调用sameVnode
判断是否为相同节点(判断key
、tag
等静态属性),如果是相同节点的话执行patchVnode
方法,开始比较节点差异,如果不是相同节点的话,则进行替换操作patch()
接收新旧虚拟 DOM,即oldVnode
、vnode
- 首先判断
vnode
是否存在,如果不存在,删除旧节点 - 如果
vnode
存在,再判断oldVnode
,如果不存在,只需新增整个vnode
即可 - 如果
vnode
和oldVnode
都存在,判断两者是不是相同节点,如果是,调用patchVnode()
,对两个节点进行详细比较 - 如果两者不是相同节点,只需将
vnode
转换为真实 DOM 替换oldVnode
- 首先判断
patchVnode
同样接收新旧虚拟 DOM,即oldVnode
、vnode
- 首先判断两个虚拟 DOM 是不是全等,即没有任何变动,是的话直接结束函数,否者继续执行
- 其次更新节点的属性,接着判断
vnode.text
是否存在,存在的话只需更新节点文本即可,否则继续执行 - 判断
vnode
和oldVnode
是否有孩子节点- 如果两者都有孩子节点,执行
updateChildren()
,进行比较更新 - 如果
vnode
有孩子,oldVnode
没有,则直接删除所有孩子节点,并将该文本属性设为空 - 如果
oldVnode
有孩子,vnode
没有,则直接删除所有孩子节点 - 如果两者都没有孩子节点,就判断
oldVnode.text
是否有内容,有的话情况内容即可
- 如果两者都有孩子节点,执行
updateChildren
接收三个参数:parentElm
父级真实节点、oldCh
为oldVnode
的孩子节点、newCh
为Vnode
的孩子节点oldCh
和newCh
都是一个数组。正常我们想到的方法就是对这两个数组一一比较,时间复杂度为O(NM)
。Vue 中是通过四个指针实现的- 首先是
oldStartVnode
和newStartVnode
进行比较(两头比较),如果比较相同的话,就可以执行patchVnode
- 如果
oldStartVnode
和newStartVnode
匹配不上的话,接下来就是oldEndVnode
和newEndVnode
做比较了(两尾比较) - 如果两头和两尾比较都不是相同节点的话,就开始交叉比较,首先是
oldStartVnode
和newEndVnode
做比较(头尾比较) - 如果
oldStartVnode
和newEndVnode
匹配不上的话,就oldEndVnode
和newStartVnode
进行比较(尾头比较)
如果这四种比较方法都匹配不到相同节点,才是用暴力解法,针对
newStartVnode
去遍历oldCh
中剩余的节点,一一匹配- 首先是
图片来源:图文并茂地来详细讲讲Vue Diff算法
Vue 为何是异步渲染,$nextTick 何用
- 因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染。异步渲染(合并 data 修改),再更新视图,可以提高渲染性能
1 | class Watcher { |
$nextTick
在 DOM 更新完之后,触发回调,用于获得更新后的 DOM$nextTick
方法主要是使用了 宏任务 和 微任务,定义一个异步方法,多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列Vue 2.4 之前都是使用微任务,但是微任务的优先级过高,有些情况下可能会出现比事件冒泡更快的情况,但如果都是用宏任务,有可能会出现渲染的性能问题
新版,默认使用微任务,但在特殊情况下会使用宏任务,比如:
v-on
对于实现宏任务,会先判断是否能用
setImmediate
,不能的话降级为MessageChannel
,以上都不行的话就是用setTimeout
1 | if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { |
Vue 常见性能优化方式
- 合理使用
v-show
和v-if
- 合理使用
computed
v-for
时加key
(Vue 会进行复用),以及避免和v-if
同时使用- 自定义事件、DOM 事件及时销毁
- 合理使用异步组件、路由懒加载
- 合理使用
keep-alive
(SPA 页面) data
层级不要太深,不要讲所有数据都放在data
中(会增加getter
和setter
,收集对应watcher
)- 使用
vue-loader
在开发环境做模板编译(预编译) - webpack 层面的优化
- 前端通用的性能优化,如图片懒加载、防抖、节流
- 使用 SSR
释义参数 vue-template-compiler
render
中的参数释义
1 | _c = createElement |