关于前端: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的相干学习分享😁!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理