Vue3 源码解读系列(三)——组件渲染

组件渲染

vnode 本质是用来描述 DOM 的 JavaScript 对象,它在 Vue 中可以描述不同类型的节点,比如:普通元素节点、组件节点等。

vnode 的优点:

  1. 抽象:引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升

  2. 跨平台:因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、weex 平台、小程序平台的渲染

组件的渲染流程:

Vue3 源码解读系列(三)——组件渲染_第1张图片

  1. 创建 vnode

    createVNode 主要做了四件事:

    1. 处理 props,标准化 class 和 style
    2. 对 vnode 类型信息编码
    3. 创建 vnode 对象
    4. 标准化子节点
    /**
     * 创建 vnode
     */
    function createVNode(type, props = null, children = null) {
      // 1、处理 props,标准化 class 和 style
      if (props) {
        // ...
      }
    
      // 2、对 vnode 类型信息编码
      const shapeFlag = isString(type)
        ? 1 /* ELEMENT */
        : isSuspense(type)
          ? 128 /* SUSPENSE */
          : isTeleport(type)
            ? 64 /* TELEPORT */
            : isObject(type)
              ? 4 /* STATEFUL_COMPONENT */
              : isFunction(type)
                ? 2 /* FUNCTIONAL_COMPONENT */
                : 0
    
      // 3、创建 vnode 对象
      const vnode = {
        type,
        props,
        shapeFlag,
        // 一些其他属性
      }
    
      // 4、标准化子节点,把不同数据类型的 children 转成数组或者文本类型
      normalizeChildren(vnode, children)
      return vnode
    }
    
  2. 渲染 vnode

    render 主要做了几件事:

    1. 检查是否存在 vnode
      • 如果之前有,现在没有,则销毁
      • 如果现在有,则创建或更新
    2. 缓存 vnode,用于判断是否已经渲染
    /**
     * 渲染 vnode
     */
    const render = (vnode, container) => {
      // vnode 为 null,则销毁组件
      if (vnode == null) {
        if (container._vnode) {
          unmount(container._vnode, null, null, true)
        }
      }
      // 否则创建或者更新组件
      else {
        patch(container._vnode || null, vnode, container)
      }
    
      // 缓存 vnode 节点,表示已经渲染
      container._vnode = vnode
    }
    

    patch 主要做了两件事:

    1. 判断是否销毁节点
    2. 挂载新节点
    /**
     * 更新 DOM
     * @param {vnode} n1 - 旧的 vnode(为 null 时表示第一次挂载)
     * @param {vnode} n2 - 新的 vnode
     * @param {DOM} container - DOM 容器,vnode 渲染生成 DOM 后,会挂载到 container 下面
     */
    const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
      // 如果存在新旧节点,且新旧节点类型不同,则销毁旧节点
      if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1)
        unmount(n1, parentComponent, parentSuspense, true)
        n1 = null
      }
    
      // 挂载新 vnode
      const { type, shapeFlag } = n2
      switch (type) {
        case Text:
          // 处理文本节点
          break
        case Comment:
          // 处理注释节点
          break
        case Static:
          // 处理静态节点
          break
        case Fragment:
          // 处理 Fragment 元素
          break
        default:
          if (shapeFlag & 1/* ELEMENT */) {
            // 处理普通 DOM 元素
            processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
          } else if (shapeFlag & 6/* COMPONENT */) {
            // 处理 COMPONENT
            processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
          } else if (shapeFlag & 64/* TELEPORT */) {
            // 处理 TELEPORT
          } else if (shapeFlag & 128/* SUSPENSE */) {
            // 处理 SUSPENSE
          }
      }
    }
    

    处理组件

    /**
     * 处理 COMPONENT
     */
    const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      // 旧节点为 null,表示不存在旧节点,则直接挂载组件
      if (n1 == null) {
        mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      // 旧节点存在,则更新组件
      else {
        updateComponent(n1, n2, parentComponent, optimized)
      }
    }
    
    /**
     * 挂载组件
     * mountComponent 做了三件事:
     * 1、创建组件实例
     * 2、设置组件实例
     * 3、设置并运行带副作用的渲染函数
     */
    const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      // 1、创建组件实例,内部也通过对象的方式去创建了当前渲染的组件实例
      const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
    
      // 2、设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文包括对 props、插槽,以及其他实例的属性的初始化处理
      setupComponent(instance)
    
      // 3、设置并运行带副作用的渲染函数
      setupRenderEffet(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
    }
    
    /**
     * 初始化渲染副作用函数
     * 副作用:当组件数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的
     */
    const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
      // 创建响应式的副作用渲染函数
      instance.update = effect(function componentEffect() {
        // 如果组件实例 instance 上的 isMounted 属性为 false,说明是初次渲染
        /**
         * 初始化渲染主要做两件事情:
         * 1、渲染组件生成子树 subTree
         * 2、把 subTree 挂载到 container 中
         */
        if (!instance.isMounted) {
          // 1、渲染组件生成子树 vnode
          const subTree = (instance.subTree = renderComponentRoor(instance))
    
          // 2、把子树 vnode 挂载到 container 中
          patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
    
          // 保留渲染生成的子树根 DOM 节点
          initialVNode.el = subTree.el
          instance.isMounted = true
        }
        // 更新组件
        else {
          // ...
        }
      }, prodEffectOptions)
    }
    

    处理普通元素

    /**
     * 处理 ELEMENT
     */
    const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      isSVG = isSVG || n2.type === 'svg'
      // 旧节点为 null,说明没有旧节点,为第一次渲染,则挂载元素节点
      if (n1 == null) {
        mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      // 否则更新元素节点
      else {
        patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
    
    /**
     * 挂载元素
     * mountElement 主要做了四件事:
     * 1、创建 DOM 元素节点
     * 2、处理 props
     * 3、处理子节点
     * 4、把创建的 DOM 元素节点挂载到 container 上
     */
    const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      let el
      const { type, props, shapeFlag } = vnode
    
      // 1、创建 DOM 元素节点
      el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
    
      // 2、处理 props,比如 class、style、event 等属性
      if (props) {
        // 遍历 props,给这个 DOM 节点添加相关的 class、style、event 等属性,并作相关的处理
        for (const key in props) {
          if (!isReservedProp(key)) {
            hostPatchProp(el, key, null, props[key], isSVG)
          }
        }
      }
    
      // 3、处理子节点
      // 子节点是纯文本的情况
      if (shapeFlag & 8/* TEXT_CHILDREN */) {
        hostSetElementText(el, vnode.children)
      }
      // 子节点是数组的情况
      else if (shapeFlag & 16/* ARRAY_CHILDREN */) {
        mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
      }
    
      // 4、把创建的 DOM 元素节点挂载到 container 上
      hostInsert(el, container, anchor)
    }
    
    /**
     * 创建元素
     */
    function createElement(tag, isSVG, is) {
      // 在 Web 环境下的方式
      isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag, is ? { is } : undefined)
    
      // 如果是其他平台就不是操作 DOM 了,而是平台相关的 API,这些相关的方法是在创建渲染器阶段作为参数传入的
    }
    
    /**
     * 处理子节点是纯文本的情况
     */
    function setElementText(el, text) {
      // 在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本
      el.textContent = text
    }
    
    /**
     * 处理子节点是数组的情况
     */
    function mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) {
      // 遍历 chidren,获取每一个 child,递归执行 patch 方法挂载每一个 child
      for (let i = start; i < children.length; i++) {
        // 预处理 child
        const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))
    
        // 执行 patch 挂载 child
        // 执行 patch 而非 mountElement 的原因:因为子节点可能有其他类型的 vnode,比如 组件 vnode
        patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
    
    /**
     * 把创建的 DOM 元素节点挂载到 container 下
     * 因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点,最终挂载到最外层的容器上
     */
    function insert(child, parent, anchor) {
      // 如果有参考元素 anchor,则把 child 插入到 anchor 前
      if (anchor) {
        parent.insertBefore(child, anchor)
      }
      // 否则直接通过 appendChild 插入到父节点的末尾
      else {
        parent.appendChild(child)
      }
    }
    

扩展:嵌套组件

组件 vnode 主要维护着组件的定义对象,组件上的各种 props,而组件本身是一个抽象节点,它自身的渲染其实是通过执行组件定义的 render 渲染函数生成的子树 vnode 来完成,然后再通过 patch 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。

你可能感兴趣的:(Vue,前端,javascript,vue.js)