vue3源码之渲染和更新流程

1.创建虚拟dom

createAppAPImount方法中, 通过createVNode创建了VNode:

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    ...

     mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
        }

        ...
     }
  }
}

createVNode的大致实现如下:

export const createVNode = (__DEV__
  ? createVNodeWithArgsTransform
  : _createVNode) as typeof _createVNode

只讨论生产环境的情形:

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 {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
  
    type = Comment
  }

  if (isVNode(type)) {
    // createVNode receiving an existing vnode. This happens in cases like
    // <component :is="vnode"/>
    // #2078 make sure to merge refs during the clone instead of overwriting it
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // class component normalization.
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // class & style normalization.
  if (props) {
    // 处理props相关逻辑, 标准化class和style
    。。。。
  }

  // 对vnode类型信息编码
  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: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children: null,
    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
  }


  // 标准化子节点, 把不同数据类型的children转成数组或者文本类型
  normalizeChildren(vnode, children)

 。。。。

  return vnode
}

 在mount方法中, 渲染vnode是通过render方法实现的:

render(vnode, rootContainer, isSVG)

该方法是在baseCreateRenderer方法中定义的: 

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
// 销毁组件
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
// 创建或者更新组件的逻辑
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
// 缓存vnode节点, 表示已经渲染
    container._vnode = vnode
  }

2.vnode到真实dom之间的转变 

patch方法:

  const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = false
  ) => {
    // patching & not same type, unmount old tree   如果存在新旧节点, 且新旧节点类型不同, 则销毁旧节点
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      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) {
// 处理普通DOM元素
          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   类似于react的portals
          ;(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)
    }
  }

组件的渲染和对普通dom元素的渲染处理 :

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
// 挂载
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
// 更新
      updateComponent(n1, n2, optimized)
    }

这个方法主要是两个功能:

  1. 将vnode挂载dom
  2. 以vnode更新dom

如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。

 

 挂载mount 方法:
 

const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 创建组件
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    ...

    // 设置组件实例
    setupComponent(instance)
    
    ...

    // 设置并运行带副作用的渲染函数
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

    ...
  }
  • 创建组件实例: 内部返回了一个instance对象;,通过对象的方式去创建当前渲染的组件实例
  • 设置组件实例: instance保留了很多组件相关的数据, 维护了组件的上下文, 包括对props, 插槽, setup函数的处理;
  • 设置并运行带副作用的渲染函数

setupRenderEffect方法中, 主要代码如下:

const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 创建响应式的副作用渲染函数
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        // 初始渲染
        ...
        // 渲染组件生成子树vnode
        const subTree = (instance.subTree = renderComponentRoot(instance))

        if (el && hydrateNode) {
          ...
        } else {
         ...
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )   
          ...
        }
        // 保留渲染生成的子树根DOM节点
        initialVNode = container = anchor = null as any
      } else {
        // 更新组件
      // 更新组件
      let { next, vnode } = instance
      // next 表示新的组件 vnode
      if (next) {
        // 更新组件 vnode 节点信息
        updateComponentPreRender(instance, next, optimized)
      }
      else {
        next = vnode
      }
      // 渲染新的子树 vnode
      const nextTree = renderComponentRoot(instance)
      // 缓存旧的子树 vnode
      const prevTree = instance.subTree
      // 更新子树 vnode
      instance.subTree = nextTree
      // 组件更新核心逻辑,根据新旧子树 vnode 做 patch
      patch(prevTree, nextTree,
        // 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
        hostParentNode(prevTree.el),
        // 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG)
      // 缓存更新后的 DOM 节点
      next.el = nextTree.el
    }
      }
    }, prodEffectOptions)
  }

对于元素的渲染, 主要需要调用processElement:

  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent, 
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
    }
  }

 首先看mountElement方法的实现:

const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const {
      type,
      props,
      shapeFlag,
      transition,
      scopeId,
      patchFlag,
      dirs
    } = vnode
    if (
      !__DEV__ &&
      vnode.el &&
      hostCloneNode !== undefined &&
      patchFlag === PatchFlags.HOISTED
    ) {
      el = vnode.el = hostCloneNode(vnode.el)
    } else {
      // 创建DOM元素节点
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )

      // 处理props
      if (props) {
        for (const key in props) {
          if (!isReservedProp(key)) {
            hostPatchProp(
              el,
              key,
              null,
              props[key],
              isSVG,
              vnode.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
      }

      // 创建子节点
 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(el, vnode.children as string)
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(
        vnode.children as VNodeArrayChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG && type !== 'foreignObject',
        optimized || !!vnode.dynamicChildren
      )
      
      // 挂载元素到container上
      hostInsert(el, container, anchor)
    }
  }

其中, hostCreateElement方法如下:

createElement: (tag, isSVG, is): Element =>
  isSVG
    ? doc.createElementNS(svgNS, tag)
    : doc.createElement(tag, is ? { is } : undefined),

创建完DOM节点后, 判断是否有props, 有则添加相关的class, style, event等属性 

 

然后就是创建子节点了

对于纯文本, 则调用setElementText:

setElementText: (el, text) => {
  el.textContent = text
},

对于数组, 则调用mountchildren:

const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized,
    start = 0
  ) => {
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  }

递归处理调用patch

 最后调用hostInsert方法, 把节点挂载contianer中.

insert: (child, parent, anchor) => {
  parent.insertBefore(child, anchor || null)
},

3.更新流程

patch方法中,对于已经挂载过的组件执行updateComponent 逻辑:

 const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
    const instance = (n2.component = n1.component)!
    // 根据新旧子组件vnode判断是否需要更新子组件
    if (shouldUpdateComponent(n1, n2, optimized)) {
      if (
        __FEATURE_SUSPENSE__ &&
        instance.asyncDep &&
        !instance.asyncResolved
      ) {
        // 更新组件的代码
        updateComponentPreRender(instance, n2, optimized)
        return
      } else {
        // normal update
        instance.next = n2
        // 避免子组件由于自身数据变化导致的重复更新
// in case the child component is also queued, remove it to avoid
        // double updating the same child component in the same flush
        invalidateJob(instance.update)
        // 主动触发子组件的更新
        instance.update()
      }
    } else {
      // 没有更新, 直接复制属性
      n2.component = n1.component
      n2.el = n1.el
      instance.vnode = n2
    }
  }
  • 一种是组件本身的数据变化, 这种情况下nextnull
  • 另一种是父组件在更新过程中, 遇到子组件节点, 先判断子组件是否需要更新. 如果需要则主动执行子组件的重新渲染方法, 这种情况下, next就是新的子组件vnode.

shouldUpdateComponent方法的内部, 主要是通过比较新旧节点的props, children, dfs, transition等来判断是否需要更新.

vue中的更新粒度是组件级别的, 组件的数据变化只会影响当前组件的更新. 但是在组件更新的过程中, 也会对子组件做一定的检查, 判断子组件是否也需要更新, 并通过某种机制避免子组件重复更新

const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean
  ) => {
    // 新组件 vnode 的 component 属性指向组件实例
    nextVNode.component = instance
    // 旧组件的 vnode 的 props 属性
    const prevProps = instance.vnode.props
    // 组件实例的vnode属性指向新的组件vnode
    instance.vnode = nextVNode
    // 清空 next 属性, 为了下一次重新渲染做准备
    instance.next = null
    // 更新 props
    updateProps(instance, nextVNode.props, prevProps, optimized)
    // 更新插槽
    updateSlots(instance, nextVNode.children)

    // props update may have triggered pre-flush watchers.
    // flush them before the render update.
    flushPreFlushCbs(undefined, instance.update)
  }

对于processComponent处理组件vnode, 就是在做一件事: 判断子组件是否需要更新.

如果需要则递归执行子组件的副作用渲染函数来更新, 否则仅仅更新vnode的属性, 并让子组件实例保留对组件vnode的引用, 用于子组件自身数据变化引起组件重新渲染的时候再渲染函数内部可以拿到新的组件vnode

 

组件更新中对元素的处理

processComponent 主要通过执行 updateComponent 函数来更新子组件

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
// 挂载
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
// 更新
      updateComponent(n1, n2, optimized)
    }

updateComponent 函数 实现如下:

  const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
    const instance = (n2.component = n1.component)!
    if (shouldUpdateComponent(n1, n2, optimized)) {
      if (
        __FEATURE_SUSPENSE__ &&
        instance.asyncDep &&
        !instance.asyncResolved
      ) {
        updateComponentPreRender(instance, n2, optimized)

        return
      } else {
        // normal update 
        instance.next = n2
        // in case the child component is also queued, remove it to avoid
        // double updating the same child component in the same flush.避免子组件由于自身数据变化导致的重复更新
        invalidateJob(instance.update)
        // instance.update is the reactive effect runner. 子组件的副作用渲染函数 instance.update 来主动触发子组件的更新
        instance.update()
      }
    } else {
      // no update needed. just copy over properties
      n2.component = n1.component
      n2.el = n1.el
      instance.vnode = n2
    }
  }

 副作用渲染函数

// 更新组件
let { next, vnode } = instance
// next 表示新的组件 vnode
if (next) {
  // 更新组件 vnode 节点信息
  updateComponentPreRender(instance, next, optimized)
}
else {
  next = vnode
}
const updateComponentPreRender = (instance, nextVNode, optimized) => {
  // 新组件 vnode 的 component 属性指向组件实例
  nextVNode.component = instance
  // 旧组件 vnode 的 props 属性
  const prevProps = instance.vnode.props
  // 组件实例的 vnode 属性指向新的组件 vnode
  instance.vnode = nextVNode
  // 清空 next 属性,为了下一次重新渲染准备
  instance.next = null
  // 更新 props
  updateProps(instance, nextVNode.props, prevProps, optimized)
  // 更新 插槽
  updateSlots(instance, nextVNode.children)
}

一个组件重新渲染可能会有两种场景,一种是组件本身的数据变化,这种情况下 next 是 null;

另一种是父组件在更新的过程中,遇到子组件节点,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法,这种情况下 next 就是新的子组件 vnode。

 

它是在父组件重新渲染的过程中,通过 renderComponentRoot 渲染子树 vnode 的时候生成,因为子树 vnode 是个树形结构,通过遍历它的子节点就可以访问到其对应的组件 vnode。再拿我们前面举的例子说,当 App 组件重新渲染的时候,在执行 renderComponentRoot 生成子树 vnode 的过程中,也生成了 hello 组件对应的新的组件 vnode。

 

processComponent 处理组件 vnode,本质上就是去判断子组件是否需要更新,如果需要则递归执行子组件的副作用渲染函数来更新,否则仅仅更新一些 vnode 的属性,并让子组件实例保留对组件 vnode 的引用,用于子组件自身数据变化引起组件重新渲染的时候,在渲染函数内部可以拿到新的组件 vnode。

 

组件更新中对元素的处理

对元素的更新处理在patch函数中的processElement:

const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      // 挂载元素
    } else {
      // 更新元素
      patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
    }
  }
 const patchElement = (
    n1: VNode,
    n2: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    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
   
    // ...

    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.FULL_PROPS) {
        // element props contain dynamic keys, full diff needed
        patchProps(
          el,
          n2,
          oldProps,
          newProps,
          parentComponent,
          parentSuspense,
          isSVG
        )
      } else{
        // ...
      }

      // ...
    } else if (!optimized && dynamicChildren == null) {
      // unoptimized, full diff
      // 更新props
      patchProps(
        el,
        n2,
        oldProps,
        newProps,
        parentComponent,
        parentSuspense,
        isSVG
      )
    }

    const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
    if (dynamicChildren) {
      // ...
    } else if (!optimized) {
      // full diff
      // 更新子节点
      patchChildren(
        n1,
        n2,
        el,
        null,
        parentComponent,
        parentSuspense,
        areChildrenSVG
      )
    }

    // ...
  }

 

该函数中主要做了两件事: 更新props和更新子节点.

首先是更新props, 这里的patchProps就是在更新DOM节点的classstyleevent以及其他的一些DOM属性

其次是更新子节点patchChildren:

 

 

对于一个元素的子节点, vnode可能会有三种情况: 纯文本, vnode数组和空. 那么排列组合对于新旧子节点来说就有九种情况.

旧子节点\新子节点纯文本vnode 数组
纯文本替换新文本清空文本, 添加多个新子节点删除旧子节点
vnode数组删除旧子节点, 添加新文本节点完整Diff删除旧子节点
添加新文本节点添加多个新子节点什么都不做

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值