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
目录
相关文章
|
2月前
|
数据采集 机器学习/深度学习 算法
|
1月前
|
JavaScript 前端开发 算法
React技术栈-虚拟DOM和DOM diff算法
这篇文章介绍了React技术栈中的虚拟DOM和DOM diff算法,并通过一个实际案例展示了如何使用React组件和状态管理来实现动态更新UI。
34 2
|
2月前
|
JavaScript 前端开发 算法
react中虚拟dom和diff算法
在React中,虚拟DOM(Virtual DOM)和Diff算法是两个核心概念,它们共同工作以提高应用的性能和效率。
31 4
|
1天前
|
传感器 算法 C语言
基于无线传感器网络的节点分簇算法matlab仿真
该程序对传感器网络进行分簇,考虑节点能量状态、拓扑位置及孤立节点等因素。相较于LEACH算法,本程序评估网络持续时间、节点死亡趋势及能量消耗。使用MATLAB 2022a版本运行,展示了节点能量管理优化及网络生命周期延长的效果。通过簇头管理和数据融合,实现了能量高效和网络可扩展性。
|
9天前
|
XML JavaScript 前端开发
学习react基础(1)_虚拟dom、diff算法、函数和class创建组件
本文介绍了React的核心概念,包括虚拟DOM、Diff算法以及如何通过函数和类创建React组件。
15 2
|
24天前
|
机器学习/深度学习 算法 Python
群智能算法:深入解读人工水母算法:原理、实现与应用
近年来,受自然界生物行为启发的优化算法备受关注。人工水母算法(AJSA)模拟水母在海洋中寻找食物的行为,是一种新颖的优化技术。本文详细解读其原理及实现步骤,并提供代码示例,帮助读者理解这一算法。在多模态、非线性优化问题中,AJSA表现出色,具有广泛应用前景。
|
2月前
|
前端开发 算法 JavaScript
React原理之Diff算法
【8月更文挑战第24天】
|
6天前
|
机器学习/深度学习 JavaScript 算法
面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
该文章深入探讨了虚拟DOM的概念及其diff算法,解释了虚拟DOM如何最小化实际DOM的更新,以此提升web应用的性能,并详细分析了diff算法的实现机制。
|
2月前
|
存储 负载均衡 监控
自适应负载均衡算法原理和实现
自适应负载均衡算法原理和实现
|
2月前
|
算法 安全 网络安全
Diffie-Hellman (DH) 算法的工作原理
【8月更文挑战第23天】
118 0
下一篇
无影云桌面