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

news2025/1/16 13:44:54

组件渲染

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

vnode 的优点:

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

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

组件的渲染流程:

在这里插入图片描述

  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 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1197526.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

漏刻有时百度地图API实战开发(4)显示指定区域在移动端异常的解决方案

漏刻有时百度地图API实战开发(1)华为手机无法使用addEventListener click 的兼容解决方案漏刻有时百度地图API实战开发(2)文本标签显示和隐藏的切换开关漏刻有时百度地图API实战开发(3)自动获取地图多边形中心点坐标漏刻有时百度地图API实战开发(4)显示指定区域在移动端异常的解…

Linux文件系统——重定向

文章目录 1. 文件描述符分配规则2. 重定向接口dup2自定义shell重定向(补充) 3. 标准输出和标准错误4. 如何理解一切接文件 本章代码gitee地址&#xff1a;文件重定向 1. 文件描述符分配规则 文件描述符的分配规则是从0下标开始&#xff0c;寻址最小的没有使用的数组位置&#…

飞机社交软件开发:重新定义社交媒体的空中交互体验

【导语】 随着互联网技术的快速发展&#xff0c;社交媒体平台的界限也逐渐模糊。飞机社交软件应运而生&#xff0c;打破传统的地面社交模式&#xff0c;为空中旅行的旅客提供全新的交流平台。本文将从市场需求、技术实现、用户体验和未来发展等方面&#xff0c;深入探讨飞机社交…

探索内存函数的奥秘【memcpy、memmove、memset、memcmp】

目录 一&#xff0c;memcpy函数 1&#xff0c;memcpy函数简介 2&#xff0c;memcpy函数的原理 3&#xff0c;memcpy函数的用法 4&#xff0c;注意事项 5&#xff0c;memcpy函数模拟实现 二&#xff0c;memmove函数 1&#xff0c;memmove函数简介 2&#xff0c;memmove函…

文件夹批量改名技巧:简单步骤,实现文件夹随机重命名

在日常生活和工作中&#xff0c;经常需要处理大量的文件夹&#xff0c;需要对其进行有效的管理。在这些情况下&#xff0c;文件夹的命名就变得非常重要。一个好的命名策略可以快速找到所需的文件夹&#xff0c;也可以帮助更好地组织文件。然而&#xff0c;手动为每个文件夹重命…

Ocelot:.NET开源API网关提供路由管理、服务发现、鉴权限流等功能

随着微服务的兴起&#xff0c;API网关越来越常见。API网关是连接应用程序和用户之间的桥梁&#xff0c;就像一个交通指挥员&#xff0c;负责处理所有进出应用的数据和请求&#xff0c;确保安全、高效、有序地流通。 今天给大家推荐一个.NET开源API网关。 01 项目简介 Ocelot…

通过easyexcel导出数据到表格

这篇文章简单介绍一下怎么通过easyexcel做数据的导出&#xff0c;使用之前easyui构建的歌曲列表crud应用&#xff0c;添加一个导出按钮&#xff0c;点击的时候直接连接后端接口地址&#xff0c;在后端的接口完成数据的导出功能。 前端页面完整代码 let editingId; let request…

智链引擎CEO李智:游戏化增长中台,让裂变营销快十倍、便宜十倍、好十倍丨数据猿专访...

大数据产业创新服务媒体 ——聚焦数据 改变商业 双十一电商大战一触即发&#xff0c;各个垂类的App也都希望能够借力双十一营销季&#xff0c;实现用户和营收双增长。MarTech在这个风口上&#xff0c;又成为2B赛道关注的焦点。 业内人士指出&#xff0c;MarTech的引入催生营销…

毅速丨金属3D打印零件品质受哪些因素影响

金属3D打印的零件品质受到多个因素的影响&#xff0c;包括设备、材料、工艺、后处理等。不同厂商的品质差别大致由以下几个方面造成。 一、设备性能差异&#xff1a; 不同厂商的金属3D打印设备可能存在性能上的差异&#xff0c;包括激光功率、扫描速度、打印精度、稳定性等。这…

【教3妹学编程-算法题】765. 情侣牵手

3妹&#xff1a;2哥2哥&#xff0c;你看到新闻了吗&#xff1f;襄阳健桥医院院长 公然“贩卖出生证明”&#xff0c; 真是太胆大包天了吧。 2哥 : 我也看到新闻了&#xff0c;7人被采取刑事强制措施。 就应该好好查查他们&#xff0c; 一查到底&#xff01; 3妹&#xff1a;真的…

Python:词法分析(行结构与显式、隐式行拼接)

相关阅读 Pythonhttps://blog.csdn.net/weixin_45791458/category_12403403.html?spm1001.2014.3001.5482 1、逻辑结构 一个Python程序由许多逻辑行组成&#xff0c;字面意义上的一行指的是末尾有换行符(\n)&#xff0c;但在不同的情况下&#xff0c;行末尾的换行符(\n)可能有…

如何选择一个可靠的爬虫代理服务商?技术人员都需要知道

我身边从事大数据相关行业的朋友最近告诉我&#xff0c;自己新招的小伙伴工作效率很低&#xff0c;很多最基础的工具都不会选择&#xff0c;经常因为代理IP不可靠导致工作出错。 听完这些我才意识到&#xff0c;在这个大数据时代&#xff0c;还是有很多新手在进行网络爬取任务…

Elasticsearch 和 Go 中使用向量搜索寻找地鼠

作者&#xff1a;CARLY RICHMOND&#xff0c;LAURENT SAINT-FLIX 就像动物和编程语言一样&#xff0c;搜索也经历了不同实践的演变&#xff0c;很难在其中做出选择。 加入我们的第二部分&#xff0c;通过 Elasticsearch 中的矢量搜索在 Go 中狩猎地鼠&#xff08;gophers&…

沁恒微WCH592程序烧录问题

在使用wch592蓝牙芯片时&#xff0c;使用WCHISPStudio_V3.60工具烧录hex固件时&#xff0c;识别设备OK&#xff0c; 擦除flash OK&#xff0c;就是在烧录时一直报错&#xff0c;错误如下&#xff1a; 原因是:代码和数据保护模式没有启用。 改为如下&#xff1a;

Git可视化界面的操作,SSH协议的以及IDEA集成Git

目录 一. Git可视化界面的操作 二. gitee的ssh key 2.1 SSH协议 2.2 ssh key 三. IDEA集成Git 3.1 分享项目 3.2 下载项目 一. Git可视化界面的操作 上一篇博客只用到了git的命令窗口&#xff0c;现在就来看看可视化窗口要怎么操作。 点击Git GUI Here GUI界面 在g…

由于找不到 d3dx9_43.dll,无法继续执行代码。重新安装程序可能会解决此问题

电脑出现d3dx9_43.dll缺失的问题&#xff0c;通常是由于DirectX组件未安装或损坏导致的。为了解决这个问题&#xff0c;我为您提供了以下四个解决方法&#xff1a; d3dx9_43.dll解决方法1. 使用dll修复程序修复 首先&#xff0c;使用系统文件程序dll进行修复操作非常简单&…

ZYNQ_project:IP_ram_pll_test

例化MMCM ip核&#xff0c;产生100Mhz&#xff0c;100Mhz并相位偏移180&#xff0c;50Mhz&#xff0c;25Mhz的时钟信号。 例化单口ram&#xff0c;并编写读写控制器&#xff0c;实现32个数据的写入与读出。 模块框图&#xff1a; 代码&#xff1a; module ip_top(input …

人工智能与养老:技术助力银色产业的崛起

人工智能与养老&#xff1a;技术助力银色产业的崛起 随着人口老龄化的加速推进&#xff0c;养老问题成为了全球关注的热点。人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;为养老领域注入了新的活力。本文将探讨人工智能在养老领域的应用、关键挑战以及前景展望…

计算机毕业设计:水果识别检测系统 python 深度学习 YOLOv5

[毕业设计]2023-2024年最新最全计算机专业毕设选题推荐汇总 感兴趣的可以先收藏起来&#xff0c;还有大家在毕设选题&#xff0c;项目以及论文编写等相关问题都可以给我留言咨询&#xff0c;希望帮助更多的人 。 1、项目介绍 本文介绍了一种基于深度学习的水果检测与识别系统…

【解决方案】pytion 运行时提示 import psutil ModuleNotFoundError: No module named ‘psutil‘

报错原因分析 import psutil ModuleNotFoundError: No module named psutil报错原因分析 当前环境pytion中缺少了psutil包&#xff0c;使用pip命令进行安装 解决方案 pip install psutil