在Vue中,双向绑定主要是指响应式数据改变后对应的DOM发生变化,用<input v-model>这种DOM改变、影响响应式数据的方式也属于双向绑定,其本质都是响应式数据改变所发生的一系列变化,其中包括响应式方法触发、新的VNode生成、新旧VNode的diff过程,对应需要改变DOM节点的生成和渲染。整体流程如图所示。
双向绑定流程图
看以下Demo代码,让其触发一次响应式数据变化,代码如下:
<body>
<div id="app">
<div>
{{name}}
</div>
<p>123</p>
</div>
</body>
</html>
<script src="vue.global.js"></script>
<script type="text/javascript">
var app = Vue.createApp({
data() {
return {
name: 'jack'
}
},
mounted(){
setTimeout(()=>{
// 改变响应式数据
this.name = 'tom'
},1000*2)
}
}).mount("#app")
</script>
当修改this.name时,页面上对应的name值会对应地发生变化,整个过程到最后的DOM变化在源码层面的执行过程如图所示(顺序从下往上)。
双向绑定源码执行过程
上述流程包括响应式方法触发、新的VNode生成、新旧VNode的对比diff过程,对应需要改变DOM节点的生成和渲染。当执行最终的setElementText方法时,页面的DOM就被修改了,代码如下(packages\runtime-dom\src\nodeOps.ts):
setElementText: (el, text) => {
el.textContent = text
},
可以看到,这一系列复杂的过程最终都会落到最简单的修改DOM上。接下来对这些流程进行一一讲解。
1. 响应式触发
根据响应式原理,在创建响应式数据时,会对监听进行收集,在源码reactivity/src/effect.ts的track方法中,其核心代码如下:
/**
* Tracks access to a reactive property.
*
* This will check which effect is running at the moment and record it as dep
* which records all effects that depend on the reactive property.
*
* @param target - Object holding the reactive property.
* @param type - Defines the type of access to the reactive property.
* @param key - Identifier of the reactive property to track.
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
// 获取当前target对象对应的depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取当前key对应的dep依赖
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
// 收集当前的effect作为依赖
dep.add(activeEffect!)
// 当前的effect收集dep集合作为依赖
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
extend(
{
effect: activeEffect!
},
debuggerEventExtraInfo!
)
)
}
}
}
收集完监听后,会得到targetMap,在触发监听trigger时,从targetMap拿到当前的target。
name是一个响应式数据,所以在触发name值修改时,会进入对应的Proxy对象中handler的set方法,在源码reactivity/src/baseHandlers.ts中,其核心代码如下:
function createSetter() {
...
// 触发监听
trigger(target, TriggerOpTypes.SET, key//name, value//efg, oldValue//abc)
...
}
从而进入trigger方法触发监听,在源码reactivity/src/effect.ts的trigger方法中,其核心代码如下:
/**
* Finds all deps associated with the target (or a specific property) and
* triggers the effects stored within.
*
* @param target - The reactive object.
* @param type - Defines the type of the operation that needs to trigger effects.
* @param key - Can be used to target a specific reactive property in the target object.
*/
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
//获取当前target的依赖映射表
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
trigger方法最终的目的是调度方法的调用,即运行ReactiveEffect对象中绑定的run方法。那么ReactiveEffect是什么,如何绑定对应的run方法?我们来看一下ReactiveEffect的实现,在源码reactivity/src/effect.ts中,其代码如下:
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T, // 传入回调方法
public scheduler: EffectScheduler | null = null, // 调度函数
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 执行绑定的方法
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
上面的代码中,在其构造函数中,将创建时传入的回调函数进行了run绑定,同时在Vue的组件挂载时会创建一个ReactiveEffect对象,在源码runtime-core/src/renderer.ts中,其核心代码如下:
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
...
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,// run方法绑定,该方法包括VNode生成逻辑
() => queueJob(update),
instance.scope // track it in component's effect scope
))
...
}
通过ReactiveEffect就将响应式和VNode逻辑进行了链接,其本身就是一个基于发布/订阅模式的事件对象,track负责订阅(即收集监听),trigger负责发布(即触发监听),effect是桥梁,用于存储事件数据。
ReactiveEffect也向外暴露了Composition API的effect方法,可以自定义地添加监听收集,在源码reactivity/src/effect.ts中,其核心代码如下:
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
//创建ReactiveEffect对象
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
在使用effect方法时,代码如下:
// this.name改变时会触发这里
Vue.effect(()=>{
console.log(this.name)
}
完整的响应式触发的过程总结流程图如下:
响应式触发.jpg
当响应式触发完成以后,就会进入VNode生成环节。
2. 生成新的VNode
在响应式逻辑中,创建ReactiveEffect时传入了componentUpdateFn,当响应式触发时,便会进入这个方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
const componentUpdateFn = () => {
// 首次渲染,直接找到对应DOM挂载即可,无须对比新旧VNode
if (!instance.isMounted) {
....
instance.isMounted = true
.....
}else{
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
// 判断是否是父组件带来的更新
if (next) {
next.el = vnode.el
// 子组件更新
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
...
// 获取新的VNode(根据新的响应式数据,执行render方法得到VNode)
const nextTree = renderComponentRoot(instance)
// 从subTree字段获取旧的VNode
const prevTree = instance.subTree
// 将新值赋值给subTree字段
instance.subTree = nextTree
// 进行新旧VNode对比
patch(
prevTree,
nextTree,
// teleport判断
hostParentNode(prevTree.el!)!,
// fragment判断
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
}
}
其中,对于新VNode的生成,主要是靠renderComponentRoot方法,
其内部会执行组件的render方法,通过render方法就可以获取到新的VNode,同时将新的VNode赋值给subTree字段,以便下次对比使用。
之后会进入patch方法,进行虚拟DOM的对比diff。
3. 虚拟DOM的diff过程
虚拟DOM的diff过程的核心是patch方法,它主要是利用compile阶段的patchFlag(或者type)来处理不同情况下的更新,这也可以理解为一种分而治之的策略。在该方法内部,并不是直接通过当前的VNode节点去暴力地更新DOM节点,而是对新旧两个VNode节点的patchFlag来分情况进行比较,然后通过对比结果找出差异的属性或节点按需进行更新,从而减少不必要的开销,提升性能。
patch的过程中主要完成以下几件事情:
- 创建需要新增的节点。
- 移除已经废弃的节点。
- 移动或修改需要更新的节点。
在整个过程中都会用到patchFlag进行判断,在AST到render再到VNode生成的过程中,会根据节点的类型打上对应的patchFlag,只有patchFlag还不够,还要依赖于shapeFlag的设置,在源码中对应的createVNode方法代码如下(\packages\runtime-core\src\vnode.ts):
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
// 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
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance
} as VNode
return vnode
}
_createVNode方法主要用来标准化VNode,同时添加上对应的shapeFlag和patchFlag。其中,shapeFlag的值是一个数字,每种不同的shapeFlag代表不同的VNode类型,而shapeFlag又是依据之前在生成AST时的NodeType而定的,所以shapeFlag的值和NodeType很像,代码如下:
export const enum ShapeFlags {
ELEMENT = 1, // 元素 string
FUNCTIONAL_COMPONENT = 1 << 1, // 2 function
STATEFUL_COMPONENT = 1 << 2, // 4 object
TEXT_CHILDREN = 1 << 3, // 8 文本
ARRAY_CHILDREN = 1 << 4, // 16 数组
SLOTS_CHILDREN = 1 << 5, // 32 插槽
TELEPORT = 1 << 6, // 64 teleport
SUSPENSE = 1 << 7, // 128 suspense
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,// 256 keep alive 组件
COMPONENT_KEPT_ALIVE = 1 << 9, // 512 keep alive 组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
// 组件
}
而patchFlag代表在更新时采用不同的策略,其具体每种含义如下:
export const enum PatchFlags {
// 动态文字内容
TEXT = 1,
// 动态 class
CLASS = 1 << 1,
// 动态样式
STYLE = 1 << 2,
// 动态 props
PROPS = 1 << 3,
// 有动态的key,也就是说props对象的key是不确定的
FULL_PROPS = 1 << 4,
// 合并事件
HYDRATE_EVENTS = 1 << 5,
// children 顺序确定的 fragment
STABLE_FRAGMENT = 1 << 6,
// children中带有key的节点的fragment
KEYED_FRAGMENT = 1 << 7,
// 没有key的children的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 只有非props需要patch,比如`ref`
NEED_PATCH = 1 << 9,
// 动态的插槽
DYNAMIC_SLOTS = 1 << 10,
...
// 特殊的flag,不会在优化中被用到,是内置的特殊flag
...SPECIAL FLAGS
// 表示它是静态节点,它的内容永远不会改变,在hydrate的过程中,不需要再对其子节点进行
diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2,
}
包括shapeFlag和patchFlag,和其名字的含义一致,其实就是用一系列的标志来标识一个节点该如何进行更新,其中CLASS = 1 << 1这种方式表示位运算,就是利用每个patchFlag取二进制中的某一位数来表示,这样更加方便扩展,例如TEXT|CLASS可以得到0000000011,这个值表示其既有TEXT的特性,也有CLASS的特性,如果需要新加一个flag,则直接用新数num左移1位即可,即1 << num。
shapeFlag可以理解成VNode的类型,而patchFlag则更像VNode变化的类型。
例如在demo代码中,我们给props绑定响应式变量attr,代码如下:
<div :data-a="attr"></div>
得到的patchFlag就是8(1<<3)。在源码compiler-core/src/transforms/transformElement.ts中可以看到对应的设置逻辑,核心代码如下:
// 每次都按位与,可以对多个数值进行设置
if (hasDynamicKeys) {
patchFlag |= PatchFlags.FULL_PROPS
} else {
if (hasClassBinding && !isComponent) {
patchFlag |= PatchFlags.CLASS
}
if (hasStyleBinding && !isComponent) {
patchFlag |= PatchFlags.STYLE
}
if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS
}
}
一切准备就绪,下面进入patch方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
// Note: functions inside this closure should use `const xxx = () => {}`
// style in order to prevent being inlined by minifiers.
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) { //新旧VNode是同一个对象,直接返回不比较
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
//patchFlage是BAIL类型的,跳出优化模式
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) { //根据VNode类型判断
case Text://文本
processText(n1, n2, container, anchor)
break
case Comment://注释
processCommentNode(n1, n2, container, anchor)
break
case Static://静态节点
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment://Fragment类型
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {//元素类型
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {//组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {//TELEPORT
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {//SUSPENSE
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
其中,n1为旧VNode,n2为新VNode,如果新旧VNode是同一个对象,就不再对比,如果旧节点存在,并且新旧节点不是同一类型,则将旧节点从节点树中卸载,这时还没有用到patchFlag。再往下看,通过switch case来判断节点类型,并分别对不同的节点类型执行不同的操作,这里用到了ShapeFlag,对于常用的HTML元素类型,则会进入default分支,我们以ELEMENT为例,进入processElement方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) { // 如果旧节点不存在,则直接渲染
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
processElement方法的逻辑相对简单,只是多加了一层判断,当没有旧节点时,直接进行渲染流程,这也是调用根实例初始化createApp时会用到的逻辑。真正进行对比,会进入patchElement方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
let { patchFlag, dynamicChildren, dirs } = n2
// #1426 take the old vnode's patch flag into account since user may clone a
// compiler-generated vnode, which de-opts to FULL_PROPS
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
let vnodeHook: VNodeHook | undefined | null
// disable recurse in beforeUpdate hooks
parentComponent && toggleRecurse(parentComponent, false)
//触发一些钩子
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
---
//当新VNode有动态节点时,优先更新动态节点
if (dynamicChildren) {
patchBlockChildren(
....
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if (!optimized) {//全量diff
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
//根据不同patchFlag进行不同的更新逻辑
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
//动态class
// class
// this flag is matched when the element has dynamic class bindings.
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// style 动态style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
// props 动态props
// This flag is matched when the element has dynamic prop/attr bindings
// other than class and style. The keys of dynamic prop/attrs are saved for
// faster iteration.
// Note dynamic keys like :[foo]="bar" will cause this optimization to
// bail out and go through a full diff because we need to unset the old key
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text 插值表达式 text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
在processElement方法的开头会执行一些钩子函数,然后判断新节点是否有已经标识的动态节点(就是在静态提升那一部分的优化,将动态节点和静态节点进行分离),如果有就会优先进行更新(无须对比,这样更快)。接下来通过patchProps方法更新当前节点的props、style、class等,主要逻辑如下:
- 当patchFlag为FULL_PROPS时,说明此时的元素中可能包含动态的key,需要进行全量的props diff。
- 当patchFlag为CLASS时,如果新旧节点的class不一致,则会对class进行atch;如果新旧节点的class属性完全一致,则不需要进行任何操作。这个Flag标记会在元素有动态的class绑定时加入。
- 当patchFlag为STYLE时,会对style进行更新,这是每次patch都会进行的,这个Flag会在有动态style绑定时被加入。
- 当patchFlag为PROPS时,需要注意这个Flag会在元素拥有动态的属性或者attrs绑定时添加,不同于class和style,这些动态的prop或attrs的key会被保存下来以便于更快速地迭代。
- 当patchFlag为TEXT时,如果新旧节点中的子节点是文本发生变化,则调用hostSetElementText进行更新。这个Flag会在元素的子节点只包含动态文本时被添加。
每种patchFlag对应的方法中,最终都会进入DOM操作的逻辑,例如对于STYLE更新,会进入setStyle方法,在源码runtime-dom/src/modules/style.ts中,其核心代码如下:
function setStyle(
style: CSSStyleDeclaration,
name: string,
val: string | string[]
) {
if (isArray(val)) {//多个style
val.forEach(v => setStyle(style, name, v))
} else {
if (val == null) val = ''
if (__DEV__) {
if (semicolonRE.test(val)) {
warn(
`Unexpected semicolon at the end of '${name}' style value: '${val}'`
)
}
}
if (name.startsWith('--')) {
// custom property definition 操作dom
style.setProperty(name, val)
} else {
const prefixed = autoPrefix(style, name)
if (importantRE.test(val)) {
// !important
style.setProperty(
hyphenate(prefixed),
val.replace(importantRE, ''),
'important'
)
} else {
style[prefixed as any] = val
}
}
}
}
对于一个VNode节点来说,除了属性(如props、class、style等)外,其他的都叫作子节点内容,<div>hi</div>中的文本hi也属于子节点。对于子节点,会进入patchChildren方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
...
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
....
)
return
}
}
//新节点是文本类型子节点(单个子节点)
// children has 3 possibilities: text, array or no children.
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
//旧节点是数组类型,则直接用新节点覆盖
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
//设置新节点
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
//新节点是数组类型子节点(多个子节点)
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff 新旧都是数组类型,则全量diff
patchKeyedChildren(
...
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null 设置空字符串
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
...
)
}
}
}
}
上面的代码中,首先根据patchFlag进行判断:
- 若patchFlag是存在key值的Fragment: KEYED_FRAGMENT,则调用patchKeyedChildren来继续处理子节点。
- 若patchFlag是没有设置key值的Fragment: UNKEYED_FRAGMENT,则调用patchUnkeyed Children处理没有key值的子节点。
- 然后根据shapeFlag进行判断:
- 如果新子节点是文本类型,而旧子节点是数组类型(含有多个子节点),则直接卸载旧节点的子节点,然后用新节点替换。
- 如果旧子节点类型是数组类型,当新子节点也是数组类型时,则调用patchKeyedChildren进行全量的diff,当新子节点不是数组类型时,则说明不存在新子节点,直接从树中卸载旧节点即可。
- 如果旧子节点是文本类型,由于已经在一开始就判断过新子节点是否为文本类型,因此此时可以肯定新子节点不是文本类型,可以直接将元素的文本置为空字符串。
- 如果新子节点是数组类型,而旧子节点不为数组,则说明此时需要在树中挂载新子节点,进行mount操作即可。
无论多么复杂的节点数组嵌套,其实最后都会落到基本的DOM操作,包括创建节点、删除节点、修改节点属性等,但核心是针对新旧两个树找到它们之间需要改变的节点,这就是diff的核心,真正的diff需要进入patchUnkeyedChildren和patchKeyedChildren来一探究竟。首先看一下patchUnkeyedChildren方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
const patchUnkeyedChildren = (
...
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
//获取新旧节点的最小长度
const commonLength = Math.min(oldLength, newLength)
let i
//遍历新旧节点进行patch
for (i = 0; i < commonLength; i++) {
//如果挂载过了克隆一份,否则创建新的VNode节点
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(...)
}
//如果旧节点梳理大于新节点,直接卸载多余的节点
if (oldLength > newLength) {
// remove old
unmountChildren(...)
} else {//否则创建
// mount new
mountChildren(...)
}
}
主要逻辑是首先拿到新旧节点的最短公共长度,然后遍历公共部分,对公共部分再次递归执行patch方法,如果旧节点的数量大于新节点的数量,则直接卸载多余的节点,否则新建节点。
对于没有key的情况,diff比较简单,但是性能也相对较低,很少实现DOM的复用,更多的是创建和删除节点,这也是Vue推荐对数组节点添加唯一key值的原因。
下面看下patchKeyedChildren方法,在源码runtime-core/src/renderer.ts中,其核心代码如下:
// can be all-keyed or mixed
const patchKeyedChildren = (...) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1.进行头部遍历,遇到相同的节点则继续,遇到不同的节点则跳出循环
while (i <= e1 && i <= e2) {...}
// 2.进行尾部遍历,遇到相同的节点则继续,遇到不同的节点则跳出循环
while (i <= e1 && i <= e2) {...}
// 3.如果旧节点已遍历完毕,并且新节点还有剩余,则遍历剩下的节点
if (i > e1) {
if (i <= e2) {...}
}
// 4.如果新节点已遍历完毕,并且旧节点还有剩余,则直接卸载
else if (i > e2) {
while (i <= e1) {...}
}
// 5.新旧节点都存在未遍历完的情况
else {
// 5.1创建一个map,为剩余的新节点存储键值对,映射关系:key => index
// 5.2遍历剩下的旧节点,对比新旧数据,移除不使用的旧节点
// 5.3拿到最长递增子序列进行移动或者新增挂载
}
}
patchKeyedChildren方法是整个diff的核心,其内部包括具体算法和逻辑,用代码讲解起来比较复杂,这里用一个简单的例子来说明该方法到底做了些什么,有两个数组,如下所示:
// 旧数组
["a", "b", "c", "d", "e", "f", "g", "h"]
// 新数组
["a", "b", "d", "f", "c", "e", "x", "y", "g", "h"]
上面的数组中,每个元素代表key,执行步骤如下:
- 1.从头到尾开始比较,[a,b]是sameVnode,进入patch,到[c]停止。
- 2.从尾到头开始比较,[h,g]是sameVnode,进入patch,到[f]停止。
- 3.判断旧数据是否已经比较完毕,多余的说明是新增的,需要mount,例子中没有。
- 4.判断新数据是否已经比较完毕,多余的说明是删除的,需要unmount,例子中没有。
- 到这里,说明顺序被打乱,进入5:
5.1 创建一个还未比较的新数据index的Map:[{d:2},{f:3},{c:4},{e:5},{x:6},{y:7}]。
5.2 根据未比较完的数据长度,建一个填充0的数组[0,0,0,0,0],然后循环一遍旧剩余数据,找到未比较的数据的索引arr:[4(d),6(f),3(c),5(e),0,0],如果没有在新剩余数据中找到,则说明是删除就unmount掉,找到了就和之前的patch一下。
5.3 从尾到头循环之前的索引arr,如果是0,则说明是新增的数据,就mount进去,如果不是0,则说明在旧数据中,我们只要把它们移动到对应index的前面就行了,如下:
把f移动到c之前。
把d移动到f之前。
移动之后,c自然会到e前面,这可以由之前的arr索引按最长递增子序列来找到[3,5],这样[3,5]对应的c和e就无须移动了。
这就是整个patchKeyedChildren方法中diff的核心内容和原理。
4. 完成真实DOM的修改
无论多么复杂的节点数组嵌套,其实最后都会落到基本的DOM操作,包括创建节点、删除节点、修改节点属性等,当拿到diff后的结果时,会调用对应的DOM操作方法,这部分逻辑在源码runtime-dom\src\nodeOps.ts中,存放的都是一些工具方法,其核心代码如下:
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
//插入元素
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
//删除元素
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
//创建元素
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
//创建文本
createText: text => doc.createTextNode(text),
//创建注释
createComment: text => doc.createComment(text),
//设置文本
setText: (node, text) => {
node.nodeValue = text
},
//设置元素
setElementText: (el, text) => {
el.textContent = text
},
parentNode: node => node.parentNode as Element | null,
nextSibling: node => node.nextSibling,
querySelector: selector => doc.querySelector(selector),
//设置元素属性
setScopeId(el, id) {
el.setAttribute(id, '')
},
//插入静态内容,包括处理SVG元素
// __UNSAFE__
// Reason: innerHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG, start, end) {
// <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild
// #5308 can only take cached path if:
// - has a single root node
// - nextSibling info is still available
if (start && (start === end || start.nextSibling)) {
// cached
while (true) {
parent.insertBefore(start!.cloneNode(true), anchor)
if (start === end || !(start = start!.nextSibling)) break
}
} else {
// fresh insert
templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content
const template = templateContainer.content
if (isSVG) {
// remove outer svg wrapper
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)
}
template.removeChild(wrapper)
}
parent.insertBefore(template, anchor)
}
return [
// first
before ? before.nextSibling! : parent.firstChild!,
// last
anchor ? anchor.previousSibling! : parent.lastChild!
]
}
}
这部分逻辑都是常规的DOM操作,比较简单。
至此已经将vue3双向绑定的原理讲完。
网友评论