Vue3追本溯源(八)执行patch方法生成DOM

414 阅读8分钟

上文主要解析了执行render函数生成VNode对象,本文将详细解析patch方法如何将VNode对象转化为真正的DOM

patch方法解析VNode入口

由上文解析知,在renderComponentRoot函数中,通过执行render!.call(...)生成并返回VNode对象,renderComponentRoot函数最终也是返回VNode。回归到setupRenderEffect函数中,起初是执行componentEffect方法时执行了renderComponentRoot函数(关于setupRenderEffect方法中effect函数的执行过程,可以看下"全局依赖activeEffect收集")。下面解析下setupRenderEffect函数的后续操作

if (el && hydrateNode) {/*...*/}
else {
    // ...
    patch(
      null,
      subTree,
      container,
      anchor,
      instance,
      parentSuspense,
      isSVG
    )
    // ...
    initialVNode.el = subTree.el
}

后续就是调用patch方法,传参的subTree就是VNode对象,container就是#app节点的DOM对象(关于调用setupRenderEffect函数传入的container参数,可以看下"入口函数-mount挂载"#app节点的DOM对象是如何生成的,以及如何通过调用链传递给setupRenderEffect函数),本例为<div id='app'></div>,下面解析下patch方法的内部实现

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
) => {
    if (n1 && !isSameVNodeType(n1, n2)) {/*...初始挂载时n1为null*/}
    if (n2.patchFlag === PatchFlags.BAIL/*2*/) {/*...*/}
    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT/* 1 */) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        }
    }
}

patch方法解析根VNode对象

首先解构出VNode对象的type属性,本例解析的VNode.typeSymbol('Fragment')(根VNode对象的生成过程可以看下"createBlock创建根vnode对象"),所以执行processFragment函数

// processFragment方法定义
const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
) => {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

    let { patchFlag, dynamicChildren } = n2
    if (patchFlag > 0) {
      optimized = true
    }
    // ...
    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {}
}

app元素插入两个空文本元素

processFragment方法内部首先创建VNodeelanchor属性,当n1不存在时(初始化挂载的时候老的VNode对象是不存在的),就是hostCreateText('')的返回值(在baseCreateRenderer函数开头部分定义了一系列的操作DOM的方法,这里不一一列举,遇到再详细解析),hostCreateText函数就是createText方法,而createTextnodeOps对象中对应的方法如下(关于调用baseCreateRenderer方法的options对象,就是调用createRenderer函数传入的rendererOptions对象,其中就包含操作DOM方法的nodeOps对象,这部分的执行流程,可以看下"入口函数-app对象生成"):

// 校验运行环境中是否存在document属性
const doc = (typeof document !== 'undefined' ? document : null);
// 定义createText方法
createText: text => doc.createTextNode(text)

所以createText方法就是调用createTextNode方法创建文本节点,所以n2elanchor属性都是文本节点并且内容为空,之后调用hostInsert方法,hostInsert方法在options对象中对应的是insert方法,而insertnodeOps对象中对应的方法如下

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

所以hostInsert方法的作用就是在container节点(#appDOM节点)的最后插入fragmentStartAnchorfragmentEndAnchor两个空的文本节点。

mountChildren方法解析VNode子对象

再调用mountChildren方法解析VNode对象的子对象(children数组)

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
      )
    }
}

mountChildren方法主要是循环children数组,将每一个子VNode对象继续调用patch方法。

processText解析动态文本对象

本例上文解析出来的VNode对象,children数组中有两个元素,第一个是typeSymbol(Text)的文本对象,回归到patch方法中,switch-case根据type的值调用processText方法

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
        // ...
    }
}

因为n1==null,调用hostInsert方法插入元素,第一个参数是hostCreateText方法的返回值,在本文上面解析了hostCreateText方法就是创建文本节点的,这里创建的文本节点的内容是n2.children,也就是第一个子节点对象的children值(本例为字符串"测试数据 "),之后调用hostInsert方法将"测试数据 "文本节点插入到container(#appDOM节点)的空文本节点(fragmentEndAnchor)之前。

此时#app的根DOM节点下新增了一个文本节点,<div id='app'>测试数据 </div>,页面上也由空白新增了"测试数据 "文案

processElement解析HTML标签元素对象

处理完第一个children元素回归到mountChildren方法中,继续调用patch方法解析第二个button元素的VNode对象,此时type"button"字符串,回归到patch方法中,根据type类型switch-case进入default分支,再根据shapeFlag的值(9),调用processElement方法处理元素VNode对象

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)
    }
  }

processElement函数中根据n1==null,继续调用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) {}
    else {
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )
      
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(el, vnode.children as string)
      }
      // ... dirs
      if (props) {
        for (const key in props) {
          if (!isReservedProp(key)) {
            hostPatchProp(el, key, null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren)
          }
        }
        // ...
      }
      // scopeId
    }
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      Object.defineProperty(el, '__vnode', {
        value: vnode,
        enumerable: false
      })
      Object.defineProperty(el, '__vueParentComponent', {
        value: parentComponent,
        enumerable: false
      })
    }
    if (dirs) {/*...*/}
    const needCallTransitionHooks = (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) && transition && !transition.persisted
    if (needCallTransitionHooks) {/*...*/}
    hostInsert(el, container, anchor)
    // ...异步的执行队列,v-show进入此分支
}

createElement创建HTML标签DOM元素

mountElement函数中首先调用hostCreateElement创建元素,hostCreateElementoptions中对应的是createElementcreateElementnodeOps对象中对应的方法如下

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

因为isSVGfalse,所以调用document.createElement方法,而props.is是不存在的,所以参数为(tag, undefined),从而创建了一个buttonDOM节点并赋值给了vnodeel属性。

setElementText设置元素文本内容

之后因为shapeFlag=8,所以调用hostSetElementText方法。hostSetElementTextoptions中对应的是 setElementTextsetElementTextnodeOps对象中对应的方法如下

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

就是设置el节点的文本内容,根据传参是给button节点设置vnode.children内容。vnode.childrenbutton子节点对象的子节点,本例为"修改数据"字符串。

addEventListener添加元素的事件监听

后续再判读VNode对象的props属性是否存在,如果存在则调用hostPatchProp方法为button元素添加属性(这里会通过isReservedProp方法判断是否是特定的属性值,例如:keyref等等,本例为onClick)。hostPatchProp函数在options 对象中对应的patchProp,调用baseCreateRenderer方法传入的rendererOptions对象,除了nodeOps对象,还合并了{ patchProp, forcePatchProp },看下patchProp的具体实现

// isOn定义
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)

// patchProp定义
export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
    switch (key) {
        default:
            if (isOn(key)) {
            // ignore v-model listeners
            if (!isModelListener(key)) {
              patchEvent(el, key, prevValue, nextValue, parentComponent)
            }
            // else-if
            break
      }
    }
}

patchProp方法中根据key的值选择switch-case分支,本例key=onClick,进入default分支,调用isOn方法(以on开头并且后一个字符不是小写字母a-z)判断是否为事件监听属性,本例返回true。再调用isModelListener方法(属性名key"onUpdate:"开头),本例返回为false。所以调用patchEvent函数

export function patchEvent(
  el: Element & { _vei?: Record<string, Invoker | undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {})
  const existingInvoker = invokers[rawName]
  if (nextValue && existingInvoker) {/*...*/} 
  else {
    const [name, options] = parseName(rawName)
    if (nextValue) {
      // add
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      addEventListener(el, name, invoker, options)
    } else if (existingInvoker) {
      // remove ...
    }
  }
}

patchEvent函数中,首先因为el._vei{}空对象,所以existingInvoker=undefined,然后调用parseName方法解析属性名

const optionsModifierRE = /(?:Once|Passive|Capture)$/

function parseName(name: string): [string, EventListenerOptions | undefined] {
  let options: EventListenerOptions | undefined
  if (optionsModifierRE.test(name)) {/*...*/}
  return [hyphenate(name.slice(2)), options]
}

parseName方法首先判断属性名称是否是Once|Passive|Capture,否则返回一个数组,数组的第一个元素是获取属性名称的第二位到最后一位,本例是onClick,所以name.slice(2)=click。第二个元素是options=undefined,最终此方法返回[click, undefined]

回归到patchEvent方法中,因为nextValue是存在的,就是属性值,所以调用createInvoker函数

function createInvoker(
  initialValue: EventValue,
  instance: ComponentInternalInstance | null
) {
  const invoker: Invoker = (e: Event) => {/* ...定义invoker方法 */}
  invoker.value = initialValue
  invoker.attached = getNow()
  return invoker
}

createInvoker函数内部定义了invoker方法,将invoker.value设置为props的属性值,本例为modifyMessage方法,最后返回invoker函数。回归到patchEvent方法中,继续调用addEventListener函数

export function addEventListener(
  el: Element,
  event: string,
  handler: EventListener,
  options?: EventListenerOptions
) {
  el.addEventListener(event, handler, options)
}

addEventListener方法就是给el(创建的button元素),添加event(click点击事件)事件监听,事件的回调函数就是invoker函数。然后回归到mountElement方法中,继续调用hostInsertel(button元素)插入到container(#appDOM元素)的anchor(fragmentEndAnchor空文本节点)文本元素之前。

回归到mountChildren方法中,以本模版为例,根VNode下的children已经全部循环解析并生成了DOM元素。

此时的#appDOM元素为: <div id='app'>测试数据 <button>修改数据</button></div>button按钮上有click事件监听

至此patch方法解析VNode对象生成DOM元素已经完成了。后续的关键点在于button按钮的事件监听上,点击按钮触发事件监听的回调函数,执行modifyMessage方法,修改message.value值,触发set钩子函数,重新生成VNode

总结

接上文生成VNode对象,本文主要解析patch方法如何解析VNode对象生成DOM元素,首先是解析根VNode对象,在#app元素上添加两个空文本节点,然后循环解析子元素(VNode.children数组),依次插入到第二个空文本DOM节点之前。然后对于HTML标签元素(依本例的button标签为例),创建button元素,添加元素的文本内容(按钮内容),之后在button元素上添加click事件监听。后续会解析,点击按钮时执行回调函数,修改数据,如何触发DOM的更新。