08-构建render函数,处理ELEMENT 节点的更新操作(DOM元素相同)

91 阅读6分钟

总结

  1. 流程总结

通过render函数进行ELEMENT节点的挂载大致就是下面是大致流程:

**ELEMENT节点的挂载render函数patch挂载函数type+shapeFlagprocessElementn1==nullpatchElement**→ el = (n2.el = n1.el!)并记录新旧vnode的props开始更新孩子节点patchChildren获取新旧节点的children和shapeFlaghostSetElementText(container, c2 as string)更新孩子节点结束开始更新props patchProps两次for循环,一次挂载新节点的props,一次删除只有旧节点存在的属性→props更新结束

  1. 函数总结
  2. render函数:判断vnode是否为空,进行patch挂载还是unmount卸载操作。
  3. patch函数:根据vnode的type和shapeFlag进行不同的processXXX操作,本节中是processElement
  4. processElement函数:根据vnode1(n1)是否为空进行mountElement挂载、patchElement更新
  5. patchElement函数(同DOM):更新children节点、更新props(挂载新节点的属性、删除存在于旧节点的属性)

代码测试样例

<div id="app"></div>
<script>
  const app = document.querySelector('#app')
  const { render, h } = Vue
  const vnode1 = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )
  render(vnode1, app) // ELEMENT的挂载
  setTimeout(() => { // ELEMENT(同DOM)的更新
    const vnode2 = h(
      'div',
      {
        class: 'active'
      },
      'render update'
    )
    render(vnode2, app)
  }, 2000)
</script>

函数内部实现流程

  1. render函数,如果传递了第一个参数,则进行patch更新操作,否则进行unmount卸载操作

render函数

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()
    container._vnode = vnode
  }
  1. 由于我们进行的是ELEMENT的挂载操作,所以直接进入patch函数,patch函数中主要是判断**vnode.typevnode.shapeFlag**来进行不同的处理,这里是进行ELEMENT的处理。

patch函数

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) { // 1. 判断新旧元素是否相同,相同则不进行操作
      return
    }
    const { type, ref, shapeFlag } = n2 //解构传入的参数,就是render的第一个参数vnode
    switch (type) { //根据不同的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:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default://匹配其他的项(非文本类)
        if (shapeFlag & ShapeFlags.ELEMENT) { //shapeFlag & ShapeFlags.xxx 使用二进制的与运算,这里是对vnode中的shapeFlag进行分解,获取创建vnode时使用的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
          ;(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) {
          ;(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})`)
        }
    }
  }
  1. 进入processElement,如果n1是空则进行mountElement函数,反之则进入patchElement函数,当前n1是空,进入mountElement函数。判断旧节点是否存在进行挂载和更新操作。

processElement函数

const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
  ) => {
    if (n1 == null) {
      mountElement( // 挂载
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      patchElement( // 更新
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
  1. 进入patchElement更新函数(同DOM),进行元素节点的赋值const el = (n2.el = n1.el!),定义变量存储新旧vnode的props对象,调用patchChildren进行孩子节点的更新,调用patchProps进行props的更新。

    patchElement函数总结:

    1. 元素节点复制const el = (n2.el = n1.el!)
    2. patchChildren函数:更新children内容
    3. patchProps函数:更新props内容

patchElement函数(处理同DOM)

const patchElement = (
    n1: VNode,
    n2: VNode,
  ) => {
    const el = (n2.el = n1.el!)
    let { patchFlag, dynamicChildren, dirs } = n2

    const oldProps = n1.props || EMPTY_OBJ
    const newProps = n2.props || EMPTY_OBJ

    // 更新子节点
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds,
      false
    )
    // 更新props属性
    patchProps(
      el,
      n2,
      oldProps,
      newProps,
      parentComponent,
      parentSuspense,
      isSVG
    )
  }
  1. patchChildren函数,使用c1、c2记录n1、n2的children属性,并记录n1、n2的shapeFlag。然后根据n2.shapeFlag值进行分解,最终匹配到hostSetElementText,直接进行元素文本赋值。

patchChildren函数

const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
  ) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const { patchFlag, shapeFlag } = n2

    // 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
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } 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(
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
      }
    }
  }
  1. patchProps函数,处理节点props的属性。进行两次循环,第一次是挂载新节点的props到元素上,第二次是删除旧节点在元素el上的属性。

patchProps函数

const patchProps = (
    el: RendererElement,
    vnode: VNode,
    oldProps: Data,
    newProps: Data,
  ) => {
    if (oldProps !== newProps) {
      for (const key in newProps) { // el对新节点vnode的props挂载
        // empty string is not valid prop
        if (isReservedProp(key)) continue
        const next = newProps[key]
        const prev = oldProps[key]
        // defer patching value
        if (next !== prev && key !== 'value') {
          hostPatchProp(
            el,
            key,
            prev,
            next,
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
      if (oldProps !== EMPTY_OBJ) {
        for (const key in oldProps) {// el对旧节点vnode的props卸载(只存在于旧节点上的,不是共有的)
          if (!isReservedProp(key) && !(key in newProps)) {
            hostPatchProp(
              el,
              key,
              oldProps[key],
              null,
              isSVG,
              vnode.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
      }
      if ('value' in newProps) { // 处理value的
        hostPatchProp(el, 'value', oldProps.value, newProps.value)
      }
    }
  }

总结

  1. 流程总结

通过render函数进行ELEMENT节点的挂载大致就是下面是大致流程:

**ELEMENT节点的挂载render函数patch挂载函数type+shapeFlagprocessElementn1==nullpatchElement**→ el = (n2.el = n1.el!)并记录新旧vnode的props开始更新孩子节点patchChildren获取新旧节点的children和shapeFlaghostSetElementText(container, c2 as string)更新孩子节点结束开始更新props patchProps两次for循环,一次挂载新节点的props,一次删除只有旧节点存在的属性→props更新结束

  1. 函数总结
  2. render函数:判断vnode是否为空,进行patch挂载还是unmount卸载操作。
  3. patch函数:根据vnode的type和shapeFlag进行不同的processXXX操作,本节中是processElement
  4. processElement函数:根据vnode1(n1)是否为空进行mountElement挂载、patchElement更新
  5. patchElement函数(同DOM):赋值元素el,记录新旧节点props,更新children节点、更新props(挂载新节点的属性、删除存在于旧节点的属性)
  6. patchChildren函数:根据shapeFlag的值进行不同的操作
  7. patchProps函数:处理节点props的属性。进行两次循环,第一次是挂载新节点的props到元素上,第二次是删除旧节点在元素el上的属性。