9

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

 3 years ago
source link: https://www.clzczh.top/2022/04/04/vue-virtualDOM-diff-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

什么是虚拟DOM和diff算法

diff算法简介

image-20220317115218615
image-20220317115218615

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

有两种方案。

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

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

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

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

image-20220317121207880
image-20220317121207880

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

虚拟DOM简介

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

image-20220317121849612
image-20220317121849612

diff是发生在虚拟DOM上的

image-20220317154226660
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
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
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
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
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
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
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
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
diff

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

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

diff
diff

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

在前面插入

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

const myVnode2 = h('ul', {}, [
  h('li', {}, 'e'),
  h('li', {}, 'a'),
  h('li', {}, 'b'),
  h('li', {}, 'c'),
  h('li', {}, 'd')
])
diff
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
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
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
    diff

Recommend

  • 37
    • 掘金 juejin.im 6 years ago
    • Cache

    snabbdom 源码阅读分析

    作者: steins from 迅雷前端 原文地址:github.com/linrui1994/… 随着 React Vue 等框架的流行,Virtual DOM 也越来越火,snabbdom 是其中一种实现,而且 Vue 2.x 版本的 Virtual D

  • 43
    • 掘金 juejin.im 6 years ago
    • Cache

    虚拟DOM和Diff算法 - 入门级

    什么是虚拟Dom 我们知道我们平时的页面都是有很多Dom组成,那虚拟Dom(virtual dom)到底是什么,简单来讲,就是将真实的dom节点用JavaScript来模拟出来,而Dom变化的对比,放到 Js 层来做。 下面是一个传统的dom节点,大家肯定都不

  • 34
    • segmentfault.com 6 years ago
    • Cache

    基于虚拟DOM(Snabbdom)的迷你React

    原文链接 原文写于 2015-07-31,虽然时间比较久远,但是对于我们理解虚拟 DOM 和 view 层之间的关系还...

  • 34
    • www.infoq.cn 5 years ago
    • Cache

    Vue 的 diff 算法解析

    1. 前言 diff 算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)。diff 算法的在很多场景下都有应用,例如在 vue 虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较更新时,就...

  • 15
    • zhuanlan.zhihu.com 4 years ago
    • Cache

    手写Vue源码(八) - DOM Diff

    DOM Diff Vue 创建视图分为俩种情况: 首次渲染,会用组件 template 转换成的真实 DOM 来替换应用中的根元素 当数据更新后,视图重新渲染...

  • 5
    • zwkang.com 3 years ago
    • Cache

    snabbdom源码解析

    snabbdom 本文的snabbdom源码分析采用的是0.54版本(即未用ts重写前的最后一版) snabbdom被用作vue的虚拟dom。本文的一个目的就是对于进入vue源码预备。 本文大致讲解,而不会完全细化至代码行数讲解 文件(以下只指出需要阅读...

  • 8

    各位小伙伴新年好啊~新的一年又要开始了,继续努力加油… ~ 求关注,求收藏,求点赞,如果发现博主有写的不合理的地方请及时告知,谢谢~

  • 5
    • www.zoo.team 3 years ago
    • Cache

    浅析snabbdom中vnode和diff算法

    文章目录 一、一些必要的概念解释1、什么是虚拟 DOM2、什么是 diff 算法3、snabbdom 是什么二、snabbdom 中 diff 算法...

  • 5

    Vue源码之虚拟DOM和diff算法(二) 手写diff算法个人练习结果仓库(持续更新):Vue源码解析 patch函数简要流程

  • 8
    • www.fly63.com 2 years ago
    • Cache

    聊聊 Vue 的双端 diff 算法

    vue 和 react 都是基于 vdom 的

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK