Vue源码之虚拟DOM和diff算法(一) 使用snabbdom

简介: Vue源码之虚拟DOM和diff算法(一) 使用snabbdom
前言:如果这篇文章 对你有帮助,请不要吝啬你的赞。😃

什么是虚拟DOM和diff算法

diff算法简介

image-20220317115218615

要把左图装修成右图的样子。(哪里不同?仔细找)

有两种方案。

方案一:拆掉重建(效率低,代价大)

方案二:diff(精细化比对,最小量更新)


怎么看都应该会选择方案二。


那么在Vue中使用 diff的情景呢?

image-20220317121207880

上图就是在Vue中使用 diff的情景(比如左图中,有一些元素的 v-iffalse,所以不显示,而右图中, v-if true)


虚拟DOM简介

虚拟DOM:用来描述DOM的层次结构的js对象。真实DOM中的一切属性在虚拟DOM中都存在。

image-20220317121849612


diff是发生在虚拟DOM上的

image-20220317154226660


优点:

  • 减少对真实DOM的操作
  • 虚拟 DOM 本质上是 JavaScript 对象,可以跨平台,比如服务器渲染等


snabbdom

snabbdom仓库

snabbdom是著名的虚拟DOM库,是diff算法的鼻祖。(Vue源码也借鉴了 snabbdom)

安装

npm install snabbdom

webpack配置

上一篇中,有 webpack配置可查看Vue源码系列的上一篇文章。

webpack.config.js

const path = require('path');

module.exports = {
  entry: path.join(__dirname, 'src', 'index.js'),
  mode: 'development',
  output: {
    filename: 'bundle.js',
    // 虚拟打包路径,bundle.js文件没有真正的生成
    publicPath: "/virtual/"
  },

  devServer: {
    // 静态文件根目录
    static: path.join(__dirname, 'www'),
    // 不压缩
    compress: false,
    port: 8080,
  }
}


h函数使用

h函数用来创建虚拟节点(vnode)

image-20220317154552800


参数介绍:

  • 第一个参数:是生成的虚拟节点对应DOM节点的标签名
  • 第二个参数:一个对象,虚拟节点的属性(可选)
  • 第三个参数:标签中的内容

h函数体验

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";


const myVnode = h('a', {
  props: {
    href: 'https://clz.vercel.app'
  }
}, '赤蓝紫')

console.log(myVnode)

image-20220317155922806


虚拟DOM节点属性介绍

  • children: 子元素,没有则为 undefined
  • data(对象形式): 类名、属性、样式、事件(对象形式)
  • elm: 对应的真实DOM节点(如果没有对应的,则为undefined )
  • key:唯一标识
  • sel:选择器
  • text:文字


搭配 patch函数生成真实DOM节点

通过引入的 init函数把所有的模块(类名模块、属性模块、样式模块、事件监听模块)作为参数(少的话,则上树后也会少,比如少事件监听模块,上树后,事件将不再生效)

const patch = init([classModule, propsModule, styleModule, eventListenersModule])


container只是占位符,上树后会消失

// container只是占位符,上树后会消失
const container = document.getElementById('container')
patch(container, myVnode)    // 上树(第一个参数如果不是虚拟节点,则是执行上树操作,否则是diff算法,第一个参数是旧虚拟节点,第二个参数是新虚拟节点)


完整版

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";

// 加载模块,,创建出patch函数。没有对应模块的话,上树后,也对应没有。比如少事件监听模块,上树后,事件将不再生效
// 类名模块、属性模块、样式模块、事件监听模块
const patch = init([classModule, propsModule, styleModule, eventListenersModule])


const myVnode = h('button', {
  class: {            // 类名
    "btn": true
  },
  props: {            // 属性, id也在这里面
    id: 'btn',
    title: '赤蓝紫'
  },
  style: {          // 样式
    backgroundColor: 'red',
    border: 0,
    color: '#fff',
  },
  on: {             // 事件监听
    click: function () {
      location.assign('https://clz.vercel.app')
    }
  }
}, '赤蓝紫')

console.log(myVnode)

// container只是占位符,上树后会消失
const container = document.getElementById('container')
patch(container, myVnode)     // 上树


h函数嵌套使用

h函数可以嵌套使用,从而得到虚拟DOM树

image-20220317174152025


动手实践

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";


const patch = init([classModule, propsModule, styleModule, eventListenersModule])


const myVnode = h('ul', [     // 没有数据参数
  h('li', '赤'),        // 可以没有数据参数
  h('li', {}, '蓝'),    // 数据参数可为空
  h('li', h('span', '紫'))     // 内容参数为调用h函数且只有一个时,可以不是数组形式
])

console.log(myVnode)


const container = document.getElementById('container')
patch(container, myVnode)

image-20220317174911551


手写h函数

编写vnode函数

vnode功能:把传入的参数组合成对象后返回

src \ mysnabbdom \ vnode.js

export default function (sel, data, children, text, elm) {
  return {
    sel,
    data,
    children,
    text,
    elm
  }
}


本次手写的是低配版本的h函数,必须接收3个参数

调用可以有以下三种形式

  1. h('div', {}, '文字')
  2. h('div', {}, [])
  3. h('div', {}, h())


实现第一种形式

直接调用 vnode函数,返回即可

src \ mysnabbdom \ h.js

import vnode from './vnode.js'

export default function (sel, data, content) {
  if (arguments.length !== 3) {
    throw new Error('这是低配版h函数, 必须接收3个参数')
  } else if (typeof content === 'string' || typeof content === 'number') {
    // 形式1
    return vnode(sel, data, undefined, content, undefined)
  } else if (Array.isArray(content)) {
    // 形式2
  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
    // 形式3:因为h函数最终会返回一个一定会有`sel`属性的对象

  } else {
    throw new Error('传入的第三个参数类型不对')
  }
}


测试

src \ index.js

import h from './mysnabbdom/h.js'

console.log(h('div', {}, '文字'))

image-20220317230452293


实现第二种形式

src \ mysnabbdom \ h.js

import vnode from './vnode.js'


export default function (sel, data, content) {
  if (arguments.length !== 3) {
    throw new Error('这是低配版h函数, 必须接收3个参数')
  } else if (typeof content === 'string' || typeof content === 'number') {
    // 形式1
    return vnode(sel, data, undefined, content, undefined)
  } else if (Array.isArray(content)) {
    // 形式2
    const children = []

    for (let i = 0; i < content.length; i++) {
      if (!(typeof content[i] === 'object' && content[i].hasOwnProperty('sel'))) {
        throw new Error('传入的数组中又有不是调用h函数的')
      }

      children.push(content[i])      // 因为传的数组里的元素就是调用h函数的,即得到的已经是处理过后返回的对象了
    }

    return vnode(sel, data, children, undefined, undefined)
  } else if (typeof content === 'object' && content.hasOwnProperty('sel')) {
    // 形式3:因为h函数最终会返回一个一定会有`sel`属性的对象

  } else {
    throw new Error('传入的第三个参数类型不对')
  }
}


测试

src \ index.js

import h from './mysnabbdom/h.js'

console.log(h('div', {}, [
  h('p', {}, '赤'),
  h('p', {}, '蓝'),
  h('p', {}, '紫'),
  h('p', {}, [
    h('span', {}, '黑'),
    h('span', {}, '白')
  ])
])) 

image-20220317230809710

第三种形式

src \ mysnabbdom \ h.js

/*
 * 低配版本的h函数,必须接收3个参数
 * 调用可以有以下三种形式
 * 1. h('div', {}, '文字')
 * 2. h('div', {}, [])
 * 3. h('div', {}, h())
*/

import { h } from 'snabbdom'
import vnode from './vnode.js'


export default function (sel, data, content) {
  if (arguments.length !== 3) {
    throw new Error('这是低配版h函数, 必须接收3个参数')
  } else if (typeof content === 'string' || typeof content === 'number') {
    // 形式1
    return vnode(sel, data, undefined, content, undefined)
  } else if (Array.isArray(content)) {
    // 形式2
    const children = []

    for (let i = 0; i < content.length; i++) {
      if (!(typeof content[i] === 'object' && content[i].hasOwnProperty('sel'))) {
        throw new Error('传入的数组中又有不是调用h函数的')
      }

      children.push(content[i])      // 因为传的数组里的元素就是调用h函数的,即得到的已经是处理过后返回的对象了
    }

    return vnode(sel, data, children, undefined, undefined)
  } else if (typeof content === 'object' && content.hasOwnProperty('sel')) {
    // 形式3:因为h函数最终会返回一个一定会有`sel`属性的对象

    return vnode(sel, data, [content], undefined, undefined)    //把它当成只有一个元素的数组来处理

  } else {
    throw new Error('传入的第三个参数类型不对')
  }
}

测试

src \ index.js

import h from './mysnabbdom/h.js'

console.log(h('div', {}, h('span', {}, '赤蓝紫')))

image-20220317232336683

diff算法初体验

在后面插入

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";

// 加载模块,创建出patch函数。没有对应模块的话,上树后,也对应没有。比如少事件监听模块,上树后,事件将不再生效
// 类名模块、属性模块、样式模块、事件监听模块
const patch = init([classModule, propsModule, styleModule, eventListenersModule])


const myVnode1 = h('ul', {}, [
  h('li', {}, 'a'),
  h('li', {}, 'b'),
  h('li', {}, 'c'),
  h('li', {}, 'd'),
])
const myVnode2 = h('ul', {}, [
  h('li', {}, 'a'),
  h('li', {}, 'b'),
  h('li', {}, 'c'),
  h('li', {}, 'd'),
  h('li', {}, 'e')
])

const container = document.getElementById('container')
patch(container, myVnode1)     // 上树
const btn = document.getElementById('btn')
btn.addEventListener('click', function () {
  patch(myVnode1, myVnode2)     // 点击后,从myVnode1变为myVnode2
})

diff


怎么知道是不是真的是最小量更新呢?

可以用老师用的巧妙法:在 DevTools Elements面板修改内容,查看有没有变化

diff

可以发现,确确实实是最小量更新。仔细看上面的图,发现不需要修改 Elements面板,有更新的话,会变紫,闪烁一下


在前面插入

那么,接下来就试一下在开头加入新节点的情况咯

const myVnode2 = h('ul', {}, [
  h('li', {}, 'e'),
  h('li', {}, 'a'),
  h('li', {}, 'b'),
  h('li', {}, 'c'),
  h('li', {}, 'd')
])

diff

可以说是,上面的情况压根就不是最小量更新。

这是为什么呢?这时候就需要 key的闪亮登场了

没有key的时候:会先把节点插到最后,再把插入的节点移动到要去的位置,其他节点也需要移动到要去的位置


在中间插入

可以再来测试一下

const myVnode2 = h('ul', {}, [
  h('li', {}, 'a'),
  h('li', {}, 'e'),
  h('li', {}, 'b'),
  h('li', {}, 'c'),
  h('li', {}, 'd')
])

这时候,只有a不会变化,因为e插入的位置不会影响到a



使用key,真正实现最小量更新

key的时候,就不一样了,每一个虚拟节点都有一个唯一标识,所以能够精准定位,真正实现最小化更新

const myVnode1 = h('ul', {}, [
  h('li', { key: 'a' }, 'a'),
  h('li', { key: 'b' }, 'b'),
  h('li', { key: 'c' }, 'c'),
  h('li', { key: 'd' }, 'd'),
])
const myVnode2 = h('ul', {}, [
  h('li', { key: 'e' }, 'e'),
  h('li', { key: 'a' }, 'a'),
  h('li', { key: 'b' }, 'b'),
  h('li', { key: 'c' }, 'c'),
  h('li', { key: 'd' }, 'd')
])

diff


使用key,并完全调换位置

const myVnode1 = h('ul', {}, [
  h('li', { key: 'a' }, 'a'),
  h('li', { key: 'b' }, 'b'),
  h('li', { key: 'c' }, 'c'),
  h('li', { key: 'd' }, 'd'),
])
const myVnode2 = h('ul', {}, [
  h('li', { key: 'd' }, 'd'),
  h('li', { key: 'a' }, 'a'),
  h('li', { key: 'e' }, 'e'),
  h('li', { key: 'b' }, 'b'),
  h('li', { key: 'c' }, 'c'),
])

diff

还是最小量更新。另外,闪烁法还是不太可靠,建议还是修改Element法


总结

  • 最小量更新:需要key key是节点的唯一标识,用于告诉 diff算法,在更改前后是同一个DOM节点
  • 只有是同一个虚拟节点,才会进行精细化比较,否则就是暴力删除旧的、插入新的。如上面的例子中,从 ul变为 ol

    同一个虚拟节点:选择器相同且 key相同

    // 供测试用:可以使用回上面说的修改Elemens面板法(不过,下面的例子实际开发遇到的可能性很小)
    
    const myVnode1 = h('ul', { key: 'ul1' }, [
    h('li', { key: 'a' }, 'a'),
    h('li', { key: 'b' }, 'b'),
    h('li', { key: 'c' }, 'c'),
    h('li', { key: 'd' }, 'd'),
    ])
    const myVnode2 = h('ul', { key: 'ul2' }, [
    h('li', { key: 'e' }, 'e'),
    h('li', { key: 'a' }, 'a'),
    h('li', { key: 'b' }, 'b'),
    h('li', { key: 'c' }, 'c'),
    h('li', { key: 'd' }, 'd')
    ])
  • 只进行同层比较,不进行跨层比较。如果跨层了,则依然是暴力删除旧的,然后插入新的

    // 下面的例子实际开发遇到的可能性很小
    const myVnode1 = h('div', { key: 'box' }, [
      h('p', { key: 'a' }, 'a'),
      h('p', { key: 'b' }, 'b'),
      h('p', { key: 'c' }, 'c'),
      h('p', { key: 'd' }, 'd'),
    ])
    const myVnode2 = h('div', { key: 'box' },
      h('section', { key: 'section' }, [
        h('p', { key: 'e' }, 'e'),
        h('p', { key: 'a' }, 'a'),
        h('p', { key: 'b' }, 'b'),
        h('p', { key: 'c' }, 'c'),
        h('p', { key: 'd' }, 'd')
      ])
    )

    diff

目录
相关文章
|
2月前
|
算法 数据可视化 数据挖掘
基于EM期望最大化算法的GMM参数估计与三维数据分类系统python源码
本内容展示了基于EM算法的高斯混合模型(GMM)聚类实现,包含完整Python代码、运行效果图及理论解析。程序使用三维数据进行演示,涵盖误差计算、模型参数更新、结果可视化等关键步骤,并附有详细注释与操作视频,适合学习EM算法与GMM模型的原理及应用。
|
6月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
机器学习/深度学习 自然语言处理 算法
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
1329 0
|
9月前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
673 5
|
6天前
|
传感器 机器学习/深度学习 算法
【使用 DSP 滤波器加速速度和位移】使用信号处理算法过滤加速度数据并将其转换为速度和位移研究(Matlab代码实现)
【使用 DSP 滤波器加速速度和位移】使用信号处理算法过滤加速度数据并将其转换为速度和位移研究(Matlab代码实现)
|
8天前
|
机器学习/深度学习 算法 调度
基于NSGA-III算法求解微电网多目标优化调度研究(Matlab代码实现)
基于NSGA-III算法求解微电网多目标优化调度研究(Matlab代码实现)
|
7天前
|
传感器 算法 数据挖掘
基于协方差交叉(CI)的多传感器融合算法matlab仿真,对比单传感器和SCC融合
基于协方差交叉(CI)的多传感器融合算法,通过MATLAB仿真对比单传感器、SCC与CI融合在位置/速度估计误差(RMSE)及等概率椭圆上的性能。采用MATLAB2022A实现,结果表明CI融合在未知相关性下仍具鲁棒性,有效降低估计误差。
|
8天前
|
负载均衡 算法 调度
基于遗传算法的新的异构分布式系统任务调度算法研究(Matlab代码实现)
基于遗传算法的新的异构分布式系统任务调度算法研究(Matlab代码实现)
77 11
|
8天前
|
机器学习/深度学习 传感器 算法
基于全局路径的无人地面车辆的横向避让路径规划研究[蚂蚁算法求解](Matlab代码实现)
基于全局路径的无人地面车辆的横向避让路径规划研究[蚂蚁算法求解](Matlab代码实现)
|
8天前
|
算法 安全 BI
基于粒子群算法的多码头连续泊位分配优化研究(Matlab代码实现)
基于粒子群算法的多码头连续泊位分配优化研究(Matlab代码实现)

热门文章

最新文章