vue2模板渲染更新详细流程

news2024/11/26 18:21:51

vue2模板渲染更新详细流程

此文章基于vue2.6.10版本进行解析,在看文章最好结合源码一起看能帮助更快的理解。

vue中会将.vue文件或者template属性解析成一个render函数,在渲染(调用$mount方法)的时候通过执行这个render函数生成真实节点,再通过_update()方法将真实节点挂载到页面上实现最终的渲染。下面来看看具体的流程。

在初次渲染时,调用mountComponent方法组成一个updateComponent方法,并把这个方法传递到当前实例(vm)的watcher上,同时根据是否是懒加载来判断要不是直接先渲染一次。当当前实例中依赖的数据发生改变后就会通知到watcher并触发updateComponent方法实现重渲染。

相关代码:

// src/core/instance/lifecycle mountComponent
updateComponent = () => {
    vm._update(vm._render(), hydrating);
};

// 将updateComponent传递到Watcher实例中
new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true
);

_render函数是通过.vue文件里的模板解析生成的一个函数,我们只需要知道render()的返回值是一个虚拟节点(Vnode),我们现在暂时不关注render是怎么解析生成vnode

我们把重点放在_update方法上。在源码的src/core/instance/lifecycle.js中的lifecycleMixin方法中定义了一个Vue.prototype._update方法。

在这个方法中,调用了__patch__方法将vnode生成真实节点并挂载到页面上(或者在旧节点上进行更新)

// _update 逻辑
if (!prevVnode) {
  // 第一次初始化,没有旧虚拟节点
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
  // 更新节点
  vm.$el = vm.__patch__(prevVnode, vnode);
}

这个__patch__方法是createPatchFunction方法的返回值,这是因为vue兼容了webweex两个平台(传入不同的渲染相关的方法,暴露一个相同的调用方法),需要使用不同的方法来处理不同平台的渲染(比如说创建一个文本节点,web是通过document.createTextNode,而weex的则是new TextNode)。
我们这里只看web端的:

// src/plaforms/web/runtime/patch.js
const patch = createPatchFunction({ nodeOps, modules })

createPatchFunction中传入了node的操作方法、标签中一些属性处理以及事件监听器的处理方法,通过闭包的方式将这些操作方法保存下来给返回值调用。

function createPatchFunction(backend) {
 // 通过闭包方法保存提供给其他方法使用。
 const { modules, nodeOps } = backend;
 ...
 return function patch (oldVnode, vnode, hydrating, removeOnly) {
   ...
 }
}

我们先把钩子相关的代码先忽略,先重点看看是怎么生成的真实节点并挂载到页面上以及是页面怎么进行更新的。

我们分以下几种情况来逐步剖析patch方法:

1. 首次渲染

// 在前面的_update方法里提到这是第一次初始化时的传参,
vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);

可以看到传入的是vm。$el,也就是我们在初始化vue需要挂载的那个元素。
为了之后的方法可以统一传参类型,因此特地为这个真实节点也创建了一个vnode

if(isRealElement) {
  oldVnode = emptyNodeAt(oldVnode);
}

在给真实节点创建了一个vnode后,可以也看成是下面更新渲染中非同一种vnode的类型,vue内部的处理逻辑也是这样的,所以首次渲染这个部分在单独为真实节点创建了一个vnode后,后续的讲解可以跳转到更新渲染中非同个vnode继续。

2. 更新渲染

这种情况下的更新也分两种,更新前后是否是同一种vnode

src/core/vdom/patch.js中的sameVnode方法判断是否是同一种vnode

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

从上面的函数可以看出:

  • key是前提,key不相同肯定就不算同一个vnode
  • 相同标签名,isComment属性相同,标签中必须同时存在属性或者同时不存在属性(styleclass这些),如果标签是input类型,input type可以有不同,不过必须都是文本类型的输入(textpasswordemail等)。
  • 对于异步占位符的vnode,就需要判断异步的工厂函数是否相同。

同一种vnode

如果是同一种vnode,直接调用patchVnode进行新旧节点的比对更新(不需要处理当前节点,直接处理子节点即可)。

patchVnode中的关于DOM更新的算法是基于Snabbdom的。
来看看具体是怎么处理的:

// patchVnode
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
  // 新旧子节点都存在
  if (isDef(oldCh) && isDef(ch)) {
  // 新旧子节点不相同
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  } else if (isDef(ch)) {
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') 
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  } else if (isDef(oldCh)) {
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, '')
  }
// 如果要比对的新节点是文本节点,直接通过setTextContent替换文本内容即可(文本节点不存在子节点)。
} else if (oldVnode.text !== vnode.text) {
  nodeOps.setTextContent(elm, vnode.text)
}

如果新节点是非文本节点,则可能具有以下几种可能:

  1. 新旧节点都是存在子节点。
  2. 只有新节点存在子节点。使用createElm创建了对应的DOM后挂载到父元素上。
  3. 只有旧节点存在子节点。使用removeNode方法移除旧节点的子节点。
  4. 新节点是空节点,旧节点是文本节点。使用setTextContent把旧节点的值改为空值
  5. 新旧节点都是空节点。这个不需要处理,因为没有什么影响

重点需要看新旧节点都存在子节点的情况(也就是updateChildren方法):

patchVnode到现在要讲解的updateChildren方法都是在同一层级进行比较的,不会跨层级比较。这样只比较同层级的方式时间复杂度可以降低到O(n)

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      ...
    } else if (isUndef(oldEndVnode)) {
      ...
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      ...
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      ...
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      ...
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
    } else {
      ...
    }
  }
}

上面只把核心的条件代码展示出来,我们通过下面例子来更好的理解以上条件处理逻辑:
在这里插入图片描述

假设图中的每个字母代表一个节点(没有子节点),相同的字母代表是相同节点。

当第一次循环时,前面的条件都不满足,但是都存在节点B,执行else里的逻辑

// else 里的逻辑
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  idxInOld = isDef(newStartVnode.key)
    ? oldKeyToIdx[newStartVnode.key]
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

先看看这几行代码:

将旧子节点按照节点内的key值与索引建立映射并把这个map保存下来,如果当前循环中新子节点的存在key值,那么就可以直接映射到旧子节点列表中的索引值,直接找到旧的节点。

如果没有key值,那么每次循环都需要遍历旧子节点(oldStartIdx - oldEndIdx)去与当前循环中的新子节点进行比对判断是否是相同节点(非常影响性能,这也就是为什么v-for指令都需要带key进行标识

那么根据上面的对比结果,我们可以非常容易猜到下面肯定就是对有无索引值两种情况(旧子节点列表中存不存在与当前循环中的新子节点相同的节点)进行处理:

// else 里的逻辑

// 没有索引值
  if (isUndef(idxInOld)) {
    // 说明这个节点是新的,需要调用createElm生成一个DOM
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  } else {
  // 有
    vnodeToMove = oldCh[idxInOld];
    // 存在相同key不代表一定是相同节点,同样需要判断一下
    if (sameVnode(vnodeToMove, newStartVnode)) {
      // 继续对比这俩子节点
      patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // 移除索引值对应的节点(只是移除vnode中的,真实DOM仍存在)
      oldCh[idxInOld] = undefined
      // 把比对结束后的子节点插入到旧子节点的起始指针对应节点的前一位(位置要按照新子节点列表的顺序排列)
      canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    } else {
      // 生成一个DOM
      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    }
    // 移动指针
    newStartVnode = newCh[++newStartIdx]
  }

在这里插入图片描述

使用insertBefore方法会将节点从一个位置移动插入到新位置,原位置上节点相关的关系会重新建立

第二次循环,满足sameVnode(oldStartVnode, newStartVnode)条件,都是A

// sameVnode(oldStartVnode, newStartVnode) 条件逻辑
// 继续对比节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // 新旧起始指针右移
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]

这是只需要比对这两个节点,然后移动新旧节点的起始指针:

在这里插入图片描述

第三次循环,满足isUndef(oldStartVnode)的条件:

// isUndef(oldStartVnode) 条件逻辑:
oldStartVnode = oldCh[++oldStartIdx]

就只有一个移动起始指针的逻辑:

在这里插入图片描述

第四次循环,满足sameVnode(oldStartVnode, newEndVnode)的条件,都是C

// sameVnode(oldStartVnode, newEndVnode)  条件逻辑
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// 移动指针
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]

同样对比节点后插入到DOM中,然后移动指针:

这里把节点C插入到E

在这里插入图片描述

第五次循环,满足sameVnode(oldEndVnode, newStartVnode)条件,都是E

patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]

同样对比节点后插入到DOM中,然后移动指针:

在这里把节点E插到G前面

在这里插入图片描述

第六次循环,满足sameVnode(oldEndVnode, newEndVnode)的条件,都是D

// sameVnode(oldEndVnode, newEndVnode) 条件逻辑
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 移动指针
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]

同样对比节点后插入到DOM中,然后移动指针:

在这里插入图片描述

当前渲染的节点为BAEGDC,比我们的新子节点要多一个节点。

此时newEndIdx < newStartIdx,中止循环,执行updateChildren方法最后一段代码:

// 新子节点比旧子节点多
if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
// 旧子节点比新子节点多
} else if (newStartIdx > newEndIdx) {
  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}

移除G节点,所以最终生成的节点为 BAEDC

其实updateChildren方法看似复杂,不过是if判断多了几个,核心都是比较两个节点,然后在遍历对比子节点进行更新。

非同一种vnode

不是同一种的话就要通过createElm生成真实节点,在新节点挂载后把旧节点移除掉。

const oldElm = oldVnode.elm      
const parentElm = nodeOps.parentNode(oldElm)
// 生成新节点
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)

// 移除旧节点
if (isDef(parentElm)) {
  removeVnodes(parentElm, [oldVnode], 0, 0)
}

createElm方法中可以看到就是通过document.createElement来创建新元素,而后通过xxx.appendChild或者xxx.insertBefore方法把节点挂载到页面上。

function createElm( 
  vnode,
  insertedVnodeQueue,
  parentElm,
  // 旧节点的下一个兄弟节点
  refElm,
  nested,
  ownerArray,
  index
) {
  // 当是组件的情况下,createComponent就会在根据传入的参数生成组件实例,经过$mount方法选渲染成组件后返回true
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ...
  // 生成真实节点
  vnode.elm = vnode.ns
    ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode)
  // 创建子元素
  createChildren(vnode, children, insertedVnodeQueue)
  // 挂载
  insert(parentElm, vnode.elm, refElm)
}

对于普通标签而言就是通过createElement创建,但是组件则是需要使用createComponent方法解析后才能插入文档。

createComponent方法通过调用组件钩子init方法创建一个组件实例并通过$mount方法创建了真实节点后通过xxx.appendChild或者xxx.insertBefore方法插入到文档中。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  ...
  // 组件实例相关的生成看下面渲染相关钩子部分
  if (isDef(vnode.componentInstance)) {
    // 把组件实例上的真实节点赋值到vnode.elm中
    initComponent(vnode, insertedVnodeQueue)
    // 挂载
    insert(parentElm, vnode.elm, refElm)
    if (isTrue(isReactivated)) {
      reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    }
    return true
  }
}

至此,渲染相关的部分其实已经执行完了,也就是页面上已经渲染好节点了。

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

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

相关文章

#systemverilog# 关于流操作符>>和<<引发的思考

前言 对于流操作符&#xff0c;相比大家都不陌生&#xff0c;在实际项目中运用特别广泛。今天我们通过几个小例子&#xff0c;来回顾和深层认识一下该操作符。 概念 流操作符&#xff08;bit-stream&#xff09;&#xff0c;表示方式为{>>{}} 和 {<<{}}。前者会…

spring源码 IoC 之xml配置bean注册解析的 BeanDefinitions

概述 IoC 容器的初始化过程分为三步骤&#xff1a;Resource 定位、BeanDefinition 的载入和解析&#xff0c;BeanDefinition 注册 1、Resource 定位。我们一般用外部资源来描述 Bean 对象&#xff0c;所以在初始化 IoC 容器的第一步就是需要定位这个外部资源 2、BeanDefinition…

opencv4 傅里叶变换

傅里叶变换 ① 高频&#xff1a;变化剧烈的灰度分量&#xff0c;例如边界礁石。 ② 低频&#xff1a;变化缓慢的灰度分量&#xff0c;例如一片大海。 ③ 高通滤波器&#xff1a;只保留高频&#xff0c;会使得图像细节增强。高频边界锐化了&#xff0c;增强了&#xff0c;细节…

【群智能算法改进】一种改进的算术优化算法 改进算术优化算法 改进AOA[2]【Matlab代码#38】

文章目录 【获取资源请见文章第5节&#xff1a;资源获取】1. 原始AOA2. 改进后的MAOA算法2.1. Kent映射种群初始化2.2 复合摆线法优化MOA参数2.3 种群top20%精英变异和Cauchy变异组合2.3.1 麻雀精英变异2.3.2 柯西变异 3. 部分代码展示4. 仿真结果展示5. 资源获取说明 【获取资…

高性能软件负载OpenResty整合Reids集群配置

目录 1 OpenResty整合Reids集群配置1.1 下载安装lua_resty_redis1.1.1 连接Redis集群封装1.1.2 配置lua脚本路径1.1.3 测试脚本 1.2 请求参数封装1.2.1 测试脚本 1.3 抓取模板内容封装1.3.1 下载安装lua-resty-http1.3.2 测试脚本 1.4 模版渲染配置1.4.1 下载安装lua-resty-tem…

基于RK3399/RK3588 H.265/HEVC的低延迟视频传输系统设计与实现

近年来&#xff0c;随着短视频直播的兴起&#xff0c;视频传输设备在生活中的应用越发普及。人们对图像 清晰度、帧率、码率等技术指标的要求不断提高&#xff0c;视频帧所包含的数据量也在急速增加。在 有限的网络带宽下&#xff0c;传统的视频采集设备面临压缩率不足、帧率…

Ui自动化测试如何上传文件

前言 实施UI自动化测试的时候&#xff0c;经常会遇见上传文件的操作&#xff0c;那么对于上传文件你知道几种方法呢&#xff1f;今天我们就总结一下几种常用的上传文件的方法&#xff0c;并分析一下每个方法的优点和缺点以及哪种方法效率&#xff0c;稳定性更高 被测HTML代码…

python基础知识(九):函数

目录 1. 函数的定义2. 传递实参2.1 位置实参2.2 关键字实参2.3 默认值2.4 传递任意数量的实参2.5 结合使用位置实参和任意数量实参 3. 导入模块3.1 导入特定的函数3.2 使用 as 给函数指定别名3.3 使用 as 给模块指定别名3.4 导入模块中的所有函数 1. 函数的定义 函数的定义形式…

Wijmo 5.20231.888 JavaScript UI Crack

Wijmo使用更快、更灵活的 JavaScript UI 组件构建更好的应用程序 使用 Wijmo&#xff0c;利用我们引人注目的 UI 组件库&#xff0c;将更多时间花在应用程序的核心功能上。要求零依赖&#xff0c;Wijmo sports弹性网格&#xff0c;业内最好的 JavaScript 数据网格&#xff0c;提…

基于深度学习的高精度动物检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度动物检测识别系统可用于日常生活中或野外来检测与定位动物目标&#xff08;狼、鹿、猪、兔和浣熊&#xff09;&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的动物&#xff08;狼、鹿、猪、兔和浣熊&#xff09;目标检测识…

前端基础面试题(HTML,CSS,JS)

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 html语义化的理解 代码结构: 使页面在没有css的情况下,也能够呈现出好的内容结构 有利于SE…

AI实战营:MMPreTrain代码实现

环境 环境安装 pip install openmim mim install mmengine mim install mmcv mim install mmpretrain # 安装多模态模型 pip install "mmpretrain[multimodal]" 验证环境 In [1]: import mmengineIn [2]: mmengine.__version__ Out[2]: 0.7.3In [3]: import …

开发者出海合规手册;@levelsio独立开发月入20万解析;MJ+AR设计珠宝;SD算法原理-通俗版 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 独立开发者必看&#xff0c;出海应用开发者合规手册 这是 JourneymanChina 多年出海经验教训的总结&#xff0c;适用于Google Play 以…

在Wamp环境中如何下载Composer并且使用Laravel配置Apache服务器

一.Composer的安装 方法1.到Composer官网Composer (getcomposer.org)下载 点击Composer-Setup.exe下载Composer安装包 点击Next 这里选择你的php.exe的地址 然后一直点next结束。 然后打开cmd命令输入composer -v看是否运行成功。 方法2.CMD命令安装composer php -r &quo…

学生考试作弊检测系统 yolov8

学生考试作弊检测系统采用yolov8网络模型人工智能技术&#xff0c;学生考试作弊检测系统过在考场中安装监控设备&#xff0c;对学生的作弊行为进行实时监测。当学生出现作弊行为时&#xff0c;学生考试作弊检测系统将自动识别并记录信息。YOLOv8 算法的核心特性和改动可以归结为…

SolVES 模型与多技术融合【多语言】实现生态系统服务功能社会价值评估及拓展案例分析

生态系统服务是人类从自然界中获得的直接或间接惠益&#xff0c;可分为供给服务、文化服务、调节服务和支持服务4类&#xff0c;对提升人类福祉具有重大意义&#xff0c;且被视为连接社会与生态系统的桥梁。自从启动千年生态系统评估项目&#xff08;Millennium Ecosystem Asse…

IDEA中Maven依赖包下载不了的一种“奇怪”解决方案【亲测有效】

&#x1f4a7; 记录一下今天遇到的 b u g \color{#FF1493}{记录一下今天遇到的bug} 记录一下今天遇到的bug&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我亦是行人.✨ &#x1f984; 个人主页——微风撞见云的博客&#x1f390; &#x1f433; 数据结构与算法…

Linux - fd文件描述符和文件详解

​​​​​​​ ​​​​​​​ ​​​​​​​ 感谢各位 点赞 收藏 评论 三连支持 本文章收录于专栏【Linux系统编程】 ❀希望能对大家有所帮助❀ 本文章由 风君子吖 原创 ​​​​​​​ ​​​​​​​ ​​​​​​​ …

WPF 如何实时查看页面元素如何使用实时可视化树

文章目录 往期回顾可视化页面元素如何使用调试工具 总结 往期回顾 WPF 学习&#xff1a;如何使用实时可视化树&#xff0c;照着MaterialDesign的Demo学习 可视化页面元素 我们知道&#xff0c;网页的页面元素是可以通过按F12查看代码。查看到页面元素的。 WPF也有类似的工具…

基于相位共轭法的散射聚焦成像研究-Matlab代码

▒▒本文目录▒▒ 一、引言二、相位共轭法散射聚焦成像Matlab仿真三、参考文献四、Matlab程序开发与实验指导 一、引言 一直以来&#xff0c;研究人员致力于分析造成散射的原因、随机介质性质以及各种散射光的特征&#xff0c;并且研究透过散射介质成像。1990年&#xff0c;I.…