Vue3核心源码解析 (四) : 双向绑定的原理

2023-04-17  本文已影响0人  奋斗_登

  在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等,主要逻辑如下:

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进行判断:

  1. 如果新子节点是文本类型,而旧子节点是数组类型(含有多个子节点),则直接卸载旧节点的子节点,然后用新节点替换。
  2. 如果旧子节点类型是数组类型,当新子节点也是数组类型时,则调用patchKeyedChildren进行全量的diff,当新子节点不是数组类型时,则说明不存在新子节点,直接从树中卸载旧节点即可。
  3. 如果旧子节点是文本类型,由于已经在一开始就判断过新子节点是否为文本类型,因此此时可以肯定新子节点不是文本类型,可以直接将元素的文本置为空字符串。
  4. 如果新子节点是数组类型,而旧子节点不为数组,则说明此时需要在树中挂载新子节点,进行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,执行步骤如下:

这就是整个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双向绑定的原理讲完。

上一篇下一篇

猜你喜欢

热点阅读