vue 原理【详解】MVVM、响应式、模板编译、虚拟节点 vDom、diff 算法

vue 的设计模式 —— MVVM

在这里插入图片描述

  • M —— Model 模型,即数据
  • V —— View 视图,即DOM渲染
  • VM —— ViewModel 视图模型,用于实现Model和View的通信,即数据改变驱动视图渲染,监听视图事件修改数据

初次渲染

  1. 将模板编译为 render 函数 ( webpack 中使用的 vue-loader 插件在开发环境启动项目时会完成编译)
  2. 触发响应式,监听 data 属性触发 getter 和 setter 方法 (主要是getter 方法)
  3. 执行 render 函数,生成 vnode ,执行 patch(elem, vnode) 完成 DOM 渲染

更新过程【需会画和讲解图】

  1. 修改 data,触发 setter 方法
  2. 重新执行 render 函数,生成 newVnode
  3. 执行 patch(vnode,newVnode) 更新发生变化的 DOM 节点

在这里插入图片描述

【重点】异步渲染

vue 的更新过程,是一种异步渲染,即并不是每一点 data 的改变都会立马触发视图更新, 而是会汇总 data 的修改,再一次性更新视图,这样可以减少 DOM 的操作次数,提高性能。

在这里插入图片描述

vue 原理的三大核心

一、响应式

vue 的响应式机制是在vue 实例初始化时建立的,即 data 函数中定义的变量,在页面初始化后,都具有响应式。

对于vue 实例初始化之后新增的属性,不具有响应式,解决方案是改用 $set 的方式新增属性。

监听 data 的变化

【核心】 API-Object.defineProperty

在这里插入图片描述
此时只能监听到对象的第一层属性,而无法实现更深层次属性变化的监听。

实现深度监听

  • 深层对象属性的监听,通过递归遍历深层对象的属性实现
  • 深层数组的监听,通过变更数组原型(为所有改变数组的 api 中加入更新视图),再递归遍历深层数组实现。
// 触发更新视图
function updateView() {
  console.log("视图更新");
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype;
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
["push", "pop", "shift", "unshift", "splice"].forEach((methodName) => {
  arrProto[methodName] = function () {
    updateView(); // 触发视图更新
    oldArrayProperty[methodName].call(this, ...arguments);
  };
});

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
  // 深度监听
  observer(value);

  // 核心 API
  Object.defineProperty(target, key, {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // 深度监听
        observer(newValue);

        // 设置新值
        // 注意,value 一直在闭包中,此处设置完之后,再 get 时会获取最新的值
        value = newValue;

        // 触发更新视图
        updateView();
      }
    },
  });
}

// 监听对象属性
function observer(target) {
  // 不是对象或数组,无需深度监听
  if (typeof target !== "object" || target === null) {
    return target;
  }

  // 若是数组,则修改为自定义的添加了视图刷新的数组原型
  if (Array.isArray(target)) {
    target.__proto__ = arrProto;
  }

  // 重新定义各个属性(for in 也可以遍历数组)
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

// 准备数据
const data = {
  name: "张三",
  age: 20,
  info: {
    address: "北京", // 需要深度监听
  },
  nums: [10, 20, 30], // 需要深度监听
};

// 监听数据
observer(data);

// 测试
data.name = "李四"; // 无需深度监听
data.age = 21; // 无需深度监听
data.x = "100"; // 新增属性,监听不到 —— 需用 Vue.set
delete data.name; // 删除属性,监听不到 —— 需用 Vue.delete
data.info.address = "上海"; // 对象属性的属性,需要深度监听
data.nums.push(4); // 数组需要深度监听

Object.defineProperty 的缺点

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听属性的新增和删除,这会导致以下操作无响应式:
    • 对象新增属性
    • 对象删除属性
    • 通过数组下标修改数组元素的值
    • 修改数组的长度

为了弥补以上操作无响应式的缺陷,vue 补充了 set 和 delete 方法。

Vue.set() 和 this.$set() 这两个api的实现原理基本一模一样,都是使用了set函数

$set 的响应式原理

this.$set(this.arr, "3", 7)
  • 对于数组,$set 的参数为数组、数组下标、新的值,通过调用被 vue 改造过的添加了视图更新的 splice 方法实现响应式,相关vue 源码如下:

    // 判断操作目标是否是数组,传入的数组下标是否规范
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      // 若传入的数组下标超过数组长度,则将数组长度增长为传入的下标,以防后续调用splice方法时因下标超出数组长度而报错。
      target.length = Math.max(target.length, key)
      // 使用添加了视图更新的 splice 方法实现响应式
      target.splice(key, 1, val)
      return val
     }
    
this.$set(this.obj, "新的属性", "新增的属性的值");
  • 对于对象,$set 的参数为对象、新的属性、新增的属性的值,通过对新增属性添加深度监听实现响应式,相关vue 源码如下:

    // 判断如果key本来就是对象中的一个属性,并且key不是Object原型上的属性, 则此属性已添加过响应式,直接修改值即可。
     if (key in target && !(key in Object.prototype)) {
      target[key] = val
      return val
     }
     // 获取 target对象的 __ob__ 属性
     const ob = (target: any).__ob__
     // 若 target对象是vue实例对象或者是根数据对象,则抛出错误警告。
     if (target._isVue || (ob && ob.vmCount)) {
      process.env.NODE_ENV !== 'production' && warn(
       'Avoid adding reactive properties to a Vue instance or its root $data ' +
       'at runtime - declare it upfront in the data option.'
      )
      return val
     }
     // 若 target 对象的 __ob__ 属性不存在,则 target 不是响应式对象,无需添加响应式监听,直接新增属性赋值即可。( vue给响应式对象都加了 __ob__ 属性,如果一个对象有 __ob__ 属性,则说明这个对象是响应式对象)
     if (!ob) {
      target[key] = val
      return val
     }
     // 给新属性添加响应式监听
     defineReactive(ob.value, key, val)
     // 触发视图更新
     ob.dep.notify()
     return val
    

二、模板编译

vue 文件中支持指令、插值、JS 表达式,还能实现判断、循环,大大便捷了开发,但无法在浏览器中渲染,需要先将其转换成 JS 代码才行,这个转换的过程,即模板编译。

编译过程

  1. 借助插件 vue-template-compiler 将vue 文件编译成 render 函数
  2. 执行 render 函数,返回 vnode
  3. 基于 vnode 执行 patch 和 diff ,完成 DOM 渲染

演示代码

const compiler = require('vue-template-compiler')
const template = `<p>{{message}}</p>`
const res = compiler.compile(template)
console.log(res.render)

得到函数

with(this){return _c('p',[_v(_s(message))])}
  • _c 对应插件内定义的函数 createElement
  • _v 对应插件内定义的函数 createTextVNode
  • _s 对应插件内定义的函数 toString

即实现了模板向 JS 的转换。

with 语法

在这里插入图片描述

  • 改变 {} 内自由变量的查找规则,将其当做 obj 属性来查找
  • 如果找不到匹配的 obj 属性,就会报错
  • with 要慎用,它打破了作用域规则,易读性变差

编译形式

  • 在 webpack 中使用的 vue-loader 插件,在开发环境启动项目时,就完成了模板的编译(提升了渲染效率)
  • vue 组件可以用 template 写法,也可以直接用 render 函数(react 中全是 render 函数 )
    在这里插入图片描述

三、虚拟节点 vDom

数据变化驱动视图更新,就需要执行DOM 操作重新渲染视图,但DOM 操作非常耗费性能,怎样提升性能呢?

解决思路:使用虚拟节点 vDom,即用 JS 模拟 DOM 结构,计算出最小的变更,更新 DOM。

因为 JS 的执行速度比DOM 操作快得多!

在这里插入图片描述
通过 h 函数生成 vnode

在这里插入图片描述

  • 初次渲染【增】
patch(container,vnode);

在目标容器中,渲染节点

  • 更新视图【改】
patch(vnode,newVnode);

用新节点,替代旧节点

在这里插入图片描述
只会重新渲染新旧节点中有差异的部分,不会重新渲染整个节点。

  • 销毁视图【删】
patch(newVnode,null);

用 null 替代目标节点

【核心】diff 算法

用于计算出 vDom 的最小变更(即比较出新旧 DOM 树的差异)

在这里插入图片描述
树 diff 的时间复杂度为 O(n^3)

  1. 遍历 tree1
  2. 遍历 tree2
  3. 排序

1000 个节点,要计算1亿次,算法不可用!

改用 diff 算法将时间复杂度降为 O(n)

  • 只比较同一层级,不跨级比较
    在这里插入图片描述

  • tag 不相同,则直接删掉重建,不再深度比较

在这里插入图片描述

  • tag 和 key,两者都相同,则认为是相同节点,不再深度比较
    在这里插入图片描述
    不使用key,则所有元素会先移除,再添加
    若使用key,则若存在未改变的元素,只需进行移动即可。
    若key 使用 index,则 key 的值为 0,1,2,3,4……,则若元素的顺序发生改变时,会出现问题。

相关的重要函数

  • patchVnode
  • addVnodes
  • removeVnodes
  • updatèChildren

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Prometheus报错,查不到数据

Warning: Error fetching server time: Detected 28799.947999954224 seconds time difference between your browser and the server. Prometheus relies on accurate time and time drift might cause unexpected query results. 1.这是因为服务器和本地时间不同步导致的 查…

抖店怎么回复客户消息才能减少差评?分享几个超级实用的话术!

哈喽~我是电商月月 新手入驻抖音小店出单后&#xff0c;或多或少都会遇到差评现象 差评私信不解决&#xff0c;顾客不满意&#xff0c;店铺的体验分下降&#xff0c;差评也能被所有的顾客看见 那之后的顾客就会觉得店铺不可靠&#xff0c;那新手如何避免这一现象呢 今天我就…

SLF4J对lombok类型的对象调用toString()失败--StackOverflowError

PackingDemand.class StatusHistory.class 造成该问题的原因是&#xff1a;PackingDemand与StatusHistory之间的双向引用。这些类中生成的两个toString()方法都会无休止地相互调用导致出现java.lang.StackOverflowError。 解决方法&#xff1a; 1.对于使用ToString.Exclude生…

Redis学习从入门到掌握(基础篇)

文章目录 一、初识Redis1.认识 Redis2.Redis常见命令&#xff08;1&#xff09;Redis 数据结构介绍&#xff08;2&#xff09;Redis 通用命令&#xff08;3&#xff09;String 类型&#xff08;4&#xff09;String 类型的常见命令&#xff08;5&#xff09;Hash 类型&#xff…

练习6 beach beauty

文章目录 图片展示 图片展示

说说我理解的数据库中的Schema吧

一、SQL标准对schema如何定义&#xff1f; ISO/IEC 9075-1 SQL标准中将schema定义为描述符的持久命名集合&#xff08;a persistent, named collection of descriptors&#xff09;。 大部分的网上资料定义Schema如下&#xff1a; schema是用来组织和管理数据的一种方式。它…

【渲染加速秘籍】3D建模不再卡顿:硬件升级、场景优化与云渲染全攻略!

在使用3D建模软件时&#xff0c;我们经常会遇到硬件资源消耗较大的问题&#xff0c;尤其是在进行染操作时。为了解决这一问题&#xff0c;我们可以采取以下几种方法来优化我们的工作流程和提升渲染效率。 1.升级硬件设备 对于经常需要进行渲染工作的用户来说&#xff0c;投资高…

让你的进程24小时在linux上运行

xshell原理 可能我们都在使用xshell时&#xff0c;都会遇到一些问题&#xff0c;就是你在xshell运行了你的服务器。可是你把xshell页面一关&#xff0c;你的服务器就自动关闭了&#xff0c;这是为什么呢&#xff1f;&#xff1f; 本质是因为我们的xshell在登陆服务器时&#…

前端对接fastGPT流式数据+打字机效果

首先在对接api时 参数要设置stream: true, const data {chatId: abc,stream: true,//这里true返回流式数据detail: false,variables: {uid: sfdsdf,name: zhaoyunyao,},messages: [{ content: text, role: user }]}; 不要用axios发请求 不然处理不了流式数据 我这里使用fetch …

细胞世界:4.细胞分化(划区域)与细胞衰老(设施磨损)

(1)细胞凋亡 1. 概念&#xff1a;细胞凋亡可以比作城市的规划者主动拆除某些建筑来更新城市或防止危险建筑对市民的潜在伤害。这是一个有序的过程&#xff0c;由城市&#xff08;细胞内部&#xff09;的特定规划&#xff08;基因&#xff09;所决定。 2. 特征&#xff1a;细…

特别详细的Spring Cloud 系列教程2:微服务网关gateway的启动

继上一篇&#xff1a;特别详细的Spring Cloud 系列教程1&#xff1a;服务注册中心Eureka的启动 在比较多的教程和书籍里&#xff0c;spring cloud的微服务网关用的Zuul。然而&#xff0c;zuul已经不被官方提倡&#xff0c;现在提倡用的是spring cloud gateway。因为gateway的整…

c++编程(3)——类和对象(1)、类

欢迎来到博主的专栏——c编程 博主ID&#xff1a;代码小豪 文章目录 类对象类的访问权限类的作用域 类 c最初对c语言的扩展就是增加了类的概念&#xff0c;使得c语言在原有的基础之上可以做到信息隐藏和封装。 那么我们先来讲讲“带类的c”与C语言相比有什么改进。 先讲讲类…

2024数学建模认证杯A题成品论文更新+全套数据集+可执行代码+可视化结果图标+学长保姆级答疑

题目&#xff1a;《基于TOPSIS和数值模拟的保暖纤维性能综合评价研究 &#xff08;完整资料论文都在文末&#xff01;&#xff01;&#xff09; 摘要&#xff1a;本研究通过数学建模方法深入探讨了人造保暖纤维的保暖性能&#xff0c;并建立了一个综合性的评价体系来全面…

scaling laws for neural language models

关于scaling law 的正确认识 - 知乎最近scaling law 成了最大的热词。一般的理解就是&#xff0c;想干大模型&#xff0c;清洗干净数据&#xff0c;然后把数据tokens量堆上来&#xff0c;然后搭建一个海量H100的集群&#xff0c;干就完了。训练模型不需要啥技巧&#xff0c;模型…

19(20)-1(3)-CSS3 平面 2D 变换+CSS3 过渡

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 ✍一、CSS3 平面 2D 变换&#x1f48e;1 坐标轴&#x1f48e;2 transform 语法…

jenkins+sonar配置

安装插件 Sonar Scanner 用于扫描项目 配置sonar scanner jenkins集成sonar 1、sonar生成token 生成完保存好&#xff0c;刷新后无法查看 2、jenkins配置全局凭据 3、jenkins配置系统设置

浅谈性能测试

本文主要针对WEB系统的性能测试。不涉及具体的执行操作&#xff0c;只是本人对性能测试的一点理解和认识。 性能测试的目的&#xff0c;简单说其实就是为了获取待测系统的响应时间、吞吐量、稳定性、容量等信息。而发现一些具体的性能相关的缺陷&#xff08;如内存溢出、并发处…

关于配置nginx的反向代理时出现的一些问题及解决方法

1.配置反向代理 &#xff08;1&#xff09;上传nginx.conf到/opt/nginx/conf/中&#xff0c;并覆盖。 #查看一下IP是否正确&#xff08;需要将文件中的IP改成自己的IP&#xff09; cat /opt/nginx/conf/nginx.conf &#xff08;2&#xff09;重启 cd /opt/nginx/sbin ./n…

day9 next商业项目初探·五(java转ts全栈/3R教室)

背景&#xff1a;从头一点点学起太慢了&#xff0c;直接看几个商业项目吧&#xff0c;看看根据Java的经验&#xff0c;自己能看懂多少&#xff0c;然后再系统学的话也会更有针对性。今天看下一个项目 huanghanzhilian/c-shopping: A beautiful shopping platform developed wit…

Oracle 19c RAC集群相关日志

1.DB日志&#xff08;数据库日志&#xff09; Redo Log&#xff08;重做日志&#xff09;&#xff1a; 在Oracle数据库中&#xff0c;重做日志记录了数据库发生的所有修改操作&#xff0c;包括数据的插入&#xff0c;更新和删除。在RAC的环境中&#xff0c;每个实例都有自己的重…