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

组件渲染

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 和 styleif (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}// 挂载新 vnodeconst { type, shapeFlag } = n2switch (type) {case Text:// 处理文本节点breakcase Comment:// 处理注释节点breakcase Static:// 处理静态节点breakcase Fragment:// 处理 Fragment 元素breakdefault:if (shapeFlag & 1/* ELEMENT */) {// 处理普通 DOM 元素processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)} else if (shapeFlag & 6/* COMPONENT */) {// 处理 COMPONENTprocessComponent(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、渲染组件生成子树 vnodeconst subTree = (instance.subTree = renderComponentRoor(instance))// 2、把子树 vnode 挂载到 container 中patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)// 保留渲染生成的子树根 DOM 节点initialVNode.el = subTree.elinstance.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 elconst { 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 方法挂载每一个 childfor (let i = start; i < children.length; i++) {// 预处理 childconst child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))// 执行 patch 挂载 child// 执行 patch 而非 mountElement 的原因:因为子节点可能有其他类型的 vnode,比如 组件 vnodepatch(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.hqwc.cn/news/170871.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

区块链拆分

随着区块链技术的发展和普及&#xff0c;去中心化钱包逐渐成为数字货币领域的重要工具。去中心化钱包不仅具有高度安全性和隐私保护能力&#xff0c;还可以通过智能合约和开源技术实现定制化功能。本文将探讨去中心化钱包定制开发的基本概念、优势、流程和前景。 一、去中心化钱…

SpringBoot实现Excel导入导出

SpringBoot实现Excel导入导出 在我们平时工作中经常会遇到要操作Excel的功能&#xff0c;比如导出个用户信息或者订单信息的Excel报表。你肯定听说过 POI这个东西&#xff0c;可以实现。但是POI实现的API确实很麻烦&#xff0c;它需要写那种逐行解析的代码&#xff08;类似Xm…

初始MySQL(五)(自我复制数据,合并查询,外连接,MySQL约束:主键,not null,unique,foreign key)

目录 表复制 自我复制数据(蠕虫复制) 合并查询 union all(不会去重) union(会自动去重) MySQL表的外连接 左连接 右连接 MySQL的约束 主键 not null unique(唯一) foreign key(外键) 表复制 自我复制数据(蠕虫复制) #为了对某个sql语句进行效率测试,我们需要海量…

redis之org.springframework.data.redis.RedisSystemException: Error in execution

背景 在运行某系统时&#xff0c;在测试类向redis中存入某值&#xff0c;然后取出。 一、遇到的问题 报错&#xff1a; org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: …

查看dll是32位还是64位

根据资料&#xff0c;用记事本打开dll文件&#xff1b;找到字符串 PE&#xff0c;其后不远如果出现L&#xff0c;是32位&#xff1b;字符串PE后出现 d? 是64位&#xff1b; 打开一个看一下&#xff1b;这个是32位&#xff1b; 这是从网上看的&#xff1b; 然后用dumpbin.exe工…

docker安装AWVS 23.9.231005181

本文声明仅AWVS用作学习使用 将镜像文件secfa_awvs.tar复制到目标机器上。 我的百度网盘文件路径&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Pe4qlVp9XKbZ3dLrouaP2w 提取码&#xff1a;67mc –来自百度网盘超级会员V6的分享 在目标机器上&#xff0c;使用以下命…

No195.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

Postgres的级数生成函数generate_series应用

Postgres的级数生成函数generate_series应用 引用&#xff1a;http://postgres.cn/docs/12/functions-srf.html 函数文档 函数 参数类型 返回类型 描述 generate_series(start, stop) int、bigint或者numeric setof int、setof bigint或者setof numeric&#xff08;与参数类型相…

为什么Android 手机这么慢?如何提高 Android 手机的运行速度

速印机&#xff08;理想、荣大等&#xff09;、复印机&#xff08;夏普、东芝、理光、佳能、震旦等全系列&#xff09;、打印机、扫描仪、传真机、多媒体教学一体机、交互式电子白板、报警器材、监控、竞业达监考设备及其它监考设备、听力考试设备、特种安防设备维护及维修。吴…

时间序列预测:深度学习、机器学习、融合模型、创新模型实战案例(附代码+数据集+原理介绍)

本文介绍->给大家推荐一下我的时间序列预测实战专栏&#xff0c;本专栏平均质量分98分&#xff0c;而且本专栏目前免费阅读。其中涉及机器学习、深度学习、融合模型、个人创新模型、数据分析等一系列有关时间序列的内容&#xff0c;其中的实战案例不仅有简单的模型类似于机器…

EXIT(1)

EXTI介绍 EXTI是片上外设 NVIC是cpu内的外设 回忆起之前的GPIO和AFIO 我们是如何检测按键按下的 我们是一直用while循环读取IDR寄存器的对应位置的值 一直检测判断按键是否被按下 那么是否有第二种方式检测按键是否被按下了呢&#xff1f; 通过EXTI 当EXTI检测到按键的电平发生…

飞书开发学习笔记(五)-Python快速开发网页应用

飞书开发学习笔记(五)-Python快速开发网页应用 一.下载示例代码 首先进入飞书开放平台: https://open.feishu.cn/app 凭证与基础信息 页面&#xff0c;在 应用凭证 中获取 App ID 和 App Secret 值。 教程和示例代码位置:https://open.feishu.cn/document/home/integrating-…