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

简介: vue2 原理【详解】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
目录
相关文章
机器学习/深度学习 算法 自动驾驶
119 0
|
22天前
|
机器学习/深度学习 算法 搜索推荐
从零开始构建图注意力网络:GAT算法原理与数值实现详解
本文详细解析了图注意力网络(GAT)的算法原理和实现过程。GAT通过引入注意力机制解决了图卷积网络(GCN)中所有邻居节点贡献相等的局限性,让模型能够自动学习不同邻居的重要性权重。
103 0
从零开始构建图注意力网络:GAT算法原理与数值实现详解
|
30天前
|
机器学习/深度学习 算法 文件存储
神经架构搜索NAS详解:三种核心算法原理与Python实战代码
神经架构搜索(NAS)正被广泛应用于大模型及语言/视觉模型设计,如LangVision-LoRA-NAS、Jet-Nemotron等。本文回顾NAS核心技术,解析其自动化设计原理,探讨强化学习、进化算法与梯度方法的应用与差异,揭示NAS在大模型时代的潜力与挑战。
274 6
神经架构搜索NAS详解:三种核心算法原理与Python实战代码
|
1月前
|
传感器 算法 定位技术
KF,EKF,IEKF 算法的基本原理并构建推导出四轮前驱自主移动机器人的运动学模型和观测模型(Matlab代码实现)
KF,EKF,IEKF 算法的基本原理并构建推导出四轮前驱自主移动机器人的运动学模型和观测模型(Matlab代码实现)
|
1月前
|
算法
离散粒子群算法(DPSO)的原理与MATLAB实现
离散粒子群算法(DPSO)的原理与MATLAB实现
87 0
|
2月前
|
机器学习/深度学习 人工智能 编解码
AI视觉新突破:多角度理解3D世界的算法原理全解析
多视角条件扩散算法通过多张图片输入生成高质量3D模型,克服了单图建模背面细节缺失的问题。该技术模拟人类多角度观察方式,结合跨视图注意力机制与一致性损失优化,大幅提升几何精度与纹理保真度,成为AI 3D生成的重要突破。
179 0
|
2月前
|
算法 区块链 数据安全/隐私保护
加密算法:深度解析Ed25519原理
在 Solana 开发过程中,我一直对 Ed25519 加密算法 如何生成公钥、签名以及验证签名的机制感到困惑。为了弄清这一点,我查阅了大量相关资料,终于对其流程有了更清晰的理解。在此记录实现过程,方便日后查阅。
165 0
|
7天前
|
传感器 机器学习/深度学习 算法
【使用 DSP 滤波器加速速度和位移】使用信号处理算法过滤加速度数据并将其转换为速度和位移研究(Matlab代码实现)
【使用 DSP 滤波器加速速度和位移】使用信号处理算法过滤加速度数据并将其转换为速度和位移研究(Matlab代码实现)
|
7天前
|
机器学习/深度学习 算法 Java
基于灰狼优化算法(GWO)解决柔性作业车间调度问题(Matlab代码实现)
基于灰狼优化算法(GWO)解决柔性作业车间调度问题(Matlab代码实现)
|
7天前
|
算法 机器人 Serverless
【机器人路径规划】基于6种算法(黑翅鸢优化算法BKA、SSA、MSA、RTH、TROA、COA)求解机器人路径规划研究(Matlab代码实现)
【机器人路径规划】基于6种算法(黑翅鸢优化算法BKA、SSA、MSA、RTH、TROA、COA)求解机器人路径规划研究(Matlab代码实现)

热门文章

最新文章