美文网首页
Vue3核心源码解析 (四) : 双向绑定的原理

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

作者: 奋斗_登 | 来源:发表于2023-04-17 14:24 被阅读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等,主要逻辑如下:

    • 当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进行判断:
    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,执行步骤如下:

    • 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双向绑定的原理讲完。

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/bjhiddtx.html