第三天-Vue3组件实现方式

123 阅读5分钟

patch

// 用于将新的虚拟 DOM 节点更新到真实的 DOM 中
  const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 目标容器
    anchor = null, // 在哪个 DOM 节点之前插入新节点
    parentComponent = null, // 新节点的父组件
    parentSuspense = null, // 父悬挂节点
    isSVG = false, // 是否在 SVG 环境中
    slotScopeIds = null, // 插槽作用域 ID 数组
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 是否启用了优化(静态提升、block 块缓存等)
  ) => {
    // 新旧节点相同,则不往下走
    if (n1 === n2) {
      return
    }

    // 有旧节点,但节点类型不同,则卸载旧节点
    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: // 分片
        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) {
          // 处理传送门
          ;(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})`)
        }
    }

    // 设置 ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

patch函数是 Vue3 中非常重要的函数,它用来对比新旧 VNode,更新 DOM,以实现数据的响应式渲染。其主要作用是根据新旧 VNode 的类型,选择不同的处理函数进行处理。以下是patch函数的主要处理流程:

  1. 判断新旧 VNode 是否相同,如果相同直接返回
  2. 判断新旧 VNode 的类型是否相同,如果不相同,则卸载旧节点,并用新节点进行替换
  3. 根据新旧 VNode 的类型,选择不同的处理函数进行处理,包括:文本、注释、静态资源、分片、元素、组件、传送门和悬挂节点
  4. 如果新 VNode 中定义了ref,则设置对应的引用
  5. 如果新 VNode 中定义了动态子节点,标记为optimized,启用优化
  6. 在执行完所有操作后,将新 VNode 赋值给容器的_vnode属性

在这个过程中,processElementprocessComponent函数是比较重要的,分别用于处理普通元素和组件元素的更新和渲染。

processComponent

  const processComponent = (
    n1: VNode | null, // 旧节点,如果是首次渲染则为 null。
    n2: VNode, // 新节点,需要被挂载或更新的节点
    container: RendererElement, // 目标容器
    anchor: RendererNode | null, // 新节点插入位置的参照节点
    parentComponent: ComponentInternalInstance | null, // 新节点的父组件实例
    parentSuspense: SuspenseBoundary | null, // 父悬挂节点
    isSVG: boolean, // 是否在 SVG 环境中
    slotScopeIds: string[] | null, // 插槽作用域 ID 数组
    optimized: boolean // 是否启用了优化(静态提升、block 块缓存等)
  ) => {
    // 给新节点添加 插槽作用域 ID 数组
    n2.slotScopeIds = slotScopeIds
    // 首次渲染,传入 n1 是 null
    if (n1 == null) {
      // 激活缓存的子树节点
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 挂载组件
        // 1. 创建组件实例
        // 2. 初始化组件实例
        // 3. 建立组件更新机制
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      // 更新组件
      updateComponent(n1, n2, optimized)
    }
  }

processComponent 函数是处理组件类型的节点的函数,主要作用是判断组件节点是首次渲染还是更新,并分别调用 mountComponentupdateComponent 函数进行挂载或更新操作。

首先会给新节点 n2 添加插槽作用域 ID 数组,然后通过判断旧节点 n1 是否为空来判断是首次渲染还是更新。如果是首次渲染,如果新节点的 shapeFlag 中包含 ShapeFlags.COMPONENT_KEPT_ALIVE 则调用 activate 函数激活缓存的子树节点,否则调用 mountComponent 函数进行挂载。如果是更新操作,则调用 updateComponent 函数进行更新。

mountComponent

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const compatMountInstance =
      __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component

    // 创建组件实例
    const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

    if (__DEV__ && instance.type.__hmrId) {
      registerHMR(instance)
    }

    if (__DEV__) {
      pushWarningContext(initialVNode)
      startMeasure(instance, `mount`)
    }

    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

    if (!(__COMPAT__ && compatMountInstance)) {
      if (__DEV__) {
        startMeasure(instance, `init`)
      }
      // 2. 初始化组件实例
      setupComponent(instance)
      if (__DEV__) {
        endMeasure(instance, `init`)
      }
    }

    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
      return
    }

    // 建立更新机制
    // 获取vnode
    // 1. 创建一个组件的更新函数
    //   1.1 render 获得 vnode
    //   1.2 patch(oldvnode,vnode)
    // 2. 创建更新机制: new ReactiveEffect(更新函数)
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

    if (__DEV__) {
      popWarningContext()
      endMeasure(instance, `mount`)
    }
  }

mountComponent,用于初始化并挂载一个组件实例。函数中的主要步骤包括:

  1. 创建组件实例:根据传入的 VNode 创建一个组件实例。
  2. 初始化组件实例:对组件实例进行初始化,包括解析 props、slots 等。
  3. 建立更新机制:为组件实例创建一个更新函数,并通过 new ReactiveEffect 建立更新机制。

函数执行完毕后,该组件实例就会被挂载到指定的容器中,并进行渲染。

总结

至此 Vue3 初始化流程就结束了,补全一些概念与重点函数

重点函数

  1. 应用实例:通过 createApp 函数创建的应用实例是一个包含组件、指令、插件等功能的对象,可以调用其方法实现对应用的控制。
  2. 虚拟 DOM:Vue3 中的虚拟 DOM 是基于 VNode 的,VNode 是对真实 DOM 的抽象表示,可以通过 createVNode 函数创建。
  3. 渲染函数:Vue3 通过渲染函数将 VNode 渲染为真实 DOM,渲染函数包括 patch 函数和 render 函数,其中 patch 函数实现了虚拟 DOM 的比较和更新,render 函数实现了将 VNode 渲染为真实 DOM 的过程。

重点函数

  1. createApp: 创建一个应用实例,返回一个应用实例对象,包括提供了一些方法和选项,如 mountunmountdirectivecomponent 等。
  2. createRenderer: 创建一个渲染器实例,用于渲染组件或者 vnode。其中会创建一个 Patching 算法,用于对比新旧 vnode 的不同,并在必要时更新 DOM 树。
  3. mount: 用于挂载应用实例,其中会调用 patch 函数对组件进行初始化,并在必要时创建 DOM 节点。
  4. _createVNode: 用于创建 vnode 对象,其中会对 vnode 的 type、props、children 进行预处理,用于后续的渲染过程。
  5. patch: 用于对比新旧 vnode 的不同,并在必要时更新 DOM 树
  6. normalizeChildren: 用于规范化子节点,将子节点统一处理成一个数组,并对其中的字符串节点进行处理。