本文目录
[[toc]]
vue 生命周期
参考 源码
| 生命周期 | 描述 |
|---|---|
beforeCreate | 组件实例被创建之初, data 、 props 等都还未被初始化,无法访问 |
created | state ( props / methods / data / computed / watch ) 初始化完成,但是还未创建 vm.$el |
beforeMount | VNode 挂载到 DOM 之前触发。 此时已经能获取 vm.$el。此时 DOM 上的内容还是老的未被替换的内容,也可能 DOM 还未挂载到 DOM 上 |
mounted | VNode 挂载到 DOM 上后触发。 |
beforeUpdate | 监听组件 state ( props / methods / data / computed / watch ) 变化时触发,在组件更新之前,此时更新 state 不会触发 beforeUpdate |
updated | 组件 VNode 更新后触发,此时更新 state ( props / methods / data / computed / watch ) 将会重新触发 beforeUpdate 、 updated 等流程 |
beforeDestroy | 组件实例销毁前触发,此时仍然可以访问 state ( props / methods / data / computed / watch ) |
destroyed | 组件实例销毁后触发,此时已经无法访问到 state ( props / methods / data / computed / watch ) |
activated | keep-alive 缓存组件从缓存挂载入 vDOM 后触发,先于 updated |
deactivated | keep-alive 缓存组件从 vDOM 中被移除时触发 |
errorCaptured | 在后代组件中出现错误时调用 |
setup | composition API 中新增。@vue/composition-api 包中对 data 进行拦截,在 data 初始化之前调用此函数。在 2.7 版本后,会在 props 初始化完成、 methods 初始化之前触发。所以, setup 中不能访问到 data 定义的响应式变量 |
beforeUnmount | beforeDestroy 别名, composition API 中正式更名为 beforeUnmount |
unmounted | destroyed 别名 composition API 中正式更名为 unmounted |
初始化
- 在调用
beforeCreate之前,数据初始化并未完成,像data、props这些属性无法访问到 - 到了
created的时候,数据已经初始化完成,能够访问data、props这些属性,但这时候并未完成DOM的挂载,因此无法访问到DOM元素 state初始化顺序:props>methods>data>computed>watch
---
title: Vue2 初始化流程
---
graph TD
A[通过自增生成 uid]
B[init scope]
C[init lifecycle]
D[init events]
E[init render]
F[trigger beforeCreate]
G[init injections]
H[init state]
HA[init state: init props]
HB[init state: init methods]
HC[init state: init data]
HD[init state: init computed]
HE[init state: init watch]
I[init provide]
J[trigger created]
A --> B
B --> C
C --> D
subgraph beforeCreate 调用之前
D --> E
end
E --> F
F --> G
subgraph created 调用之前
G --> H
H --> HA
HA --> HB
HB --> HC
HC --> HD
HD --> HE
HE --> I
end
I --> J挂载
通过 vm.$mount + 公共 mount 将触发挂载流程,具体流程如下:
---
title: Vue2 挂载组件流程
---
graph TD
A[获取挂载点 DOM]
B[统一渲染参数为 render<br>优先级上: render > template > outerHTML<br>tempate 会解析为 AST 再生成 render]
C[mount 参数校验]
D[trigger beforeMount]
E[初始化 updateComponent 函数]
F[使用 watcher 监听 vDOM ,在 vDOM 变更后更新 DOM]
G[执行 immediate watch]
H[挂载完成]
I[trigger mounted]
subgraph beforeMount 触发之前
subgraph "vm.$mount 函数"
A --> B
end
B --> C
end
C --> D
D --> E
subgraph mounted 触发之前
E --> F
F --> G
G --> H
end
H --> I不能 mount 到 body / html 的原因
vue源码 中禁止此行为- vue2 中的
mount实现会对挂载点进行替换,如果直接替换body节点,bodychildren 中还有待执行的 JS script ,容易引发不可预测问题 vDOMdiff 算法基于空白DOM+ 空白vDOM对应,在无法替换body的情况下,DOM与vDOM不匹配,可能导致其他问题- 影响其他第三方库工作,可能将其自身的全局功能替换掉,如组件库中的全局弹窗等
scoped样式是基于 HTML 元素的hash属性,如果挂载到body上,scoped可能会失效- 丢失
body后,可能会导致 SEO 问题或者 a11y 问题
更新
在挂载流程中,创建了 watcher 监听 vDOM 变化,用以更新 DOM 。
当 watcher 监听到 vDOM 变更时,就会触发 beforeUpdate 了,完整的 更新流程 如下:
---
title: Vue2 Update 组件流程
---
graph TD
A[watcher 监听到 VNode 变更]
B[trigger beforeUpdate]
C[patch vnode]
D[刷新任务调度队列]
E[调度队列中触发组件的 activated 与 update hook]
subgraph beforeUpdate 触发之前
A
end
A --> B
subgraph update 触发之前
B --> C
C --> D
end
D --> E总结
new Vue的时候调用会调用_init方法- 定义
$set、$get、$delete、$watch等方法 - 定义
$on、$off、$emit、$off等事件 - 定义
_update、$forceUpdate、$destroy生命周期
- 定义
调用
$mount进行页面的挂载挂载的时候主要是通过
mountComponent方法定义
updateComponent更新函数执行
render生成虚拟DOM_update将虚拟DOM生成真实DOM结构,并且渲染到页面中
v-if 与 v-for 优先级
参考 源码 可以看到, v-for 先于 v-if 。
所以在 v-if 与 v-for 应用于同一元素的时候,会先生成 N 个元素,再对每个元素应用 v-if 判断。
data 函数与对象
- 根实例对象
data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况 - 组件实例对象
data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
为什么 data 不能是对象
参考 vue 源码
在初始化 data 时,如果 data 是对象则会直接使用,如果是函数则会调用函数。
本质问题不是 data 的形式,如果在 data 中返回了共享的对象,那么也会导致所有该组件的实例共享此 data
vue 在 合并 option 的时候 ,也会进行校验
为什么 data 需要是函数
定义组件从本质上来说,就是一个构造函数。
渲染时,会将定义组件实例化为对象,而 data 会存在于共享实例中,所有组件实例共享。
为了避免污染,所以需要隔离作用域,也就是每次组件 create 的时候重新拷贝一份,而不是使用共享的 data 。
双向绑定
原理
通过 Object.defineProperty 递归遍历对象的每个属性,为每个属性创建 Dep (依赖收集器),并在 getter 中收集依赖,在 setter 中触发更新
整个依赖收集采用 观察者模式 + 发布/订阅者模式 实现
---
title: 双向绑定架构设计图
---
flowchart TD
A[原始对象] --> B[Observer]
B -->|递归遍历| C[defineReactive]
C --> D[创建Dep实例]
D -->|getter| E[收集Watcher依赖]
D -->|setter| F["触发Dep.notify()"]
F --> G[Watcher更新视图]Observe类: 递归遍历对象所有属性,调用Object.defineProperty定义新的getter/setter,每个响应对象仅有一个Observe实例,挂载到vm.__ob__上。Dep类(依赖收集器): 每个属性对应一个Dep,通过闭包保存,如果是数组会放到vm.__ob__.dep上。depend(): 收集当前活跃的Watcher。notify(): 通知所有订阅的Watcher执行。
Watcher类- 渲染
Watcher: 监听state变化,触发组件update computed: 缓存Watcher结果- 自定义
Watch: 用户自定义的state监听行为
- 渲染
数组
数组监听采用添加中间原型的方式,进行拦截,以便监听到数据变更,触发响应回调,示例代码如下:
// 获取数组的原型
const arrayProto = Array.prototype
// 基于数组的原型创建一个新的对象
const middleProto = Object.create(arrayProto)
// 一个要被重写的方法列表
const overrideMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
for (const method of overrideMethods) {
// 缓存原始方法
const original = arrayProto[method]
// 定义重写的方法
Object.defineProperty(middleProto, method, {
value: function mutator(...args) {
// 调用原始方法
const result = original.apply(this, args)
// 获取数组对象的 ob 对象,它是一个 Observer 实例
const ob = this.__ob__
// 通知变更
ob.dep.notify()
return result
},
enumerable: false,
writable: true,
configurable: true,
})
}
// 假设 arr 是 Vue 管理的数组
// 将 arr 的原型指向重写了方法的对象
Reflect.setPrototypeOf(arr, middleProto)局限性
- 静态监听: 只能监听初始定义好的属性,对于新增属性难以监听,需要通过
$set赋予响应性,或者每次赋值后手动触发$forceUpdate重新渲染。 - 数组实现不完整
- 无法监听数组成员的变更,比如通过
index访问成员并赋值、通过给length属性赋值导致的数组成员变更等。 - 数组函数覆盖不完全,只能覆盖有限方法,扩展性差。
- 无法监听数组成员的变更,比如通过
- 性能问题: 性能开销大,尤其是嵌套对象时,在初始化
dep场景下会有大量内存开销。
diff 算法
整体策略为:深度优先,同层比较,双端比较
同层比较
---
title: Vue2 双向 diff 算法流程
---
graph TD
oldA[A]
oldB[B]
oldC[C]
oldB1[B1]
oldB2[B2]
oldC1[C1]
oldC2[C2]
newA[A]
newB[B]
newC[C]
newB1[B]
newB2[B]
newC1[C1]
newC2[C2]
subgraph old[旧的 VDOM]
oldA --> oldB
oldA --> oldC
oldB --> oldB1
oldB --> oldB2
oldC --> oldC1
oldC --> oldC2
end
subgraph new[新的 VDOM]
newA --> newB
newA --> newC
subgraph "全部替换,不会复用"
newB --> newB1
newB --> newB2
end
newC --> newC1
newC --> newC2
end
old --> newvnode 树 patch
当数据发生改变时, setter 会调用 Dep.notify 通知所有订阅者 Watcher ,订阅者就会调用 patch 更新 DOM 节点
patch 主要有以下四个判断:
- 没有新节点: 直接触发旧
VNode的destroy - 没有旧节点: 通过
createElm创建新的VNode - 新旧节点是同一节点: 通过
patchVnode更新节点 - 新旧节点不是同一节点: 删除旧节点,创建新节点
函数实现见 源码 ,下面为部分核心代码:
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 1. 没有新结点
if (isUndef(vnode)) {
if (isDef(oldVnode)) {
invokeDestroyHook(oldVnode)
}
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
if (isUndef(oldVnode)) {
// 2. 没有旧节点,直接创建新节点
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 3. 新旧节点是同一组件
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 4. 新旧节点不是统一组件
if (isRealElement) {
// 如果是 DOM 节点,判断是不是 SSR 渲染的结果
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// 如果是 SSR 水合后的节点,通过水合去更新,直接结束 patch
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
// 如果水合失败 / 不是 SSR ,将旧节点修改为空白节点去更新
oldVnode = emptyNodeAt(oldVnode)
}
// 替换旧节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建新结点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 递归更新父节点
if (isDef(vnode.parent)) {
}
// 删除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}相同节点的判断条件可以看 源码 ,主要包括以下几个条件:
key相同- 都是异步组件或者都不是
- 组件
tag相同 data都定义或者都没定义- 如果是
input的话需要比较input的type是否相同
同一节点的属性、子节点 patch
patchVnode 主要工作流程 如下:
- 新节点是否为文本节点,如果是,直接更新 DOM 的文本内容即可。
- 新旧节点如果有子节点,先
patch子节点,如果子节点不一致,通过updateChildren更新子节点 - 只有新节点有子节点,直接创建新的子节点追加到 vnode 中即可。
- 只有旧节点有子节点,直接删除旧节点的子节点即可。
以下为省略了兼容性代码后的代码摘抄:
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}
// 让 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.elm 和 oldVnode.child 都复制到 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)) {
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)
}
}
// 如果 vnode 不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用 updateChildren
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
// 如果只有新的 vnode 有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// elm 已经引用了老的 dom 节点,在老的 dom 节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新 vnode 没有子节点,而 vnode 有子节点,直接删除老的 oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新 vnode 和老 vnode 是文本节点或注释节点
// 但是 vnode.text != oldVnode.text 时,只需要更新 vnode.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)
}
}
}子节点更新(双端比较)
主要原理
查看 源码 可以发现,主要维护的是这四对关系:
oldHead与newHeadoldTail与newTailoldHead与newTailoldTail与newHead
具体来说,就是处理以下场景:
oldHead===newHead,直接通过patchVnode更新,newHead、oldHead指针后移oldTail===newTail,直接通过patchVnode更新,newTail、oldTail指针前移oldHead===newTail,直接通过patchVnode更新,再移动节点到newHead位置,然后后移newHead、 前移oldTailoldTail===newHead,直接通过patchVnode更新,再移动节点到newTail位置,然后后移oldHead、 前移newTail- 都不匹配的情况下,也会尽力复用旧节点,具体处理流程如下:
- 遍历旧节点,按照 key 查找是否存在于
newHeadkey 相同的节点,如果存在则通过patchVnode更新,再移动到newHead位置,并且newHead后移 - 旧节点中未能查找到相同 key 的节点,直接创建新节点,并且
newHead后移
- 遍历旧节点,按照 key 查找是否存在于
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引,也就是 oldHead
let newStartIdx = 0 // 新头索引,也就是 newHead
let oldEndIdx = oldCh.length - 1 // 旧尾索引,也就是 oldTail
let newEndIdx = newCh.length - 1 // 新尾索引,也就是 newTail
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. '
+ 'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的key的新节点是否是同一个节点
// 不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
}总结
- 当数据发生改变时,渲染
watcher就会调用patch给真实的DOM打补丁 - 通过
isSameVnode进行判断,相同则调用patchVnode方法 patchVnode做了以下操作:- 找到对应的真实
dom,称为el - 如果都有都有文本节点且不相等,将
el文本节点设置为Vnode的文本节点 - 如果
oldVnode有子节点而VNode没有,则删除el子节点 - 如果
oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el - 如果两者都有子节点,则执行
updateChildren函数比较子节点
- 找到对应的真实
updateChildren主要做了以下操作:- 设置新旧
VNode的头尾指针 - 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用
patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找key一致的VNode节点再分情况操作
- 设置新旧
ref
原理
本质是通过 Vue.observable 实现的。
Vue.observable,让一个对象变成响应式数据。 Vue 内部会用它来处理 data 函数返回的对象
const obj = { foo: 123 }
const objRef = Vue.observable(obj)
console.log(obj === objRef) // true
// `Vue.observable` 会通过 `Object.defineProperty` 设置每个属性的 `getter` / `setter` ,并不会直接返回一个新对象。源码分析
参考 完整源码 ,精简版如下:
export function observe(
value: any,
shallow?: boolean,
ssrMockReactivity?: boolean
): Observer | void {
// 如果已经是响应式变量就直接返回
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__
}
if (
// 全局开关,判断是否要进行响应操作
shouldObserve
// 使用 SSR 模拟响应,并且未渲染完成
&& (ssrMockReactivity || !isServerRendering())
// 非原始类型
&& (isArray(value) || isPlainObject(value))
// 变量可以扩展
&& Object.isExtensible(value)
// 变量自身未被标记不能响应
&& !value.__v_skip /* ReactiveFlags.SKIP */ &&
// 没有 composition API 的响应标记
!isRef(value)
// 不是 VNode
&& !(value instanceof VNode)
) {
// 返回新的响应
return new Observer(value, shallow, ssrMockReactivity)
}
}export class Observer {
/** 收集依赖 */
dep: Dep
/** 有多少 Vue 实例,将该对象作为 root $data */
vmCount: number
constructor(public value: any, public shallow = false, public mock = false) {
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (isArray(value)) {
if (!mock) {
if (hasProto) {
// 多加一层原型链,修改数组变异方法,基本都是走这里
/* eslint-disable no-proto */
;(value as any).__proto__ = arrayMethods
/* eslint-enable no-proto */
} else {
// 不支持 `__proto__` 的场景下,手动定义变异方法
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
// 如果需要深度 ref ,就递归继续处理
if (!shallow) {
this.observeArray(value)
}
} else {
// 处理对象只需要枚举 key ,挨个定义即可
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
/**
* Observe a list of Array items.
*/
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
}keep-live
生命周期
vue 内部的缓存组件,有一个哈希表缓存已经创建出来的组件 VNode ,被缓存的组件生命周期如下:
- 首次进入
beforeRouteEnterbeforeCreatecreatedbeforeMountmountedactivatedbeforeRouteLeavedeactivated
- 再次进入
beforeRouteEnteractivatedbeforeRouteLeavedeactive
SSR 期间不会触发
activated、deactivated
实现原理
见 源码
缓存结构为
interface CacheEntry {
/** 组件 name */
name?: string
/** 组件 tag */
tag?: string
/** 组件实例 */
componentInstance?: Component
}
// key 不是组件 name ,而是根据规则生成的唯一 ID
// 解决同时缓存多个同一组件不同实例的互相冲突的问题
type CacheEntryMap = Record<string, CacheEntry | null>缓存失效、过期的时候,会调用 poruneCacheEntry 清理缓存:
- 销毁组件
- 从
cache中移除缓存数据 - 从已缓存的
keys中移除组件对应的唯一 ID
keep-alive 工作流程如下:
function render() {
// 获取默认插槽的内容
const slot = this.$slots.default
// 从插槽中取出需要展示的组件
const vnode = getFirstComponentChild(slot)
// 获取组件配置
const componentOptions = vnode && vnode.componentOptions
// 找不到配置直接返回,找到配置开始检查是否有缓存
if (componentOptions) {
// 获取组件名,匹配组件名是否满足 include 、 exclude 规则
const name = _getComponentName(componentOptions)
const { include, exclude } = this
if (
// 不在 included 中
(include && (!name || !matches(include, name)))
// 在 excluded 中
|| (exclude && name && matches(exclude, name))
) {
// 不满足使用缓存条件,直接返回
return vnode
}
const { cache, keys } = this
// 缓存的 key 不是组件名,否则会重复,需要生成唯一 ID
const key
= vnode.key == null
? // 根据 cid + tag 作为缓存 key ,基本上可以确定唯一,冲突可能性较小
componentOptions.Ctor.cid
+ (componentOptions.tag ? `::${componentOptions.tag}` : '')
// 有 key 就直接使用 key ,在约定中 key 本身就是作用域内唯一
: vnode.key
// 查找缓存是否存在
if (cache[key]) {
// 存在缓存,复用组件实例
vnode.componentInstance = cache[key].componentInstance
// LRU ,将其更新到队尾
remove(keys, key)
keys.push(key)
} else {
// 标记为需要缓存的组件, keep-alive 触发 updated 的时候会缓存该组件
// 在 updated 中处理的原因是: 子组件更新会触发父组件 updated 。
this.vnodeToCache = vnode
this.keyToCache = key
}
// @ts-expect-error can vnode.data can be undefined
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}移除已缓存的组件
主流方案
通过动态修改 exclude ,利用 keep-alive 对 include 、 exclude 等属性的监听,动态修改绑定的组件名,在 nextTick 后重新更新 exclude 即可。
优点:
- 不需要侵入
keep-alive内部,后续升级较为简单。
缺点:
- 数据传递逻辑较为复杂,需要从触发组件一直传递到
keep-alive组件的父组件。 - 如果本身存在动态
exclude场景,逻辑上可能存在冲突( 手动删除后又被追加进来 )。 exclude改动后需要全量更新缓存表,如果缓存的内容较多,可能会有一定的性能风险。
侵入组件内部方案
遍历 VDOM ,查找 keep-alive 组件 VNode ,通过源码中对应的 key 生成策略生成组件 key ,再将 cache[key] 删除。
优点
- 老版本兼容( 2.1.0 支持
include/exclude)。 - 不需要改动
exclude。 - 整体性能开销低,只需要往上遍历
VNode,不需要重新更新所有缓存数据。
缺点
- 侵入组件内部,可能导致意外错误。
- 升级
Vue的时候有遗留风险,可能导致升级后功能异常。
自定义指令
基本形式
以 v-name:arg.modify1.modify2="1 + 1" 为例:
name: 指令名,作为name参数传递给指令arg: 指令参数,作为arg参数传递给指令modify1/modify2: 指令修饰符,整合为{ modify1: true, modify2: true }作为modifiers参数传递给指令1 + 1: 指令表达式,作为expression参数传递给指令2: 指令表达式的执行结果,作为value参数传递给指令
定义
Vue.directive('demo', {
bind(el, binding, vnode) {
const s = JSON.stringify
el.innerHTML
= `name: ${s(binding.name)}<br>`
+ `value: ${s(binding.value)}<br>`
+ `expression: ${s(binding.expression)}<br>`
+ `argument: ${s(binding.arg)}<br>`
+ `modifiers: ${s(binding.modifiers)}<br>`
+ `vnode keys: ${Object.keys(vnode).join(', ')}`
}
})
// 组件内使用的指令
Vue.extends({
directives: {
focus(el) {
// 直接使用 function 而不是对象也是可以的
// 默认会为 `bind` 、 `update` 应用此 function
el.focus()
}
}
})支持的钩子函数及触发时机
| 钩子函数名称 | 触发时机 |
|---|---|
bind | 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 |
inserted | 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 |
update | 所在组件的 VNode 更新时调用, 但是可能发生在其子 VNode 更新之前 。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 |
componentUpdated | 指令所在组件的 VNode 及其子 VNode 全部更新后 调用。 |
unbind | 只调用一次,指令与元素解绑时调用。 |
所有钩子支持的参数如下:
el: 指令绑定的元素,用于 DOM 操作binding: 指令使用时的对象操作name: 指令名value: 指令绑定的值oldValue: 指令绑定的前一个值,仅在update、componentUpdated中可用,不区分值是否修改都会传递expression: 指令表达式。arg: 指令参数modifiers: 指令包含哪些修饰符
vnode: Vue 生成的虚拟节点oldVnode: 上一个虚拟节点,仅在update、componentUpdated中可用
虚拟 DOM
解决的问题
- 虚拟 DOM 诞生源自于 Diff 需求,在进行节点 Diff 的时候,原生 DOM 操作效率较低,每次创建、删除都需要大量资源开销,远不如只记录了关键信息的虚拟 DOM 高效。
- 虚拟 DOM 本身就是一套抽象概念,在非 DOM 场景也可以正常渲染,比如跨端场景,只需要根据
schema映射出对应的视图结构即可。 - 编程范式从命令式转为声明式,开发者只需要描述状态对应的 UI 即可,无需手动操作 DOM 。并且由于开发者能力参差不齐,框架底层封装的 DOM 操作,往往比大多数手写的局部刷新代码还要高效。
- 每个组件独立维护 VDOM ,通过
props、events等方式通信,使得组件得以实现高内聚、低耦合的结构,提高可维护性。
无虚拟 DOM 路线
无虚拟 DOM 重新流行的原因:
- 绕过 Diff 算法的性能瓶颈: 虚拟 DOM 的 Diff 算法复杂度较高(如传统
O(n³)时间复杂度),在大规模数据更新时可能成为性能瓶颈。而无虚拟 DOM 框架通过细粒度响应式系统(如Signals),直接定位数据变化影响的 DOM 节点,无需全量比对 - 编译时优化替代运行时开销: 以
Solid.js和Svelte为代表的框架,在构建阶段将组件模板编译为高效的原生 DOM 操作代码,消除虚拟 DOM 的运行时计算和内存占用。例如,Solid.js编译后仅生成与数据绑定的精确更新逻辑。 - 原生 DOM 操作的高效性: 直接调用
element.textContent等浏览器原生 API ,减少中间抽象层(虚拟 DOM )带来的性能损耗。实测显示,无虚拟 DOM 框架在低端设备上的渲染速度可提升 20 - 30% 。 - 内存占用与启动性能优化: 虚拟 DOM 需维护完整树结构的副本,而
Solid.js等框架通过细粒度响应式仅跟踪必要数据,显著降低内存占用和首次渲染耗时。
| 指标 | 虚拟 DOM | 无虚拟 DOM |
|---|---|---|
| 首次加载体积 | 较大(含有运行时库) | 较小(仅包含编译后的代码) |
| 内存占用 | 高(额外存储虚拟 DOM ) | 低 |
| 渲染速度 | 依赖 Diff 算法效率 | 直接更新对应节点,速度提升 20% - 30% |
| CPU 使用率 | 较大(计算 Diff ) | 较低(跳过 Diff 环节) |
组件 API 化
// 导入组件
import Modal from './Modal.vue'
// extends 创建一个新的组件,隔离影响
const ComponentClass = Vue.extend(Modal);
// 实例化组件,并挂载到空 div
const instance = new ComponentClass({ el: document.createElement("div") });
// 将新 div 挂载到 body 上
document.body.appendChild(instance.$el);