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
目录
相关文章
|
16天前
|
算法 JavaScript UED
Diff 算法的实现原理
【10月更文挑战第18天】Diff 算法是 Vue.js 中实现高效 DOM 更新的核心机制,通过合理的比较和优化策略,能够在保证界面正确性的同时,最大程度地减少 DOM 操作,提高应用的性能和用户体验。
21 2
|
16天前
|
算法 JavaScript
Vue 中的 Diff 算法
【10月更文挑战第18天】需要注意的是,Diff 算法虽然能够提高性能,但在某些复杂的场景下,可能仍然会存在一些性能瓶颈。因此,在实际开发中,我们需要根据具体情况合理地使用 Diff 算法,并结合其他优化手段来提高应用的性能。
9 1
|
24天前
|
JavaScript 算法 前端开发
vue 中diff算法
【10月更文挑战第10天】
26 1
|
1月前
|
JavaScript 算法 前端开发
【VUE】Vue的diff算法和React的diff算法
【VUE】Vue的diff算法和React的diff算法
|
16天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
1天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
2天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
3天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。
|
2天前
|
机器学习/深度学习 算法 芯片
基于GSP工具箱的NILM算法matlab仿真
基于GSP工具箱的NILM算法Matlab仿真,利用图信号处理技术解析家庭或建筑内各电器的独立功耗。GSPBox通过图的节点、边和权重矩阵表示电气系统,实现对未知数据的有效分类。系统使用MATLAB2022a版本,通过滤波或分解技术从全局能耗信号中提取子设备的功耗信息。
|
2天前
|
机器学习/深度学习 算法 5G
基于MIMO系统的SDR-AltMin混合预编码算法matlab性能仿真
基于MIMO系统的SDR-AltMin混合预编码算法通过结合半定松弛和交替最小化技术,优化大规模MIMO系统的预编码矩阵,提高信号质量。Matlab 2022a仿真结果显示,该算法能有效提升系统性能并降低计算复杂度。核心程序包括预编码和接收矩阵的设计,以及不同信噪比下的性能评估。
14 3