提示:本文包含vue3版本的源码分析。写本文花了笔者将近1个月时间,建议小白收藏本文,有需要再拿出来看
为啥需要阅读vue源码
- 岗位需要。从这个HC可以看出,前端技术专家需要熟读源码
- 开发框架,需要开发框架或者库时,参考成熟的前端框架实现是有必要的
- 代码质量,vue作为一个优秀的开源库,学习它的设计思想和设计模式可以帮助我们写质量更高、性能更优的代码
Vue3新特性
组合式API(composition API)
为什么使用composition API
- options API的组件,比如在A组件中定义了B/C组件的data,methods,生命周期方法,computed,各个逻辑分散在组件的不同区域,代码难以复用,使用composition API解决了这个问题,可以做到高内聚、低耦合,代码可复用性和可维护性更好
- vue2逻辑复用使用的是mixins,当一个组件引用多个mixin时,想知道query来源于哪个mixin,需要在每个引用的mixin中寻找一遍方法,即数据来源不清晰;且多个mixin中定义的属性和方法会存在命名冲突问题;composition API解决了上面的问题
- 更好的类型推断,对Typescript更友好
- composition API看不到this的使用,解决了this指向不明的问题
mixins:[TabMixin,TableQueryMixin,GrayBackgroundMixin,BaseQueryMixin],
methods:{
query(){
...
}
}
teleport
Teleport类似于React的Portal,可以将组件挂载在任何DOM节点上
<button @click="openToast">打开toast</button>
<!--挂载在id为dialog的节点上-->
<teleport to="#dialog">
<div v-if="visible" class="toast-container">
<div class="toast-msg">我是一个toast</div>
</div>
</teleport>
Fragment
Fragment组件支持多个根节点,作用:减少标签层级,减少内存占用
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
diff算法优化
createRenderer
生命周期变更
beforeDestory->beforeUnmount
destroyed->unmounted
更多变更详见vue3迁移指南
vue3性能提升是通过哪些方面实现的
- 响应式系统升级,从vue2的Object.defineProperty变为了vue3的proxy,原因:
- proxy性能优于Object.defineProperty
此处存疑,实际上proxy性能比Object.defineProperty要差,那么为啥vue3还要用proxy?参考 thecodebarbarian.com/thoughts-on…
proxy本质是对某个属性的劫持,proxy可以监听数据的新增和删除,而Object.defineProperty做不到,只能监听属性的变化
- 可以监听数组的索引和length属性
- 可以监听动态属性的添加
- 可以监听删除属性
- 编译优化,主要有:
- diff算法优化
vue3相比vue2增加了静态标记,静态标记的作用是会标志为一个flag,下次发生变化的时候直接找该处进行比较;已经标记为静态节点的元素不会参与diff比较
// 静态类型枚举
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}
- 静态提升、树结构打平
vue3对不参与更新的元素做静态提升,只会被创建一次,在渲染时直接复用。 作用:减少重复节点的创建,节省内存开销
- 事件监听缓存
- SSR优化
更新类型标记和树结构打平都大大提升了 Vue SSR 激活的性能表现:
- 单个元素的激活可以基于相应 vnode 的更新类型标记走更快的捷径。
- 在激活时只有区块节点和其动态子节点需要被遍历,这在模板层面上实现更高效的部分激活。
- 源码体积优化,移除了一些不常用的API,再就是tree shaking,任何一个函数,比如ref、reactive,只有在用到的时候才进行打包,无用模块都被摇树优化,减少了打包代码体积
import {computed,ref} from "vue"
export default defineComponent({
setup(props,context){
const age = ref(18)
let state = reactive({name:"lyllovelemon"})
const readOnlyAge = computed(()=>age.value++)
return {
age,
state,
readOnlyAge
}
}
})
虚拟DOM
什么是虚拟DOM?如何实现虚拟DOM
React和Vue都使用了虚拟DOM技术,虚拟DOM是对真实DOM的一层抽象,它是一颗js对象树,用对象的属性描述节点,最后通过渲染器(renderer)将虚拟DOM渲染为真实DOM。
VNode不依赖某一个平台,它可以是浏览器平台,也可以是node平台或者weex平台,这也为前后端同构提供了可能
为什么要使用虚拟DOM
一个真实的dom元素,包含的属性和方法是很多的。浏览器在渲染DOM时性能开销很大,频繁渲染DOM最直接的结果就是页面卡顿。
// 在控制台输出document
document
而且浏览器每次收到DOM更新流程时会从头到尾执行一遍更新流程。当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。
而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算。
浏览器执行js运算的速度 > 渲染DOM元素的速度
在vue框架中,渲染流程为:
数据改变 -> 虚拟DOM ->操作真实DOM-> 视图更新
虚拟DOM是怎么变为真实DOM的
每一次DOM更新流程,Vue会用patch函数,对新老节点进行判断,执行创建/销毁节点。通过patchVnode函数和diff算法(参考了snabbdom),新旧Vnode diff比较,只更新有差异的部分
在页面首次渲染的时候会调用patch创建新的VNode,不会进行深层次的比较
每个组件对应一个Watcher实例,当数据变化,会触发setter通过notify通知Watcher,对应的Watcher会通知更新并执行更新函数,它会执行render函数获取新的虚拟DOM,然后执行patch比较新旧Vnode,得到有差异的部分;最后根据有差异的部分更新视图
Diff算法执行过程
vue2版本
- 只比较同一层级,不跨层比较
这样的优点是减少了比较次数,算法的事件复杂度降低
- 比较标签名
如果同一层级的标签名type不同,直接删除老的VNode
- 比较key
如果标签名和key相同,代表新旧VNode是同一个节点
Diff算法核心:patch,sameVNode,patchVnode,updateChildren
patch源码位置(vue2):vue/blob/main/src/core/vdom/patch.ts
主要逻辑:
- vnode存在,oldVNode不存在,新增VNode节点
- vnode不存在,oldVNode存在,删除oldVNode节点
- 两个都存在,通过sameVnode函数判断是不是同一个节点,如果是,通过patchVnode进行后续对比;不是则把VNode挂载在oldVNode的父元素下,如果组件的根节点被替换,就遍历父节点删除旧节点;如果是服务端渲染就通过hydrating把oldVNode和真实DOM结合
//vue2版本: vue/blob/main/src/core/vdom/patch.ts
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
// 如果新旧节点一致,不做处理直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
const elm = (vnode.elm = oldVnode.elm)
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新旧节点都是静态节点且新旧节点key相同(同一个节点)
// 当vnode是克隆节点且是v-once节点,只需要把oldVnode的组件实例赋给vnode节点
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// patch比较新旧节点,更新有差异的部分
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
// 新旧节点都有子节点,则处理比较更新子节点
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (__DEV__) {
// 新节点有子节点且在开发环境,检查重复的key
checkDuplicateKeys(ch)
}
// 旧节点有文本属性,设置为空文本元素
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
// 旧的子节点存在而新的子节点不存在,删除旧节点
else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
}
// 新旧节点文本内容不相同,直接插入新文本内容
else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode)
}
}
patchVnode主要做了几个判断:
- 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
- 新节点和旧节点如果都有子节点,则处理比较更新子节点
- 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
- 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除
再查看updateChildren方法
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
// 定义了四个指针:旧前、新前、旧后、新后
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
// 旧前节点
let oldStartVnode = oldCh[0]
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
// 新前节点
let newStartVnode = newCh[0]
// 新后节点
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
if (__DEV__) {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
// 1.旧前节点与新前节点命中
else if (sameVnode(oldStartVnode, newStartVnode)) {
// 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
// 2.旧后与新后命中
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
// 移动两个尾指针
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
// 3.旧前与新后命中
else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
// 当新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的旧后的后面
// 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
// 4.旧后与新前命中
else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
//当新前与旧后命中的时候,此时要移动节点。移动新前(旧后)指向的这个节点到老节点的 旧前的前面
// 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// 四种都没有匹配到,key相同但元素不同,创建新元素
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// 指针移动结束,旧的开始指针>旧的结束指针,代表产生了新元素
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// 节点新增
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
)
}
// 遍历结束,新的开始指针 > 新的结束指针,代表有元素需要被删除
else if (newStartIdx > newEndIdx) {
// 节点删除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
vue3版本的diff
- vue2是全量diff,vue3是静态标记+非全量diff,节点被打上静态标记就不需要参与diff
- 事件缓存,可以理解为事件为静态的,初始化后就会在更新时从缓存中查找事件
- vue3使用最长递增子序列,主要在patchKeyedChildren函数里
- 头和头比
- 尾和尾比
- 基于最长递增子序列进行新增/更新(移动)/删除
- 老的 children:[ a, b, c, d, e, f, g ]
- 新的 children:[ a, b, f, c, d, e, h, g ]
- 先进行头和头比,发现不同就结束循环,得到 [ a, b ]
- 再进行尾和尾比,发现不同就结束循环,得到 [ g ]
- 再保存没有比较过的节点 [ f, c, d, e, h ],并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的就说明是新增
- 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ]
- 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了
//packages/runtime-core/src/renderer.ts
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
i++
}
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`Duplicate keys found during update:`,
JSON.stringify(nextChild.key),
`Make sure keys are unique.`
)
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
// used to track whether any node has moved
let maxNewIndexSoFar = 0
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// key-less node, try to locate a key-less node of the same type
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
组件是怎么渲染成DOM的
template->render函数->虚拟DOM-> 真实DOM
源码解析
// vue2版本源码位置:src/core/vdom/vnode.js
export default class VNode {
tag: string | void; /*当前节点的标签名*/
data: VNodeData | void; /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
children: ?Array<VNode>; /*当前节点的子节点,是一个数组*/
text: string | void; /*当前节点的文本*/
elm: Node | void; /*当前虚拟节点对应的真实dom节点*/
ns: string | void; /*当前节点的命名空间*/
context: Component | void; /*编译作用域*/
key: string | number | void;/*节点的key属性,被当作节点的标志,用以优化*/
functionalContext: Component | void;/*函数化组件作用域*/
componentOptions: VNodeComponentOptions | void;/*组件的option选项*/
componentInstance: Component | void; /*当前节点对应的组件的实例*/
parent: VNode | void; /*当前节点的父节点*/
raw: boolean; /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
isStatic: boolean; /*是否静态节点*/
isRootInsert: boolean;/*是否作为根节点插入*/
isComment: boolean; /*是否为注释节点*/
isCloned: boolean; /*是否为克隆节点*/
isOnce: boolean;/*是否有v-once指令*/
constructor (
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions)
{
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.functionalContext = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false}
}
get child (): Component | void {
return this.componentInstance
}
}
举个例子,当前我有一颗VNode树,结构如下:
{
tag: 'div'
data: {
class: 'root'
},
children: [
{
tag: 'span',
data: {
class: 'demo'
}
text: 'hello,lyllovelemon'
}
]
}
渲染后变为:
<div class="root">
<span class="demo">hello,lyllovelemon</span>
</div>
//vue3版本源码位置:core/packages/runtime-core/src/vnode.ts(3.2.37版本)
...
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
// vnode节点标记,判断是否是vnode节点
__v_isVNode: true
// 响应式flag标记
[ReactiveFlags.SKIP]: true
// 节点类型:vnode节点
type: VNodeTypes
// 节点props属性
props: (VNodeProps & ExtraProps) | null
// 节点的key属性
key: string | number | symbol | null
// 节点的ref属性
ref: VNodeNormalizedRef | null
/**
* SFC only. This is assigned on vnode creation using currentScopeId
* which is set alongside currentRenderingInstance.
*/
scopeId: string | null
/**
* SFC only. This is assigned to:
* - Slot fragment vnodes with :slotted SFC styles.
* - Component vnodes (during patch/hydration) so that its root node can
* inherit the component's slotScopeIds
* @internal
*/
slotScopeIds: string[] | null
children: VNodeNormalizedChildren // 当前节点的子节点
component: ComponentInternalInstance | null // 当前节点的组件实例
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
el: HostNode | null // DOM
anchor: HostNode | null // fragment锚点
target: HostElement | null // teleport目标元素
targetAnchor: HostNode | null // teleport目标锚点
staticCount: number // 静态vnode节点个数
suspense: SuspenseBoundary | null// suspense
ssContent: VNode | null
ssFallback: VNode | null
// 仅用于optimization
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null // 动态props
dynamicChildren: VNode[] | null// 动态子节点,需要进行patch
appContext: AppContext | null // 仅用于应用根节点
memo?: any[] // v-memo
isCompatRoot?: true // 仅用于compact
ce?: (instance: ComponentInternalInstance) => void // 内部自定义元素的拦截hook
}
创建虚拟DOM
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,// 节点类型
props: (Data & VNodeProps) | null = null, // 节点props属性
children: unknown = null, // 子元素
patchFlag: number = 0, // patch标志
dynamicProps: string[] | null = null, // 动态props属性
isBlockNode = false // 是否为区块节点,区块节点指内部结构稳定的节点
): VNode {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment // 注释节点
}
if (isVNode(type)) {
// 创建一个克隆的虚拟DOM,接收3个参数,分别为节点类型、节点props属性、是否合并ref属性
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)// 存在子节点,则建立当前虚拟节点与子节点的联系
}
// 不为区块节点且当前的节点存在
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 2.x async/functional component compat
if (__COMPAT__) {type = convertLegacyComponent(type, currentRenderingInstance) }
// class & style normalization.
if (props) {
// 用于对象的响应式或代理, we need to clone it to enable mutation.
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass) // 标准化class,每一项class都转为字符串处理
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)// 标准化style
}
}
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with `markRaw` or using `shallowRef` ` +
`instead of `ref`.`,
`\nComponent that was made reactive: `,
type
)
}
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
树结构打平
这里我们引入一个概念“区块”,内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if 或者 v-for)。
每一个块都会追踪其所有带更新类型标记的后代节点 (不只是直接子节点),举例来说:
<div> <!-- root block -->
<div>...</div> <!-- 不会追踪 -->
<div :id="id"></div> <!-- 要追踪 -->
<div> <!-- 不会追踪 -->
<div>{{ bar }}</div> <!-- 要追踪 -->
</div>
</div>
编译的结果会打平为一个数组,仅包含所有动态的后代节点dynamicChildren
div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定
当组件需要重渲染时,只需要遍历这个打平的树而不是整棵树,这个过程叫树结构打平,大大减少了虚拟DOM需要遍历的节点数量,模板中任何静态的部分都会被跳过
依赖收集与响应式原理
什么是响应式
数据变化驱动视图更新就叫响应式
vue2的响应式是通过Object.defineProperty实现的,有以下几个问题:
- 不能深层监听对象的变化
- 不能获取数组下标和length
let val='lyllovelemon'
Object.defineProperty(obj,'name',{
configurable:true,// 属性是否可配置,默认为false,为true时对应属性可被删除和修改,并且可以通过Object.defineProperty修改
enumerable:true,// 属性是否可枚举,默认为false,为true是属性可被for...in或者Object.keys遍历
writable:true,// 属性是否可写,默认为false,为true时属性值才可改变
value:'lyllovelemon',// 属性的初始值,可以是任何有效的JavaScript值(数值、对象、函数等),默认为undefined
get(){
return val
},// 属性读取
set(newVal){
val = newVal
}// 属性写入
})
console.log(obj.name)// 'lyllovelemon',表示属性的value已生效
obj.name='lyl'
console.log(obj.name)// 'lyl',getter和setter都已生效
vue3使用ES6的proxy实现响应式,proxy是支持数组的,因此解决了无法获取数组下标和length的问题;对于深层监听也不需要使用递归解决,当get判断值为对象时,将对象响应式处理即可
原理实现
vue3源码位置:packages/reactivity/src/reactive.ts
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// 监听元素为只读的,不做处理返回
if (isReadonly(target)) {
return target
}
// 否则通过createReactiveObject处理
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
响应式实际是在createReactiveObject函数中处理的,该函数主要做了几件事:
- 判断监听
- 实例化proxy对象
function createReactiveObject(
target: Target, // 监听的元素
isReadonly: boolean, // 是否只读
baseHandlers: ProxyHandler<any>, // proxy处理函数
collectionHandlers: ProxyHandler<any>,// 收集变化函数
proxyMap: WeakMap<Target, any> // proxyMap
) {
// 监听元素不是对象则返回该元素
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 监听元素是原生且不是只读元素,是响应式的则返回该元素
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 监听元素已经被Proxy处理过了,从proxyMap中查找该元素并返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 获取元素类型
const targetType = getTargetType(target)
// 监听元素为基本数据类型则返回该元素
if (targetType === TargetType.INVALID) {
return target
}
// new实例化proxy对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 将监听元素加入到proxyMap并返回监听元素
proxyMap.set(target, proxy)
return proxy
}
getTargetType函数主要用于处理元素类型
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
//TargetType常量定义,基本数据类型为INVALID,object/array为COMMON
// Map/Set/WeakMap/WeakSet为COLLECTION
const enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
事件机制原理
Vue事件机制API
vue提供了四个事件机制API,分别是:off,once
初始化事件
vue2版本源码位置:vue/blob/main/src/core/instance/events.ts
核心方法为initEvents,主要作用:
- 初始化events和hasHookEvent变量
- 父组件绑定的事件存在就调用updateComponentListeners,这个函数里调用updateListeners,接收新旧事件对象作为参数,遍历新事件对象,标准化事件,事件绑定了once就调用对应逻辑;旧事件没有新事件有,创建事件对象,旧事件有新事件没有,删除对应旧事件;新旧事件不一致使用新事件
export function initEvents(vm: Component) {
// 给vm实例绑定_events属性,值为null
vm._events = Object.create(null)
// 给vm实例绑定_hasHookEvent实例,标志是否存在钩子
vm._hasHookEvent = false
// 初始化父组件绑定的事件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
$on
emit触发
// 接收2个参数,分别为事件名(事件数组),需要执行的函数
Vue.prototype.$on = function (
event: string | Array<string>,
fn: Function
): Component {
const vm: Component = this
// 如果events是数组,遍历执行,给每一项事件绑定fn函数
if (isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// events是单个,建立一个空数组,将fn函数push进数组
;(vm._events[event] || (vm._events[event] = [])).push(fn)
// hook性能优化专用
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
$off
$off用于移除自定义事件
//接收2个可选参数,分别为事件名(事件数组),需要执行的函数
Vue.prototype.$off = function (
event?: string | Array<string>,
fn?: Function
): Component {
const vm: Component = this
// off函数未传参,将事件置空,返回vm实例
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// events是数组,遍历每一项解除函数绑定,返回vm实例
if (isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event!]
if (!cbs) {
return vm
}
// fn未传参,清空events数组
if (!fn) {
vm._events[event!] = null
return vm
}
// 特别处理cbs,遍历每一项移除
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
$emit
$emit触发自定义事件
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (__DEV__) {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(
vm
)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(
event
)}" instead of "${event}".`
)
}
}
// 从vm的_events属性中获取cbs
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// cbs类数组转数组
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
// 遍历cbs每一项调用invokeWithErrorHandling,向上冒泡捕获错误
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
$once
$once自定义一个只执行一次的事件,执行过后自动移除该事件
// 接收两个参数,分别为事件名和需要执行的函数
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
// 内部定义on方法,做2件事:
// 1.在第1次执行的时候将事件销毁
// 2. 将实例绑定到fn函数
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
事件缓存
vue3在vue2的基础上增加了事件监听缓存
模板编译原理
template函数怎么编译成render函数的
vue2版本创建vue实例时,组件通过 _init方法进行初始化, 这个方法主要用于初始化data,method,生命周期等属性,最后调用$mount进行实例挂载
//vue2.7.10源码地址:src/core/instance/init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 实例挂载
}
}
}
查看$mount方法,render函数不存在,将template通过compilerToFunctions方法编译成得到AST,render和staticRenderFns(vue的编译优化,静态节点不需要patch),render函数在运行时会返回虚拟DOM 核心方法就是compileToFunctions
//src/platforms/web/runtime-with-compiler.ts
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
compileToFunction是在createCompiler方法中被调用的,createCompiler调用了parse进行模板编译
//src/platforms/web/compiler/index.ts
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
//src/platforms/web/compiler/index.ts
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)//1.parse模板解析
if (options.optimize !== false) {
optimize(ast, options)// 2.optimize 优化
}
const code = generate(ast, options)//3.generate 代码生成
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
template -> AST -> render
- parse方法主要用于处理template,通过正则表达式解析template模板中的指令、class、style等数据,生成AST
- optimize主要用于标记static静态节点和静态根节点,这是vue编译时进行的优化,优点是只有在挂载的时候生成,静态节点不会参与后续的diff,也就是说视图更新时,会跳过静态节点的比较,减少了节点的比较次数,diff性能得到了提升
- generate,主要用于将AST转换为render function字符串,得到render字符串和staticRenderFns字符串
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast
? ast.tag === 'script'
? 'null'
: genElement(ast, state)
: '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
Transition实现原理
Transition组件基本使用
Transition组件是vue提供的内置组件,用于制作状态变化的动画
会在组件进入或离开DOM时应用动画
<Transition name="fade" mode="out-in">
<p v-if="show">hello,lyllovelemon</p>
</Transition>
<style scoped>
.fade-enter-active,.fade-leave-active{
transition:opacity 0.5s ease;
}
.fade-enter-from,.fade-leave-to{
opacity: 0;
}
</style>
Transition组件插槽只支持单个元素,当插槽内容时组件时,必须确保组件只有一个根元素,否则会报错 expects exactly one child element or component.
用于v-for列表元素、组件的插入、移除、改变添加动画效果
<template>
<button @click="addItem">任意位置添加一项</button>
<button @click="reset">重置</button>
<button @click="shuffleItem">打乱</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in list" :key="item">{{item}}
<button @click="remove(item)">删除</button>
</li>
</TransitionGroup>
</template>
<script>
import {ref,defineComponent} from 'vue'
import {shuffle} from 'lodash-es'
export default defineComponent({
setup(){
const getInitialItems=()=>[1,2,3,4,5]
const list = ref(getInitialItems())
let id = list.value.length + 1
const addItem=()=>{
const i = Math.round(Math.random()*list.value.length)
list.value.splice(i,0,id++)
}
const reset=()=>{
list.value=getInitialItems()
}
const shuffleItem=()=>{
list.value = shuffle(list.value)
}
const remove=(item)=>{
const index = list.value.indexOf(item)
index>-1 && list.value.splice(index,1)
}
return{
list,
shuffleItem,
addItem,
reset,
remove
}
}
})
</script>
<style scoped>
// 声明过渡效果
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
// 声明进入和离开的状态
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
// 确保离开的项被移出了布局流,以便正确计算移动时的动画效果
.list-leave-active{
position: absolute;
}
</style>
Transition组件实现原理
源码位置(vue3):packages/runtime-dom/src/components/Transition.ts
原理也很简单,Transition既然是vue组件,当然也会经历虚拟DOM转换为真实DOM。Transtion组件的特殊之处是为渲染子节点的VNode添加key属性,然后在子节点的VNode下添加transition属性,表示这是个transition组件渲染的VNode,然后在转换为真实DOM的时候特殊处理
// 导出Transition组件常量,它是一个函数组件类型
// 接收参数分别为props,slots
// 返回一个h函数
export const Transition: FunctionalComponent<TransitionProps> = (
props,
{ slots }
) => h(BaseTransition, resolveTransitionProps(props), slots)
// TransitionProps是一个接口类型
export interface TransitionProps extends BaseTransitionProps<Element> {
// 过渡效果命名,用于基于css的过渡效果
name?: string
// 类型,TRANSITION或者ANIMATION
type?: typeof TRANSITION | typeof ANIMATION
// 是否支持css过渡效果
css?: boolean
// 动画持续时长,接收数值类型,单位毫秒;也可以单独定义进入/离开动画的持续时长
duration?: number | { enter: number; leave: number }
// 自定义transition classes,相比vue2的6个增加了3个
enterFromClass?: string
enterActiveClass?: string
enterToClass?: string
appearFromClass?: string
appearActiveClass?: string
appearToClass?: string
leaveFromClass?: string
leaveActiveClass?: string
leaveToClass?: string
}
查看TransitionProps继承的BaseTransitionProps,这个接口主要定义了mode和一些JavaScript钩子
// packages/runtime-core/src/components/BaseTransition.ts
export interface BaseTransitionProps<HostElement = RendererElement> {
// 过渡模式,支持in-out/out-in/default
mode?: 'in-out' | 'out-in' | 'default'
// 是否首次渲染
appear?: boolean
// 是否通过自定义指令(v-show等)控制
// If true, indicates this is a transition that doesn't actually insert/remove
// the element, but toggles the show / hidden status instead.
// The transition hooks are injected, but will be skipped by the renderer.
// Instead, a custom directive can control the transition by calling the
// injected hooks (e.g. v-show).
persisted?: boolean
// 进入DOM的JavaScript钩子,可以通过@before-enter="xxx"形式调用
onBeforeEnter?: Hook<(el: HostElement) => void>
onEnter?: Hook<(el: HostElement, done: () => void) => void>
onAfterEnter?: Hook<(el: HostElement) => void>
onEnterCancelled?: Hook<(el: HostElement) => void>
// leave Javascript钩子
onBeforeLeave?: Hook<(el: HostElement) => void>
onLeave?: Hook<(el: HostElement, done: () => void) => void>
onAfterLeave?: Hook<(el: HostElement) => void>
onLeaveCancelled?: Hook<(el: HostElement) => void> // only fired in persisted mode
// appear Javascript钩子
onBeforeAppear?: Hook<(el: HostElement) => void>
onAppear?: Hook<(el: HostElement, done: () => void) => void>
onAfterAppear?: Hook<(el: HostElement) => void>
onAppearCancelled?: Hook<(el: HostElement) => void>
}
由此Transition组件的props已经清晰,再回到transition对应的源码位置,核心方法为resolveTransitionProps
// Transition返回了一个h函数,接收参数分别是Transition的props,需要执行的函数,插槽
export const Transition: FunctionalComponent<TransitionProps> = (
props,
{ slots }
) => h(BaseTransition, resolveTransitionProps(props), slots)
export function resolveTransitionProps(
rawProps: TransitionProps
): BaseTransitionProps<Element> {
const baseProps: BaseTransitionProps<Element> = {}
// 遍历Transition组件的每一项props,没有定义在DOMTransitionPropsValidators中的属性一律转换为any类型
for (const key in rawProps) {
if (!(key in DOMTransitionPropsValidators)) {
;(baseProps as any)[key] = (rawProps as any)[key]
}
}
// css属性为false,直接返回baseProps
if (rawProps.css === false) {
return baseProps
}
// 解构一些基本的属性,包括9个自定义class,name未赋值定义为v
const {
name = 'v',
type,
duration,
enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`,
enterToClass = `${name}-enter-to`,
appearFromClass = enterFromClass,
appearActiveClass = enterActiveClass,
appearToClass = enterToClass,
leaveFromClass = `${name}-leave-from`,
leaveActiveClass = `${name}-leave-active`,
leaveToClass = `${name}-leave-to`
} = rawProps
// 分别处理enter,appear,leave三种情况的class
const legacyClassEnabled =
__COMPAT__ &&
compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
let legacyEnterFromClass: string
let legacyAppearFromClass: string
let legacyLeaveFromClass: string
if (__COMPAT__ && legacyClassEnabled) {
const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
if (!rawProps.enterFromClass) {
legacyEnterFromClass = toLegacyClass(enterFromClass)
}
if (!rawProps.appearFromClass) {
legacyAppearFromClass = toLegacyClass(appearFromClass)
}
if (!rawProps.leaveFromClass) {
legacyLeaveFromClass = toLegacyClass(leaveFromClass)
}
}
// 标准化动画持续时长,拿到进入/离开DOM持续时长
const durations = normalizeDuration(duration)
const enterDuration = durations && durations[0]
const leaveDuration = durations && durations[1]
// 解构拿到JavaScript钩子方法
const {
onBeforeEnter,
onEnter,
onEnterCancelled,
onLeave,
onLeaveCancelled,
onBeforeAppear = onBeforeEnter,
onAppear = onEnter,
onAppearCancelled = onEnterCancelled
} = baseProps
// 处理进入动画的完成,移除对应的enter to class和enter active class
const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
done && done()
}
// 处理离开动画的完成,移除对应的leave from class,leave to class,leave active class
const finishLeave = (
el: Element & { _isLeaving?: boolean },
done?: () => void
) => {
el._isLeaving = false
removeTransitionClass(el, leaveFromClass)
removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass)
done && done()
}
const makeEnterHook = (isAppear: boolean) => {
return (el: Element, done: () => void) => {
const hook = isAppear ? onAppear : onEnter
const resolve = () => finishEnter(el, isAppear, done)
callHook(hook, [el, resolve])
// 封装了requestAnimationFrame方法
nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(
el,
isAppear ? legacyAppearFromClass : legacyEnterFromClass
)
}
addTransitionClass(el, isAppear ? appearToClass : enterToClass)
if (!hasExplicitCallback(hook)) {
whenTransitionEnds(el, type, enterDuration, resolve)
}
})
}
}
return extend(baseProps, {
onBeforeEnter(el) {
callHook(onBeforeEnter, [el])
addTransitionClass(el, enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyEnterFromClass)
}
addTransitionClass(el, enterActiveClass)
},
onBeforeAppear(el) {
callHook(onBeforeAppear, [el])
addTransitionClass(el, appearFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyAppearFromClass)
}
addTransitionClass(el, appearActiveClass)
},
onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true),
onLeave(el: Element & { _isLeaving?: boolean }, done) {
el._isLeaving = true
const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyLeaveFromClass)
}
// force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow()
addTransitionClass(el, leaveActiveClass)
nextFrame(() => {
if (!el._isLeaving) {
// cancelled
return
}
removeTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(el, legacyLeaveFromClass)
}
addTransitionClass(el, leaveToClass)
if (!hasExplicitCallback(onLeave)) {
whenTransitionEnds(el, type, leaveDuration, resolve)
}
})
callHook(onLeave, [el, resolve])
},
onEnterCancelled(el) {
finishEnter(el, false)
callHook(onEnterCancelled, [el])
},
onAppearCancelled(el) {
finishEnter(el, true)
callHook(onAppearCancelled, [el])
},
onLeaveCancelled(el) {
finishLeave(el)
callHook(onLeaveCancelled, [el])
}
} as BaseTransitionProps<Element>)
}
keepAlive原理
基本使用
keepAlive是一个内置组件,主要用于组件缓存,它包裹的组件在切换后不会被销毁,而是保留在内存中,避免重复渲染DOM。include/exclude用于包含/排除组件,max用于限制最大缓存实例个数,它使用的是LRU算法
LRU缓存(最大最少使用缓存):缓存的实例个数超过最大数量,最久没被访问的缓存实例将被销毁,以便为新实例腾出空间
缓存实例的生命周期
onActivated:组件被挂载时调用
onDeactivated:组件被卸载时调用
<script setup>
import {onActivated,onDeactivated} from "vue"
onActivated(()=>{
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(()=>{
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
keepAlive实现原理
// vue3版本源码位置:packages/runtime-core/src/components/KeepAlive.ts
// MatchPattern类,支持传入字符串、正则表达式或者字符串数组、正则数组
type MatchPattern = string | RegExp | (string | RegExp)[]
// 定义KeepAliveProps接口,接收三个参数
export interface KeepAliveProps {
// include缓存白名单实例,可选参数
include?: MatchPattern
// exclude缓存黑名单实例,可选参数
exclude?: MatchPattern
// 最大可缓存个数,可选参数,支持传入数字或字符串
max?: number | string
}
type CacheKey = string | number | symbol | ConcreteComponent
// 定义Cache类,用于缓存虚拟DOM,Map类型
type Cache = Map<CacheKey, VNode>
// 定义Keys类,用于缓存虚拟DOM对应的key,Set类型
type Keys = Set<CacheKey>
// KeepAliveContext接口
export interface KeepAliveContext extends ComponentRenderContext {
// 渲染器实例
renderer: RendererInternals
// 组件创建实例方法
activate: (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean,
optimized: boolean
) => void
// 组件销毁实例方法
deactivate: (vnode: VNode) => void
}
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
// 标志用于摇树优化
__isKeepAlive: true,
// 定义的props,前面已说过不再重复
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
// 在setup生命周期执行
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 获取当前正在渲染的实例
const instance = getCurrentInstance()!
// 取实例的上下文作为共享上下文(摇树优化用)
const sharedContext = instance.ctx as KeepAliveContext
// 如果内部渲染实例未注册,表明是服务端渲染,在keepAlive内部渲染子组件
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}
// cache用于缓存虚拟DOM,keys缓存虚拟DOM对应的key
const cache: Cache = new Map()
const keys: Keys = new Set()
// 当前渲染的虚拟DOM,初始化时为null
let current: VNode | null = null
//开发环境且开启了devtools,实例增加__v_cache属性使用缓存
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
;(instance as any).__v_cache = cache
}
// 获取实例的suspense属性
const parentSuspense = instance.suspense
// 从共享上下文中解构出patch,move,unmount,createElement方法
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
// 创建div元素
const storageContainer = createElement('div')
// 共享上下文的activate属性定义组件被创建的方法
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
// 从虚拟DOM的component属性获取实例
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 调用patch方法进行新旧VNode比较
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
invokeArrayFns(instance.a)
}
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// 更新组件树
devtoolsComponentAdded(instance)
}
}
// 共享上下文的deactivate属性定义组件被销毁的方法
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
if (instance.da) {
invokeArrayFns(instance.da)
}
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// 更新组件树
devtoolsComponentAdded(instance)
}
}
function unmount(vnode: VNode) {
// 重置shapeFlag用于unmounted
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense, true)
}
function pruneCache(filter?: (name: string) => boolean) {
// 遍历已缓存的虚拟DOM map
cache.forEach((vnode, key) => {
// 获取当前组件名
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
function pruneCacheEntry(key: CacheKey) {
// 从缓存的虚拟DOM map中取出key对应的VNode
const cached = cache.get(key) as VNode
// 当前没有被渲染的实例(旧的VNode需要被删除)或者
//缓存VNode的type不等于当前实例的type(新旧VNode不相等)
if (!current || cached.type !== current.type) {
// unmount卸载VNode
unmount(cached)
} else if (current) {
// 当前激活实例不再使用keep-alive,重置flag
resetShapeFlag(current)
}
// 从cache和keys中删除对应实例
cache.delete(key)
keys.delete(key)
}
// watch include/exclude属性变更
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
// include命中则增加
include && pruneCache(name => matches(include, name))
// exclude命中则删除
exclude && pruneCache(name => !matches(exclude, name))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)
// 缓存渲染后的子树
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
onBeforeUnmount(() => {
cache.forEach(cached => {
// 解构获取subTree,suspense
const { subTree, suspense } = instance
// 获取子树的child vnode
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type) {
// 重置标志
resetShapeFlag(vnode)
// 触发deactivated hook
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})
return () => {
pendingCacheKey = null
// 没有插槽则返回
if (!slots.default) {
return null
}
const children = slots.default()
// 获取keepAlive第一个子节点的VNode
const rawVNode = children[0]
// keepAlive内部只允许包裹一个组件
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
} else if (
// 包裹的组件不是VNode,也不是有状态的组件或者Suspense,直接返回当前子组件
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
return rawVNode
}
// 获取包裹组件的子节点VNode
let vnode = getInnerChild(rawVNode)
// 获取vnode的type属性,它是ConcreteComponent
const comp = vnode.type as ConcreteComponent
// 异步组件需要基于已加载的内部组件进行命名检查
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp
)
// 解构include,exclude,max
const { include, exclude, max } = props
// 定义了include属性且命名不匹配
// 或定义了exclude属性且命名匹配
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
// 当前节点为VNode,返回keepAlive第一层子组件
current = vnode
return rawVNode
}
// vnode没有key属性,key为comp,否则取vnode.key
const key = vnode.key == null ? comp : vnode.key
// 根据key获取缓存对应的虚拟DOM
const cachedVNode = cache.get(key)
if (vnode.el) {
// 克隆vnode节点
vnode = cloneVNode(vnode)
// 是Suspense组件,给原生VNode赋值ssContent属性,值为vnode
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
// 在beforeMount/beforeUpdate钩子中缓存到instance.subTree
pendingCacheKey = key
if (cachedVNode) {
// 已缓存的vNode存在就把el和component赋值到vnode
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
// 递归更新子树的transition hook
setTransitionHooks(vnode, vnode.transition!)
}
// 避免vnode作为新元素挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// keys set中先删后加,确保是最新的key
keys.delete(key)
keys.add(key)
} else {
// 新增key
keys.add(key)
// max属性存在且当前已缓存的keys容量大于max
if (max && keys.size > parseInt(max as string, 10)) {
// 使用LRU更新key
pruneCacheEntry(keys.values().next().value)
}
}
// 避免vnode被unmounted
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
// 是否为Suspense组件,是返回原生Vnode,否返回vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
}
}
export const isSuspense = (type: any): boolean => type.__isSuspense
keepAlive是在哪个生命周期被调用的
vue2
- 在created阶段,初始化cache、keys,cache用于缓存虚拟DOM,是一个map集合。keys用于缓存组件的key集合,是一个Set
- 在mounted阶段,监听include,exclude的变化,执行相应操作
- destroyed阶段,删除所有缓存相关实例
vue3
和vue2的过程类似,只不过生命周期钩子变更了
created->onCreated,
mounted->OnMount,
destroyed->onUnmount
Supsense原理与异步
基本使用
suspense是vue的一个内置组件,用于处理异步依赖,它有两个插槽, #default和 #fallback,每个插槽只允许一个直接的子节点
vue应用中组件加载时间过长就可以使用这个组件
<Suspense>
<!--1.初始渲染时,渲染#default插槽的内容,如果在这个过程遇到异步依赖,会进入挂起状态-->
<Container />
<!--2.在挂起状态时,展示的是#fallback插槽的内容-->
<!--3.当所有异步依赖完成后,suspense进入完成状态,展示#default插槽的内容,当异步加载失败时,会展示#fallback插槽的内容-->
<template #fallback>
loading...
</template>
</Suspense>
Suspense实现原理
//vue3源码位置:packages/runtime-core/src/components/Suspense.ts
Teleport是怎么实现选择性挂载的
Teleport是vue的一个内置组件,类似于React的Portal,它可以让组件渲染在父组件以外的DOM上,主要支持to和disabled两个参数
to 必选,Teleport目标挂载的DOM元素
disabled 可选,用于禁用Teleport的功能,插槽内容不会移动到任何位置
使用场景:弹窗
// index.vue
<template>
<div class="outer">
<h3>Tooltip with Vue3 Teleport</h3>
<button id="show-modal" @click="show">显示</button>
<Teleport to="body">
<modal :show="showModal" @close="hide">
<template #header>
<h3>custom modal</h3>
</template>
</modal>
</Teleport>
</div>
</template>
<script setup>
import Modal from './Modal.vue'
import {ref} from 'vue'
const showModal = ref(false)
const show = ()=>{
showModal.value = true
}
const hide=()=>{
showModal.value = false
}
</script>
// Modal.vue
<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>
<div class="modal-body">
<slot name="body">default body</slot>
</div>
<div class="modal-footer">
<slot name="footer">default footer</slot>
<button class="modal-default-button" @click="$emit('close')">x</button>
</div>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
import {defineProps} from 'vue'
const props=defineProps({
show:Boolean
})
</script>
<style scoped>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0 auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
注意,Teleport的to目标必须已经存在于DOM中,在挂载Teleport前,to目标的元素必须挂载完成,否则会报错
实现原理
主要是通过document的querySelector实现的,Teleport实质是一个组件,必然会经历组件的初始化、挂载与更新
- Teleport组件通过process初始化,查看是否禁用teleport,禁用则在初始化容器时渲染,处理HMR
- 通过resolveTarget拿到要渲染teleport的容器节点,目标不存在就报错返回,存在打注释定位标记
- 进入挂载阶段,确认内部是否是array children结构,是否配置了disabled参数,配置了就解析在默认容器中,没有配置就解析在to 指定的容器中,初始化结束
- 更新阶段,核心函数moveTeleport,能触发Teleport更新只有两种情况:
- 修改to或者disabled的值
- 组件内部的内容发生更新
根据这两种情况进行不同的处理,更新结束
moveTeleport 用于更新
修改to或者disabled的值分为以下情况:
- disabled从false为true,会将to指定的容器移动到默认的容器
- disabled从true为false,会将默认容器移动到to指定的容器
- to指定的容器改变,向重新传递一个选择器,重新解析,移动到新的选择器位置
function moveTeleport(
vnode: VNode,
container: RendererElement,
parentAnchor: RendererNode | null,
{ o: { insert }, m: move }: RendererInternals,
moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
) {
// 确认新的目标容器,目标容器变了,将Teleport插入新的目标容器
if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
insert(vnode.targetAnchor!, container, parentAnchor)
}
const { el, anchor, shapeFlag, children, props } = vnode
const isReorder = moveType === TeleportMoveTypes.REORDER
// 重新排序
if (isReorder) {
insert(el!, container, parentAnchor)
}
// 没有重排序或者disabled属性设置为true
if (!isReorder || isTeleportDisabled(props)) {
// 遍历vnode子节点,将子节点移动到
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
for (let i = 0; i < (children as VNode[]).length; i++) {
move(
(children as VNode[])[i],
container,
parentAnchor,
MoveType.REORDER
)
}
}
}
// 需要重新排序
if (isReorder) {
insert(anchor!, container, parentAnchor)
}
}
nextTick实现原理
以下代码点击按钮以后输出是什么
<template>
<div class="container">
<div ref="test">{{msg}}</div>
<button @click="handleClick">点击</button>
</div>
</template>
<script>
export default {
data(){
return{
msg:'Hello,lyllovelemon'
}
},
methods:{
handleClick(){
this.msg='Hello,lyl'
console.log(this.$refs.test.innerHTML)
}
}
}
</script>
输出结果是Hello,lyllovelemon,这是为什么呢
异步更新DOM策略
Vue的DOM更新是异步的,当一个响应式数据变化时,会在Watcher的setter函数中通知闭包中的Dep,Dep会调用它管理的所有Watcher对象,触发update方法
//vue2.7.10 src/core/observer/watcher.ts
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() // 同步执行run方法,直接渲染视图
} else {
queueWatcher(this) // 异步执行queueWatcher,加到观察者队列中,在下一个tick调用
}
}
我们再看看queueWatcher的具体实现
// src/core/observer/scheduler.ts
// 接收一个Watcher实例作为参数
export function queueWatcher(watcher: Watcher) {
const id = watcher.id // 获取watcher的id
if (has[id] != null) { // 观察者队列中存在就跳过
return
}
if (watcher === Dep.target && watcher.noRecurse) { // noRecurse为true
return
}
has[id] = true // 加入到has哈希表,用于下次校验
if (!flushing) {
queue.push(watcher) // 没有flush到加到队列
} else {
// flush过了, 根据id从watcher队列中删除
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true // waiting标志为true,表示不是立即更新视图,而是进入等待
if (__DEV__ && !config.async) {
flushSchedulerQueue() // 开发环境且同步
return
}
// 在nextTick中执行flushSchedulerQueue方法
nextTick(flushSchedulerQueue)
}
}
queueWatcher中可以看到视图更新不是立即进行,而是将watcher对象push到一个队列,此时处于waiting状态,watch对象会不停的加入到队列中,等到下一个tick时,这些对象才会被遍历取出,更新视图。下一个tick就是nextTick
Vue为什么不使用同步更新DOM
<template>
<div class="container">
<div>{{count}}</div>
<button @click="handleClick">点击</button>
</div>
</template>
<script>
export default {
data(){
return{
count:0
}
},
methods:{
handleClick(){
for(let i=0;i<1000;i++){
this.count++
}
}
}
}
</script>
如上图,点击按钮时,执行了1000次 count的++,当DOM更新是同步时,就会触发1000次DOM更新刷新视图,这是很耗性能的,Vue使用异步更新DOM策略,将count++的操作放到队列中,在下一个tick时统一执行。同一个id的Watcher不会重复加到queue中,所以最终更新视图直接将count从0加到1000,保证视图更新DOM是从当前栈执行完后的下一个tick调用,大大优化了性能
nextTick原理
vue2.5以前的版本:nextTick实质是产生一个回调函数加入到task(宏任务)或者microtask(微任务),当前栈执行完后调用该回调函数,起到了异步触发的作用
vue2.5以后全部使用微任务实现,原因是使用宏任务会产生一些问题
// src/core/util/next-tick.ts
export let isUsingMicroTask = false
const callbacks: Array<Function> = []
let pending = false
function flushCallbacks() { //清空回调函数
pending = false // pending为false,表示等待结束,准备执行
const copies = callbacks.slice(0) // 浅拷贝回调函数
callbacks.length = 0 // 将回调函数清空
for (let i = 0; i < copies.length; i++) { // 拷贝后的数组遍历执行回调
copies[i]()
}
}
let timerFunc // 延迟执行函数
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 支持promise,用promise实现
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// 不支持promise但是支持MutationObserver,用MutationObserver实现
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 以上不支持但支持setImmediate,用setImmediate实现
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 以上都不支持,最后用setTimeout实现
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// cb存到callbacks中
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true // pending是一个状态标志,保证timerFunc在下一个tick之前只执行一次
timerFunc()//timerFunc
}
// $flow-disable-line
// 支持promise就用promise实现
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc函数做了什么:先后按照promise、MutationObserver、setImmediate、setTimeout实现,promise、MutationObserver都是微任务的实现
为什么优先使用微任务
由于浏览器的事件循环机制,引擎在每一个宏任务执行完毕,从队列中取下一个宏任务执行之前,会将这个宏任务下的微任务队列拿出来依次执行,因此微任务的执行时间早于宏任务。每个task执行完后都会触发UI的重新渲染,在microTask中完成数据更新,当前task结束就能拿到最新的UI了,如果再新建一个task,UI渲染就会进行两次
为什么优先使用Promise而不是MutationObserver
MutationObserver虽然浏览器兼容性更好,但是在iOS7,Android 4.4的touch事件上会有问题
setImmediate为什么比setTimeout好
setImmediate可以保证调用后立即执行,setTimeout需要和系统时间保持一致,最快也要4ms以后才能执行。但是setTimeout的浏览器兼容性更好,setImmediate只支持IE浏览器
watch函数实现原理
watch原理
对watch的每一个属性创建watcher,watcher在初始化时会将监听的目标值缓存到watcher.value中,触发data[key]的get方法,被对应的dep进行依赖收集,当data[key]发生改变时触发set方法,执行dep.notify方法,
通知所有收集的依赖,触发收集的watcher的watch,执行watch.cb,也就是watch中的监听函数
computed原理
computed是响应式的, 给computed设置get和set会和Object.defineProperty关联起来(vue2),vue3会和proxy关联
computed是有缓存的,主要通过dirty控制
dirty是一个脏数据标志位,为false时表示读取computed使用缓存,为true时表示读取computed会执行get函数重新计算
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
) {
let getter: ComputedGetter<T> // 收集getter
let setter: ComputedSetter<T> // 收集setter
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
warn('Write operation failed: computed value is readonly')
}
: noop
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 是服务端渲染就赋值null,客户端渲染实例化watcher
const watcher = isServerRendering()
? null
: new Watcher(currentInstance, getter, noop, { lazy: true })
if (__DEV__ && watcher && debugOptions) { // 用于debug
watcher.onTrack = debugOptions.onTrack
watcher.onTrigger = debugOptions.onTrigger
}
const ref = {
// some libs rely on the presence effect for checking computed refs
// from normal refs, but the implementation doesn't matter
effect: watcher,
get value() {
if (watcher) {
if (watcher.dirty) { // dirty标志位,表示数据需要更新
watcher.evaluate() // 执行watcher的evaluate方法
}
if (Dep.target) {
if (__DEV__ && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
}
watcher.depend()
}
return watcher.value
} else {
return getter()
}
},
set value(newVal) {
setter(newVal)
}
} as any
def(ref, RefFlag, true)
def(ref, ReactiveFlags.IS_READONLY, onlyGetter)
return ref
}
性能优化
性能优化主要在以下几个方面:页面加载速度优化,打包体积优化,打包速度优化,浏览器安全
页面加载速度优化
- 使用正确的架构
SPA/SSR/SSG
- v-for循环中正确的使用key
key需要在循环列表中保持唯一,不要使用数组下标作为key,key用于高效的更新diff
- 保持props稳定
如下图代码,它使用了id和activeId两个prop来保证它是否是当前活跃的一项,代码存在什么问题呢?
<ListItem
v-for="item in list"
:id="item.id"
:active-id="activeId" />
当activeId更新时,ListItem的每一项都会进行更新,造成了不必要的重复渲染,我们需要改成只有活跃状态发生改变的项才需要更新,将上面的代码改写。对于大多数的组件来说,activeId 改变时,它们的 active prop 都会保持不变,因此它们无需再更新。总结一下,就是让传给子组件的 props 尽量保持稳定。
<ListItem
v-for="item in list"
:id="item.id"
:active="item.id === activeId" />
4. 正确使用v-once和v-memo 5. 使用shallowRef和shallowReactive绕开深度响应
Vue3的响应式默认是深度的,优点在于便于进行数据的状态管理,缺点是数据量大时,性能负担加重,因为每个属性访问都会进行依赖的深度追踪。
解决办法:使用shallowRef和shallowReactive绕开深度响应
const shallowArray = shallowRef([
/* 巨大的列表,里面包含深层的对象 */
])
// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]
// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [ { ...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]
6. 长列表渲染:虚拟列表
长列表渲染的最优方案是虚拟列表,其次还有time slice和css方案。渲染的列表项成千上万时,对应的DOM元素也会递增,但是我们并不需要一次性将所有数据渲染出来,虚拟列表的原理就是创建一个可视化区域,让可视化区域的数据渲染出来
- 避免内存泄漏
事件绑定后需要取消绑定,定时器也需要定时销毁
created(){
window.addEventListener('click',this.onClick,false)
setTimeout(timer,2000)
}
beforeUnmount(){
window.removeEventListener('click',this.onClick,false)
clearTimeout(timer,2000)
}
8. 预加载与懒加载
图片,路由都可以进行懒加载
- v-if和v-show使用
- computed和watch使用
- 使用keep-alive缓存DOM
- 节流/防抖
- 浏览器缓存与本地缓存
- CSS优化
GPU加速
减少回流和重绘
打包速度优化
- 开启多进程打包
- 多线程打包
- CDN
打包体积优化
以打包工具webpack举例
- 开启gzip压缩
- 压缩HTML
- 压缩CSS
- 压缩JavaScript
- 图片压缩
- 尽量选择ES模式的依赖
选择lodash-es优于lodash,它对于tree-shaking更友好
- 使用Html-webpack-bundle-analyaser分析包体积
这个插件可用于分析包体积,查看最大的包,按需进行体积优化
- 代码分割
使用打包工具将整个包拆分为多个较小的包,可以按需加载或者并行加载,通过适当的代码分割,页面加载时需要的功能可以立即下载,而额外的块只在需要时才加载,从而提高性能。
// defineAsyncComponent可用于按需加载组件
import {defineAsyncComponent} from "vue"
// Container是一个异步组件,按需加载
const Container=defineAsyncComponent(()=>import("./container.vue"))
7. 开启tree-shaking 8. 第三方库按需引入
以element为例,项目中并没有用到所有的组件时,可以进行按需引入,可以有效减少第三方包体积
- 使用合适的sourceMap
开发环境最优sourceMap:cheap-module-eval-source-map
生产环境推荐souceMap:source-map
浏览器安全
- 避免CSRF
- 避免XSS
- 安全沙箱
写在最后
源码分析花的时间很长,白天要工作(目前工作比较饱和),只能晚上写,前前后后花了快1个月时间,如果这篇文章对你有帮助的话,不妨给博主姐姐点赞收藏评论