乐趣区

关于前端:Vue3-渲染器分析

在上一篇中,咱们晓得 render 函数最终是通过 baseCreaterenderer 创立的。

当通过 createApp API 创立的组件实例调用mount 办法挂载组件的时候,其实 mount 办法也是通过调用 render 办法。

实现组件的渲染工作。

这篇文章次要剖析是对 baseCreateRender 函数源码进行剖析。

baseCreateRenderer函数的整体代码大略有一千多行。

蕴含的信息相当丰盛。

纵向扩大,能够学习到 Vnodepatch过程、虚构 DOMdiff形式、指令的调用形式。

深度扩大,能够学完 template 的解析、转换与生成,任务调度器的执行过程、甚至响应式零碎。

还有就是写完,能够补上后面好多留的坑😂。

这次咱们先纵向学习,理解该函数在 Vue 中次要做了什么。前面在逐渐深刻。

前文回顾

上篇文章中,咱们晓得 app 实例是通过 createApp API 创立的,createAppcreateRenderer函数返回的对象中的 app 属性做了一些解决之后。再返回给用户。

createRenderer 其实调用的是 baseCreateRenderer 函数,并给 baseCreateRenderer 函数传递了一个用于配置渲染器的 options 对象。

这个 options 对象中蕴含了 DOM 的解决办法 & 属性的 patch 办法。

baseCreateRenderer 函数返回的对象中,蕴含 render 渲染函数、hydrate用于服务端渲染的注水函数、createApp函数。

Vue3顺带的将 render 办法设定为API,不便高阶玩家自由发挥。

当咱们调用 app 实例上的 mount 办法时。

会依据挂载的组件创立对应的Vnode

Vnode、挂载元素el 传给 render 函数。

最终通过 render 函数实现组件的渲染工作。

解构配置项

为了不便外部 patch 函数的应用,baseCreateRenderer函数首先对 options 进行了解构.

options次要蕴含的办法是对 DOM 的创立、插入、挪动、设置、获取父节点、克隆节点、patch属性等办法。

这里咱们须要先简略相熟下:

  insert: hostInsert,
  remove: hostRemove,
  patchProp: hostPatchProp,
  forcePatchProp: hostForcePatchProp,
  createElement: hostCreateElement,
  createText: hostCreateText,
  createComment: hostCreateComment,
  setText: hostSetText,
  setElementText: hostSetElementText,
  parentNode: hostParentNode,
  nextSibling: hostNextSibling,
  setScopeId: hostSetScopeId = NOOP,
  cloneNode: hostCloneNode,
  insertStaticContent: hostInsertStaticContent

渲染逻辑

在组件生命周期中,首次挂载会触发 mounted 钩子。

后续如果状态产生变换,会触发 beforeUpdateupdated 钩子。

这其实与渲染函数 render 无关。

render函数首先会判断 Vnode 是否存在。

如果不存在阐明须要执行进行卸载,执行 unmount 操作。

如果存在须要进行 patch 操作。

patch的过程就蕴含了组件了创立到挂载,变动到更新。

 const render = (vnode, container, isSVG) => {if (vnode == null) {
      // 如果没有 Vnode,则卸载原来的 Vnode
      if (container._vnode) {unmount(container._vnode, null, null, true)
      }
    } else {
      // 存在则对新旧 Vnode 进行 patch
      // patch 是一个递归的过程
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    // patch 完结后,开始冲刷任务调度器中的工作
    flushPostFlushCbs()
    // 更新 vnode
    container._vnode = vnode
  }

从代码来看,render函数的逻辑并不简单。

render函数的设计思维,根本就代表了 vue 解决各种类型节点的形式;

  • 首先会判断 Vnode 是否存在,如果不存在,则调用 unmount 函数,进行组件的卸载
  • 否则调用 patch 函数,对组件进行patch
  • patch 完结后,会调用 flushPostFlushCbs 函数冲刷工作池
  • 最初更新容器上的Vnode

patch Vnode

Vnode有不同的类型,在这里我将其分为:

  • 简略类型:文本、正文、Static
  • 简单类型:组件、FragmentComponentTeleportSuspense

patch思路,能够看作一个深度优先遍历。与深度克隆的逻辑十分类似。

简略类型就相当于 JS 中的原始数据类型:字符串、数字、布尔。

简单类型就相当于 JS 中的援用类型:对象、数组、Map、Set。

不同的节点类型,须要采取不同的 patch 形式。

patch 函数的主要职责就是去判断 Vnode 的节点类型,而后调用对应类型的 Vnode 解决形式,进行更粗疏的patch

上面咱们看下 patch 函数是如何解决的。

为了升高 patch 函数的了解难度,上面的流程图体现的是 patch 处理过程中的次要逻辑,并没有将所有细节记录在图中。

Text 类型

  • 匹配到 Text 类型Vnode
  • 会调用 ProcessText 函数对节点进行解决。
  • ProcessText函数首先会判断 n1 是否存在。
  • 不存在,阐明是第一次执行,间接进行文本插入。
  • 新旧,新旧文本不同,会设置新的Text

Comment 类型

  • 匹配到 Comment 类型Vnode
  • 调用 processCommentNode 函数
  • 如果 n1 不存在,则执行插入工作
  • 否则间接新的笼罩旧的,因为正文节点并不需要在页面中进行展现,不用做多余的渲染工作

Static 类型

  • 咱们晓得 Vue3 的性能晋升,有局部起因就是得益于对动态节点的解决。
  • patch过程中,匹配到 Static 类型节点。
  • 如果 n1 不存在,会调用mountStaticNode,对动态节点进行挂载操作。
  • 如果是 dev 环境,会调用 patchStaticNode 函数,patch节点。
  • 为什么仅在 dev 环境中进行 patch 呢,因为 dev 环境下波及到HMR
  • 另外动态节点不存在对 state 的依赖,不会触发tracktrigger。且放弃不变,在生产环境下,不用进行patch。以升高性能开销。

Fragment 类型

  • 匹配到 Fragment 类型节点。
  • 会调用 processFragment 函数,进行解决。
  • Fragment节点,FragmentVue3 中新增的 Fragment 组件,能够包裹多个子节点,然而并不会渲染 Fragment 节点。
  • 所以在渲染过程中次要解决的是 Fragmemt 包裹的子节点。
  • 如果 n1 不存在,会执行mountChildren,对子节点进行挂载。

    • mountChildren会对子节点进行遍历操作,递归调用 patch 函数。
  • 如果 n1 存在,会对子节点再进行进一步的判断

    • 如果 patchFlag 存在 && 存在动静节点
    • 则会调用patchBlockChildren,对子节点进行patch
    • patchBlockChildren会遍历子节点,递归调用 patch 函数
    • 否则会调用 patchChildren 函数,对子节点进行patch
    • patchChildren在执行的过程中波及到了 DOMdiff过程,这里临时不开展剖析,前面会出独自进行剖析

Element 类型

  • 匹配到 Element 类型
  • 会调用 processElement 函数
  • n1不存在,会执行 mountElement 函数,对 Vnode 进行挂载

    • mountElement在挂载 Vnode 过程中,会通过mountChildren,对子节点进行递归挂载解决。
    • 并会对 Vnodeprop进行patch
    • 并调用 queuePostRenderEffect 函数,向任务调度池中的后置执行阶段 push 生命周期钩子mounted
  • 否则会执行 patchElement 函数,对 element 进行 patchpatchElement 函数次要会执行以下工作:

    • 调用 hostPatchProp 对节点的 classstyle 进行patch
    • 遍历 props 对节点的新旧 props 进行patch

      • 调用 patchBlockChildren 或者 patchChildren 进行 patch 操作
      • 并调用 queuePostRenderEffect 函数,向任务调度池中的后置执行阶段 push 生命周期钩子updated
      • 这里须要对子节点解决的起因是因为 Element 的子节点中,也可能还有组件或者其余类型的节点

Component 类型

  • 通常状况下,咱们都会给 createApp 传递一个组件
  • 故当 render 函数执行 patch 时,首先会匹配到组件类型的节点
  • 如果是组件类型,会调用 processComponent 函数进行解决
  • 首先会判断 n1 是否存在
  • 如果存在会进一步判断

    • 该组件是否是被 Keep-Alive 包裹的组件
    • 如果是,则会执行组件的 activate 钩子
    • 否则会调用 mountComponent 函数,对组件进行挂载
    • mountComponent函数波及的层级较深,这里先不开展说,然而要晓得以下几点:

      • 会实现组件实例的创立
      • 实现 PropsSlots 的初始化
      • 执行 setup 函数,获取响应式状态
      • 实现组件模板的解析、编译与转换
      • 调用 setupRenderEffect 创立一个 渲染级别的effect
      • 用于负责组件的更新,这里我临时将其称为updateEffect
  • 否则会执行 updateComponent 函数,判断组件是否须要进行更细

    • 次要会对组件的新旧Props、子节点进行判断
    • 如果发生变化,会调用 mountComponent 阶段创立的updateEffect,触发响应式零碎
    • 否则间接原有的间接进行笼罩

Teleport 类型 & Suspense 类型

  • TeleportSuspenseVue3 新增的两个内置组件
  • 如果匹配到以上两种,会调用组件实例上的 process 办法
  • porcess办法的次要逻辑与后面的雷同
  • 首先会判断原有 Vnode 是否存在,不存在则mount,存在则patch
  • 这两种类型的具体解决形式,咱们会在剖析这两个组件的源码的时候会进行剖析

卸载组件

  • 如果调用 render 函数时没有传 Vnode,则会调用unmount 函数对组件进行卸载
  • 卸载过程中,如果存在ref,会首先重置ref
  • 如果组件是通过 Keep-Alive 缓存的组件,会通过 deactivate 对组件进行卸载
  • 如果是组件类型 Vnode,会通过unmountComponent 函数对组件进行卸载

    • 在卸载组件过程中会执行 beforeMount 生命周期钩子
    • 通过stop API 来卸载组件的所有相干effect
    • 如果存在 updateEffect,会卸载updateEffect,并递归调用unmount 函数,对组件进行卸载
    • 最初会执行 unmount 生命周期钩子
    • 并通过 queuePostRenderEffect 向任务调度器中的后置工作池中,push一个用于标记组件已实现卸载的函数
    • 至此,就实现了组件的卸载工作
  • 如果不是组件类型的Vnode,会有以下几种状况:

    • 如果是 Suspense 类型,会通过 Suspense 实例上的 unmount 办法实现 Vnode 的卸载工作
    • 如果是 Teleport 类型,会通过 Teleport 实例上的 remove 办法实现 Vnode 的卸载工作
    • 如果存在子组件,会通过 unmountChildren 实现子组件的卸载工作
    • 最初会调用 remove 函数实现 FragmentStaticElement 类型的卸载工作

从下面整个过程能够看出,卸载组件过程根本与 patch 形似,也是对各种类型的 Vnode 有不同的解决办法,并会通过 递归 调用 unmount 实现组件的卸载工作,卸载过程中,会卸载组件相干的effectupdateEffect,触发卸载相干的生命周期钩子 & 指令相干的钩子。

总结

通过下面的梳理剖析,能够晓得,对于所有类型的组件,patch过程十分类似。

首先会判断原有的 vnode 是否存在。

如果不存在,则会进行 mount 操作。

如果存在则会对新旧 Vnode 进行 patch 操作。

不同的是对于简单类型的 Vnode,因为其外部可能蕴含有其余类型的Vnode,比方Component 类型。其中会波及到:

  • 组件实例的创立
  • 模板的编译工作
  • 子组件的递归 patch 工作等等

unmount 过程中,同样的会对不同的组件类型进行解决,并卸载组件的所有相干effect,递归卸载子组件。

不过没有提及的是下面的两个过程中,都会向任务调度器中 push 工作。

在 render 函数执行的最初阶段,会通过 flushPostFlushCbs 冲刷任务调度器,对于任务调度器是如何运作的,能够移步这里👉任务调度器源码剖析

其实写到这里还有两个问题没有说:

第一个问题:还有哪些没有说?

baseCreateRenderer 蕴含的内容切实是太多了,要想一篇就剖析完并输入,对于读者和我都是一种考验。所以本文只是纵向的对 baseCreateRenderer 进行了剖析,并没有深究细节。

比拟重要的有几点:

  • 子节点的 diff 过程
  • 组件类型编译过程、响应式转换过程
  • updateEffect 做了什么
  • 如何进行指令的生命周期钩子调用
  • 生命周期的执行过程
  • 往任务调度器中都 push 了哪些工作
  • 如何设计的性能监控零碎

等等细节之处都是咱们还没有说的。不过前面咱们会持续。

第二个问题:baseCreateRenderer 这么简单,我或者你费了这么大劲读了有什么用?

上面我说说我的感触:

  1. 在工作中,很大概率下没啥用。因为 Vue 通过 createRenderer 曾经做了很全面的配置,创立的 render 函数,曾经能满足工作需要。
  2. 如果是高阶玩家,比如说我想用 createRenderer 做个 Vnode 渲染引擎,可能有帮忙。只须要通过配置 Options。就能够创立一个定制化的渲染器。并且这个渲染器曾经蕴含了 Vnode 的 diff 零碎、编译系统。

最初非常感谢各位的浏览,如果文章有疏漏之处,还望批评指正。

如果有所播种,能够帮我点个关注,我会继续更新 Vue 的相干学习分享😁!

退出移动版